安卓MediaRecorder(2)錄制源碼分析

文章目錄

    • 前言
    • JAVA new MediaRecorder() 源碼分析
      • android_media_MediaRecorder.cpp native_init()
      • MediaRecorder.java postEventFromNative
      • android_media_MediaRecorder.cpp native_setup()
    • MediaRecorder 參數設置
    • MediaRecorder.prepare 分析
    • MediaRecorder.start 分析
    • MediaRecorder.stop 分析
    • 結語

本文首發地址 https://blog.csdn.net/CSqingchen/article/details/134634628
最新更新地址 https://gitee.com/chenjim/chenjimblog

前言

通過前文 安卓MediaRecorder(1)錄制音頻的詳細使用,我們已經知道如何使用。
本文主要分析一下 Framework 中相關流程。
下圖是谷歌提供的MediaRecorder狀態關系圖
在這里插入圖片描述

JAVA new MediaRecorder() 源碼分析

public class MediaRecorder implements AudioRouting,AudioRecordingMonitor,AudioRecordingMonitorClient,MicrophoneDirection {static {// 靜態代碼塊,加載鏈接 liblibmedia_jni.soSystem.loadLibrary("media_jni");native_init();}// 已經廢棄 public MediaRecorder() {// 傳入 APP Contextthis(ActivityThread.currentApplication());}public MediaRecorder(@NonNull Context context) {// 要求 Context 不為空Objects.requireNonNull(context);// 創建EventHandler,主要用于JNI層回調時切換到當前App端線程中Looper looper;if ((looper = Looper.myLooper()) != null) {// 如果當前線程有Looper,就使用當前線程的LoopermEventHandler = new EventHandler(this, looper);} else if ((looper = Looper.getMainLooper()) != null) {// 使用主線程的 Looper,Jni回調信息會在主線程執行   mEventHandler = new EventHandler(this, looper);} else {mEventHandler = null;}// 錄制音頻的聲道數,此處默認 1 (mono即單聲道),2 (stereo即雙聲道立體聲),可以通過 setAudioChannels 修改mChannelCount = 1;// 創建弱引用,并初始化try (ScopedParcelState attributionSourceState = context.getAttributionSource().asScopedParcelState()) {native_setup(new WeakReference<>(this), ActivityThread.currentPackageName(),attributionSourceState.getParcel());}}

android_media_MediaRecorder.cpp native_init()

獲取JAVA層 android.media.MediaRecorder 對象
并將 JAVA 對象的屬性 mNativeContext、mSurface、postEventFromNative 保存在 fields
詳細代碼如下

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{jclass clazz;clazz = env->FindClass("android/media/MediaRecorder");if (clazz == NULL) {return;}fields.context = env->GetFieldID(clazz, "mNativeContext", "J");if (fields.context == NULL) {return;}fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");if (fields.surface == NULL) {return;}jclass surface = env->FindClass("android/view/Surface");if (surface == NULL) {return;}fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative","(Ljava/lang/Object;IIILjava/lang/Object;)V");if (fields.post_event == NULL) {return;}clazz = env->FindClass("java/util/ArrayList");if (clazz == NULL) {return;}gArrayListFields.add = env->GetMethodID(clazz, "add", "(Ljava/lang/Object;)Z");gArrayListFields.classId = static_cast<jclass>(env->NewGlobalRef(clazz));
}

MediaRecorder.java postEventFromNative

這個是Jni消息回來的接口,最終會發到 MediaRecorder.EventHandler 的 handleMessage 中
進而可以通過 MediaRecorder.OnInfoListener 、MediaRecorder.OnErrorListener、
AudioRouting.OnRoutingChangedListener 回調到 APP
對應源碼如下

private static void postEventFromNative(Object mediarecorder_ref,int what, int arg1, int arg2, Object obj) {MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();if (mr == null) {return;}if (mr.mEventHandler != null) {Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);mr.mEventHandler.sendMessage(m);}
}// EventHandler 如下  
public class MediaRecorder {...private class EventHandler extends Handler {@Overridepublic void handleMessage(Message msg) {switch(msg.what) {case MEDIA_RECORDER_EVENT_ERROR:case MEDIA_RECORDER_TRACK_EVENT_ERROR:if (mOnErrorListener != null)mOnErrorListener.onError(mMediaRecorder, msg.arg1, msg.arg2);return;case MEDIA_RECORDER_EVENT_INFO:case MEDIA_RECORDER_TRACK_EVENT_INFO:if (mOnInfoListener != null)mOnInfoListener.onInfo(mMediaRecorder, msg.arg1, msg.arg2);return;case MEDIA_RECORDER_AUDIO_ROUTING_CHANGED:// 耳機使能的消息  return;}}}

android_media_MediaRecorder.cpp native_setup()

static void
android_media_MediaRecorder_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,jstring packageName, jobject jAttributionSource)
{// 構建 JNI 對象 MediaRecorder,attributionSource 可以認為是 JNI 中的上下文 Context sp<MediaRecorder> mr = new MediaRecorder(attributionSource);...// 創建 JNI JNIMediaRecorderListener , 其收到的消息最終通過 fields.post_event 回到JAVAsp<JNIMediaRecorderListener> listener = new JNIMediaRecorderListener(env, thiz, weak_this);mr->setListener(listener);...// 傳遞客戶端包名,以進行權限跟蹤  mr->setClientName(clientName);// 將創建的 mr 保存到 fields.context,也就是 Java 層 MediaRecorder 中的 mNativeContext  setMediaRecorder(env, thiz, mr);
}

MediaRecorder JNI 構造

// frameworks/av/media/libmedia/mediarecorder.cpp
MediaRecorder::MediaRecorder(const AttributionSourceState &attributionSource): mSurfaceMediaSource(NULL)
{// 通過 binder 獲取 MediaPlayerService const sp<IMediaPlayerService> service(getMediaPlayerService());if (service != NULL) {// 通過 MediaPlayerService 創建 MediaRecorderClient  mMediaRecorder = service->createMediaRecorder(attributionSource);}...
}

MediaRecorder 類關系如下

class MediaRecorder : public BnMediaRecorderClient, public virtual IMediaDeathNotifier {...}
class BnMediaRecorderClient: public BnInterface<IMediaRecorderClient> {...}

MediaPlayerService 類關系如下

class MediaPlayerService : public BnMediaPlayerService {...}
class BnMediaPlayerService: public BnInterface<IMediaPlayerService> {...}

MediaPlayerService 中 createMediaRecorder 如下

// frameworks/av/media/libmediaplayerservice/MediaPlayerService.cpp
sp<IMediaRecorder> MediaPlayerService::createMediaRecorder(const AttributionSourceState& attributionSource)
{...sp<MediaRecorderClient> recorder = new MediaRecorderClient(this, verifiedAttributionSource);...return recorder;
}

MediaRecorderClient 構造如下

// frameworks/av/media/libmediaplayerservice/MediaRecorderClient.cpp
MediaRecorderClient::MediaRecorderClient(const sp<MediaPlayerService>& service,const AttributionSourceState& attributionSource)
{// 構造StagefrightRecorder,mRecorder = new StagefrightRecorder(attributionSource);mMediaPlayerService = service;
}StagefrightRecorder 類繼承關系如下 
struct StagefrightRecorder : public MediaRecorderBase {...}

到此 JAVA 層 new MediaRecord() 相關源碼已經分析完成

MediaRecorder 參數設置

Java層 setAudioSource 、 setOutputFormat 等均調用了 Native 接口,下面以 setAudioSource 為例

// frameworks/base/media/java/android/media/MediaRecorder.java
public native void setAudioSource(@Source int audioSource) throws IllegalStateException;
// frameworks/base/media/jni/android_media_MediaRecorder.cpp  
static void android_media_MediaRecorder_setAudioSource(JNIEnv *env, jobject thiz, jint as)
{...// 讀取初始化時保存的mrsp<MediaRecorder> mr = getMediaRecorder(env, thiz);if (mr == NULL) {jniThrowException(env, "java/lang/IllegalStateException", NULL);return;}process_media_recorder_call(env, mr->setAudioSource(as), "java/lang/RuntimeException", "setAudioSource failed.");
}// frameworks/av/media/libmedia/mediarecorder.cpp  
status_t MediaRecorder::setVideoSource(int vs)
{...// 上面知道,這里的 mMediaRecorder 是 MediaRecorderClient status_t ret = mMediaRecorder->setVideoSource(vs);return ret;
}// frameworks/av/media/libmediaplayerservice/MediaRecorderClient.cpp
status_t MediaRecorderClient::setVideoSource(int vs)
{...// 上面知道,這里的 mRecorder 是 StagefrightRecorder return mRecorder->setVideoSource((video_source)vs);
}// frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::setVideoSource(video_source vs) {...// 最終數據保存在 mVideoSource 中 if (vs == VIDEO_SOURCE_DEFAULT) {mVideoSource = VIDEO_SOURCE_CAMERA;} else {mVideoSource = vs;}return OK;
}

同理,其它參數設置多數最終都是保存在 StagefrightRecorder 中,錄制相關的流程很多也是在其中控制

本文首發地址 https://blog.csdn.net/CSqingchen/article/details/134634628

MediaRecorder.prepare 分析

依據前面的分析,最終 prepare 真正實現如下

// frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::prepareInternal() {...status_t status = OK;// 依據不同的輸出格式,執行不同的 setup switch (mOutputFormat) {case OUTPUT_FORMAT_DEFAULT:case OUTPUT_FORMAT_THREE_GPP:case OUTPUT_FORMAT_MPEG_4:case OUTPUT_FORMAT_WEBM:status = setupMPEG4orWEBMRecording();break;case OUTPUT_FORMAT_AMR_NB:case OUTPUT_FORMAT_AMR_WB:status = setupAMRRecording();break;case OUTPUT_FORMAT_AAC_ADIF:case OUTPUT_FORMAT_AAC_ADTS:status = setupAACRecording();break;case OUTPUT_FORMAT_RTP_AVP:status = setupRTPRecording();break;case OUTPUT_FORMAT_MPEG2TS:status = setupMPEG2TSRecording();break;case OUTPUT_FORMAT_OGG:status = setupOggRecording();break;default:ALOGE("Unsupported output file format: %d", mOutputFormat);status = UNKNOWN_ERROR;break;}return status;
}

這里我們分析一下錄制 MP4 的 prepare 流程 setupMPEG4orWEBMRecording

status_t StagefrightRecorder::setupMPEG4orWEBMRecording() {// 先清理 MediaWriter mWriter.clear();mTotalBitRate = 0;status_t err = OK;sp<MediaWriter> writer;sp<MPEG4Writer> mp4writer;if (mOutputFormat == OUTPUT_FORMAT_WEBM) {writer = new WebmWriter(mOutputFd);} else {// 我們這里分析 MP4 錄制,MPEG4Writer 主要是用來寫入編碼后的音視頻內容   writer = mp4writer = new MPEG4Writer(mOutputFd);}if (mVideoSource < VIDEO_SOURCE_LIST_END) {// 如果編碼器未配置,設置默認的編碼器  setDefaultVideoEncoderIfNecessary();sp<MediaSource> mediaSource;// 設置 視頻源err = setupMediaSource(&mediaSource);if (err != OK) {return err;}sp<MediaCodecSource> encoder;// 編碼參數配置,然后通過 MediaCodecSource::Create 創建編碼器err = setupVideoEncoder(mediaSource, &encoder);if (err != OK) {return err;}// MPEG4Writer 添加編碼通道,一般會有audio、video 兩個,這里是 videowriter->addSource(encoder);mVideoEncoderSource = encoder;// 輸出文件的碼率,是視頻和音頻總碼率之和mTotalBitRate += mVideoBitRate;}// Audio source is added at the end if it exists.// This help make sure that the "recoding" sound is suppressed for// camcorder applications in the recorded files.// disable audio for time lapse recordingconst bool disableAudio = mCaptureFpsEnable && mCaptureFps < mFrameRate;if (!disableAudio && mAudioSource != AUDIO_SOURCE_CNT) {// 通過  createAudioSource() 配置音頻編碼參數,進而通過 MediaCodecSource::Create 創建 編碼器err = setupAudioEncoder(writer);if (err != OK) return err;mTotalBitRate += mAudioBitRate;}...// 監聽 MPEG4Writer 一些參數的回調 writer->setListener(mListener);mWriter = writer;return OK;
}

通過如上,可以看到 MediaRecorder.prepare 主要是進行參數的配置、編碼器的初始化

MediaRecorder.start 分析

依據前面的分析,最終 start 真正實現如下

//  frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::start() {Mutex::Autolock autolock(mLock);if (mOutputFd < 0) {ALOGE("Output file descriptor is invalid");return INVALID_OPERATION;}status_t status = OK;if (mVideoSource != VIDEO_SOURCE_SURFACE) {status = prepareInternal();if (status != OK) {return status;}}switch (mOutputFormat) {case OUTPUT_FORMAT_DEFAULT:case OUTPUT_FORMAT_THREE_GPP:case OUTPUT_FORMAT_MPEG_4:case OUTPUT_FORMAT_WEBM:{sp<MetaData> meta = new MetaData;// 設置 meta 信息setupMPEG4orWEBMMetaData(&meta);// MPEG4Writer 傳入 meta 信息,// startWriterThread 開啟寫入線程 // setupAndStartLooper 啟動 ALooper, mReflector 用于信息傳遞 // 通過 writeFtypBox(MetaData *param) 寫入// startTracks(MetaData *params) 啟動音、視頻Track,參見 MPEG4Writer::Track::startstatus = mWriter->start(meta.get());break;}...}if (status != OK) {// start 異常 mWriter.clear();mWriter = NULL;}if ((status == OK) && (!mStarted)) {mAnalyticsDirty = true;mStarted = true;...// 用于編碼耗電統計 addBatteryData(params);}return status;
}

MediaRecorder.stop 分析

依據前面的分析,最終 stop 真正實現如下

//  frameworks/av/media/libmediaplayerservice/StagefrightRecorder.cpp
status_t StagefrightRecorder::stop() {Mutex::Autolock autolock(mLock);status_t err = OK;if (mCaptureFpsEnable && mCameraSourceTimeLapse != NULL) {// 延時錄制,詳細可參見 CameraSourceTimeLapse.cppmCameraSourceTimeLapse->startQuickReadReturns();mCameraSourceTimeLapse = NULL;}int64_t stopTimeUs = systemTime() / 1000;for (const auto &source : { mAudioEncoderSource, mVideoEncoderSource }) {// 設置停止時間戳if (source != nullptr && OK != source->setStopTimeUs(stopTimeUs)) {}}if (mWriter != NULL) {// MPEG4Writer 的停止 ,實際調用其  reset(true, true)// stopWriterThread() 停止寫的線程// Track 停止// release 關閉文件,停止釋放Looper,資源狀態重置err = mWriter->stop();mLastSeqNo = mWriter->getSequenceNum();mWriter.clear();}// 寫入參數相關信息flushAndResetMetrics(true);// 重置參數狀態mDurationRecordedUs = 0;mDurationPausedUs = 0;mNPauses = 0;mTotalPausedDurationUs = 0;mPauseStartTimeUs = 0;mStartedRecordingUs = 0;mGraphicBufferProducer.clear();mPersistentSurface.clear();mAudioEncoderSource.clear();mVideoEncoderSource.clear();......return err;
}

結語

到這里,已經完成了 MediaRecorder 錄制 Framework 源碼的分析。
其它部分流程,可以對照參見 StagefrightRecorder.cpp 中源碼。希望對你有所幫助。
如果你在使用MediaRecorder的過程中遇到了其他問題,歡迎留言討論。
如果你覺得本文還不錯,可以點贊+收藏。


相關文章
安卓MediaRecorder(1)錄制音頻的詳細使用
安卓MediaRecorder(2)錄制源碼分析
安卓MediaRecorder(3)音頻采集編碼寫入詳細源碼分析
安卓MediaRecorder(4)視頻采集編碼寫入詳細源碼分析

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/211666.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/211666.shtml
英文地址,請注明出處:http://en.pswp.cn/news/211666.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

當前 .NET SDK 不支持面向 .NET X.0 (如8.0)問題的解決方案

如果您加載方案或運行時出現如下錯誤時&#xff1a; 當前 .NET SDK 不支持面向 .NET 8.0。請面向 .NET 7.0 或更低版本&#xff0c;或者使用支持 .NET 8.0 的 .NET SDK 版本。從 https://aka.ms/dotnet/download 下載 .NET SDK (項目名稱).Domain C:\Program Files\dotnet\…

Windows在cmd中執行bat腳本

在Linux中執行腳本常用的是sh或者直接輸入腳本名稱即可。 sh shell腳本.sh # 或者 shell腳本.sh在Windows中類似&#xff0c;使用start或者直接輸入腳本名稱。 start bat腳本.bat :: 或者 bat腳本.bat

【Angular開發】Angular在2023年之前不是很好

做一個簡單介紹&#xff0c;年近48 &#xff0c;有20多年IT工作經歷&#xff0c;目前在一家500強做企業架構&#xff0e;因為工作需要&#xff0c;另外也因為興趣涉獵比較廣&#xff0c;為了自己學習建立了三個博客&#xff0c;分別是【全球IT瞭望】&#xff0c;【架構師酒館】…

SSL證書更新

首先&#xff0c;我們需要理解為什么需要更新SSL證書。SSL證書的有效期通常為一年。一旦證書過期&#xff0c;瀏覽器會顯示警告&#xff0c;提示用戶該網站的SSL證書已經過期&#xff0c;這可能會導致用戶對網站的信任度下降&#xff0c;甚至直接離開網站。此外&#xff0c;一些…

【Python】手把手教你用tkinter設計圖書管理登錄UI界面(一)

下一篇&#xff1a; 本項目將分段設計“圖書管理登錄UI界面”的用戶登錄、用戶注冊、用戶賬號找回等。主要圍繞GUI標準庫tkinter、以及類的繼承&#xff08;重點&#xff09;來設計本項目。 首先新建一個文件夾命名為“圖書管理系統項目”&#xff0c;并在其目錄下新建文件夾…

【分治】最接近點對Python實現

文章目錄 [toc]問題描述一維最接近點對算法Python實現 二維最接近點對算法分治算法時間復雜性Python實現 問題描述 給定平面上 n n n個點&#xff0c;找其中的一對點&#xff0c;使得在 n n n個點組成的所有點對中&#xff0c;該點對的距離最小 一維最接近點對算法 Python實…

LED透鏡粘接UV膠是一種特殊的UV固化膠,用于固定和粘合LED透鏡。

LED透鏡粘接UV膠是一種特殊的UV固化膠&#xff0c;用于固定和粘合LED透鏡。 它具有以下特點&#xff1a; 1. 高透明度&#xff1a;LED透鏡粘接UV膠具有高透明度&#xff0c;可以確保光線的透過性&#xff0c;不影響LED的亮度和效果。 2. 快速固化&#xff1a;經過UV紫外線照射…

CPU、MCU、MPU、DSP、FPGA各是什么?有什么區別?

1、CPU 中央處理器&#xff0c;簡稱 CPU&#xff08;Central Processing Unit&#xff09;&#xff0c;中央處理器主要包括兩個部分&#xff0c;即控制器、運算器&#xff0c;其中還包括高速緩沖存儲器及實現它們之間聯系的數據、控制的總線。 電子計算機三大核心部件就是CPU…

力扣257. 二叉樹的所有路徑(遞歸回溯與迭代)

題目&#xff1a; 給你一個二叉樹的根節點 root &#xff0c;按 任意順序 &#xff0c;返回所有從根節點到葉子節點的路徑。 葉子節點 是指沒有子節點的節點。 示例 1&#xff1a; 輸入&#xff1a;root [1,2,3,null,5] 輸出&#xff1a;["1->2->5","…

[隴劍杯 2021]簡單日志分析

[隴劍杯 2021]簡單日志分析 題目做法及思路解析&#xff08;個人分享&#xff09; 問一&#xff1a;某應用程序被攻擊&#xff0c;請分析日志后作答&#xff1a; 黑客攻擊的參數是______。&#xff08;如有字母請全部使用小寫&#xff09;。 題目思路&#xff1a; 分析…

C++牛客知識點2

提示&#xff1a;接上文 文章目錄 前言一、pandas是什么&#xff1f;二、使用步驟 1.引入庫2.讀入數據總結 前言 提示&#xff1a;這里可以添加本文要記錄的大概內容&#xff1a; 例如&#xff1a;隨著人工智能的不斷發展&#xff0c;機器學習這門技術也越來越重要&#xff0…

http與https的區別,以及生產環境配置https的幾種方式

http HTTP(超文本傳輸協議)是一種用于傳輸和處理超文本文檔的協議。HTTP使用客戶端-服務器模型。客戶端通過HTTP請求協議向服務器發送請求&#xff0c;服務器則使用HTTP響應協議返回響應。HTTP協議通常使用TCP/IP作為底層傳輸協議&#xff0c;但它也可以使用其他傳輸協議。 H…

sql注入學習

基礎查詢語句&#xff1a; 給指定字段添加數據 insert into 表名(字段名1,字段名2,.....) values(值1,值2,......); 給全部字段添加數據 insert into 表名 values (值1,值2,.....);--無限制條件的修改,會修改整張表 update 表名 set 字段 值; --有限制條件的修改,只修改特定記…

軟件設計師——計算機網絡(二)

&#x1f4d1;前言 本文主要是【計算機網絡】——軟件設計師——計算機網絡的文章&#xff0c;如果有什么需要改進的地方還請大佬指出?? &#x1f3ac;作者簡介&#xff1a;大家好&#xff0c;我是聽風與他&#x1f947; ??博客首頁&#xff1a;CSDN主頁聽風與他 &#x1…

Promise介紹和使用

Promise Promise是一門新的技術&#xff08;ES6規范&#xff09;&#xff0c;JS中進行異步編程的新解決方案。&#xff08;舊的方案是使用回調函數&#xff0c;比如AJAX請求&#xff09;。 從語法上來說Promise是一個構造函數。 從功能上來說Promise對象用來封裝一個異步操作并…

生成式AI賦能千行百業加速創新,2023亞馬遜云科技re:Invent行業盤點

2023亞馬遜云科技re:Invent全球大會已于上周圓滿閉幕&#xff0c;在本次大會中&#xff0c;亞馬遜云科技又為大家帶來了很多功能/項目迭代更新&#xff0c;也重磅發布了很多全新的功能。今天從行業視角來盤點回顧哪些重磅發布適用于垂直行業客戶&#xff0c;以及面向汽車、制造…

ChatGLM3-6B和langchain阿里云部署

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、ChatGLM3-6B部署搭建環境部署GLM3 二、Chatglm2-6blangchain部署三、Tips四、總結 前言 提示&#xff1a;這里可以添加本文要記錄的大概內容&#xff1a; …

ffmpeg之ffprobe.c源碼分析一---大流程及核心代碼分析

文章目錄 前言為什么學習ffprobe源碼源碼調試main()函數重要流程函數分析open_input_file函數分析avformat_match_stream_specifier函數分析read_packets函數分析本篇文章帶你打通ffprobe源碼的脈絡。 關注公眾號免費看: 前言 注:本文章全憑個人經驗以及平時學習所記錄,由…

gdal合成多個波段

def synthesis_bands(dst_list, outfile):"""將多光譜波段合成一個tif:param dst_list: 輸入待合成文件的列表:param outfile: 影像的輸出文件夾"""dataset_init gdal.Open(dst_list[0])# 創建待輸出的圖tiff_driver gdal.GetDriverByName(GTi…

【MySQL進階】索引使用

一、索引使用 1.驗證索引效率 tb_sku 這張表中準備了 1000w 的記錄。 我用夸克網盤分享了「1000w的模擬數據」鏈接&#xff1a;https://pan.quark.cn/s/15cf665202b2 這張表中id為主鍵&#xff0c;有主鍵索引&#xff0c;而其他字段是沒有建立索引的。 我們先來查詢其中的…