MediaScanner 分析 一 MediaScannerService 多媒體掃描是從 MediaScannerService 開始的,。這是一個單獨的 package ,。位于 packages\providers\MediaProvider :含以下 java 文件 l MediaProvider.java l MediaScannerReceiver.java l MediaScannerService.java l MediaThumbRequest.java 分析這個目錄的 Android.mk 文件,發(fā)現(xiàn)它運行的進程名字就是 android.process.media ,。 application android:process = android.process.media 1.1 MediaScannerReceiver這個類從 BroadcastReceiver 中派生,,用來接收任務的。 MediaScannerReceiver extends BroadcastReceiver 在它重載的onRecieve 函數(shù)內有以下幾種走向: if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { // 收到” 啟動完畢“廣播后,,掃描內部存儲 scan(context, MediaProvider.INTERNAL_VOLUME); } else { ………. if (action.equals(Intent.ACTION_MEDIA_MOUNTED) && externalStoragePath.equals(path)) { / 收到MOUNT 信息后,,掃描外部存儲 scan(context, MediaProvider.EXTERNAL_VOLUME); } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) && path != null && path.startsWith(externalStoragePath + "/")) { // 收到請求要求掃描某個文件,,注意不會掃描內部存儲上的文件 scanFile(context, path); ………………………….. } …… 下面是它調用的scan 函數(shù): scan(Context context, String volume) Bundle args = new Bundle(); args.putString("volume", volume); // 直接啟動MediaScannerService 了, context.startService( new Intent(context, MediaScannerService.class).putExtras(args));
總結: MediaScannerReceiver 是用來接收任務的,,它收到廣播后,,會啟動 MediaService 進行掃描工作。 下面看看 MediaScannerService. 1.2 MediaScannerServiceMSS 標準的從 Service 中派生下來,, MediaScannerService extends Service implements Runnable // 注意:是一個Runnable… ,,可能有線程之類的東西存在 下面從 Service 的生命周期的角度來看看它的工作。 1. onCreate public void onCreate() PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); // 獲得電源鎖,,防止在掃描過程中休眠 // 單獨搞一個線程去跑掃描工作,,防止ANR Thread thr = new Thread(null, this, "MediaScannerService"); thr.start(); 2. onStartCommand @Override public int onStartCommand(Intent intent, int flags, int startId) { // 注意這個handler ,是在另外一個線程中創(chuàng)建的,,往這個handler 里sendMessage // 都會在那個線程里邊處理 // 不明白的可以去查看handler 和Looper 機制 // 這里就是同步機制,,等待mServiceHandler 在另外那個線程創(chuàng)建完畢 while (mServiceHandler == null) { synchronized (this) { try { wait(100); } catch (InterruptedException e) { } } }
if (intent == null) { Log.e(TAG, "Intent is null in onStartCommand: ", new NullPointerException()); return Service.START_NOT_STICKY; }
Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent.getExtras(); // 把MediaScannerReceiver 發(fā)出的消息傳遞到另外那個線程去處理。 mServiceHandler.sendMessage(msg); …………. 基本上 MSR(MediaScannerReceiver) 發(fā)出的請求都會傳到 onStartCommand 中處理,。如果有多個存儲的話,,也只能一個一個掃描了。 下面看看那個線程的主函數(shù) 3. run public void run() { // reduce priority below other background threads to avoid interfering // with other services at boot time. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE); // 不明白的去看看Looper 和handler 的實現(xiàn) Looper.prepare();// 把這個looper 對象設置到線程本地存儲
mServiceLooper = Looper.myLooper(); mServiceHandler = new ServiceHandler();// 創(chuàng)建handler ,,默認會把這個looper // 的消息隊列賦值給handler 的消息隊列,,這樣往handler 中發(fā)送消息就是往這個線程的looper 發(fā)
Looper.loop();// 消息循環(huán),內部會處理消息隊列中的消息 // 也就是handleMessage 函數(shù) } 上面 handler 中加入了一個掃描請求(假設是外部存儲的),,所以要分析 handleMessage 函數(shù),。 4. handleMessage private final class ServiceHandler extends Handler { @Override public void handleMessage(Message msg) { Bundle arguments = (Bundle) msg.obj; String filePath = arguments.getString("filepath");
try { ……… 這里不講了 } else { String volume = arguments.getString("volume"); String[] directories = null; if (MediaProvider.INTERNAL_VOLUME.equals(volume)) { // 是掃描內部存儲的請求? // scan internal media storage directories = new String[] { Environment.getRootDirectory() + "/media", }; } else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) { // 是掃描外部存儲的請求? 獲取外部存儲的路徑 directories = new String[] { Environment.getExternalStorageDirectory().getPath(), }; } if (directories != null) { // 真正的掃描開始了,,上面只不過是把存儲路徑取出來罷了. scan(directories, volume); ….. // 掃描完了,,就把service 停止了 stopSelf(msg.arg1); } }; 5. scan 函數(shù) private void scan(String[] directories, String volumeName) { mWakeLock.acquire(); // 下面這三句話很深奧… // 從 getContentResolver 獲得一個ContentResover ,然后直接插入 // 根據(jù)AIDL ,,這個ContentResover 的另一端是MediaProvider ,。只要去看看它的 //insert 函數(shù)就可以了 // 反正這里知道獲得了一個掃描URI 即可。 ContentValues values = new ContentValues(); values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName); Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
Uri uri = Uri.parse("file://" + directories[0]); // 發(fā)送廣播,,通知掃描開始了 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
try { if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) { openDatabase(volumeName); } // 創(chuàng)建真正的掃描器 MediaScanner scanner = createMediaScanner(); // 交給掃描器去掃描文件夾 scanDirectories scanner.scanDirectories(directories, volumeName); } catch (Exception e) { Log.e(TAG, "exception in MediaScanner.scan()", e); } // 刪除掃描路徑 getContentResolver().delete(scanUri, null, null); // 通知掃描完畢 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri)); mWakeLock.release(); } 說說上面那個深奧的地方,,在 MediaProvider 中重載了 insert 函數(shù), insert 函數(shù)會調用 insertInternal 函數(shù),。 如下: private Uri insertInternal(Uri uri, ContentValues initialValues) { long rowId; int match = URI_MATCHER.match(uri); // handle MEDIA_SCANNER before calling getDatabaseForUri() // 剛才那個insert 只會走下面這個分支,,其實就是獲得一個地址…. // 太繞了!?。,。?! if (match == MEDIA_SCANNER) { mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); return MediaStore.getMediaScannerUri(); } …….. 再看看它創(chuàng)建了什么樣的 Scanner ,,這就是 MSS 中的 createMediaScanner private MediaScanner createMediaScanner() { // 下面這個MediaScanner 在framework/base/ 中,,待會再分析 MediaScanner scanner = new MediaScanner(this); // 設置當前的區(qū)域,這個和字符編碼有重大關系,。 Locale locale = getResources().getConfiguration().locale; if (locale != null) { String language = locale.getLanguage(); String country = locale.getCountry(); String localeString = null; if (language != null) { if (country != null) { // 給掃描器設置當前國家和語言,。 scanner.setLocale(language + "_" + country); } else { scanner.setLocale(language); } } } return scanner; } 至此, MSS 的任務完成了,。接下來是 MediaScanner 的工作了,。 6. 總結 MSS 的工作流程如下: l 1 單獨啟動一個帶消息循環(huán)的工作線程。 l 2 主線程接收系統(tǒng)發(fā)來的任務,,然后發(fā)送給工作線程去處理,。 l 3 工作線程接收任務,創(chuàng)建一個 MediaScanner 去掃描,。 l 4 MSS 順帶廣播一下掃描工作啟動了,,掃描工作完畢了。
二 MediaScanner MediaScanner 位置在 frameworks\base\media\ 下,,包括 jni 和 java 文件,。 先看看 java 實現(xiàn)。 這個類巨復雜,,而且和 MediaProvider 交互頻繁,。在分析的時候要時刻回到 MediaProvider 去看看。 1. 初始化 public class MediaScanner { static { //libmedia_jni.so 的加載是在MediaScanner 類中完成的 // 這么重要的so 為何放在如此不起眼的地方加載,?,?? System.loadLibrary("media_jni"); native_init(); } public MediaScanner(Context c) { native_setup();// 調用jni 層的初始化,,暫時不用看了,,無非就是一些 // 初始化工作,待會在再進去看看 …….. } 剛才 MSS 中是調用 scanDirectories 函數(shù),,我們看看這個。 2. scanDirectories public void scanDirectories(String[] directories, String volumeName) { try { long start = System.currentTimeMillis(); initialize(volumeName);// 初始化 prescan(null);// 掃描前的預處理 long prescan = System.currentTimeMillis();
for (int i = 0; i < directories.length; i++) { // 掃描文件夾,,這里有一個很重要的參數(shù) mClient // processDirectory 是一個native 函數(shù) processDirectory(directories[i], MediaFile.sFileExtensions, mClient); } long scan = System.currentTimeMillis(); postscan(directories);// 掃描后處理 long end = System.currentTimeMillis(); ….. 打印時間,,異常處理… 沒了… 下面簡單講講 initialize , preScan 和 postScan 都干嘛了,。 private void initialize(String volumeName) { // 打開MediaProvider ,,獲得它的一個實例 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); // 得到一些uri mAudioUri = Audio.Media.getContentUri(volumeName); mVideoUri = Video.Media.getContentUri(volumeName); mImagesUri = Images.Media.getContentUri(volumeName); mThumbsUri = Images.Thumbnails.getContentUri(volumeName); // 外部存儲的話,可以支持播放列表之類的東西,,搞了一些個緩存池之類的 // 如mGenreCache 等 if (!volumeName.equals("internal")) { // we only support playlists on external media mProcessPlaylists = true; mGenreCache = new HashMap<String, Uri>(); … preScan ,,這個函數(shù)很復雜: 大概就是創(chuàng)建一個 FileCache ,用來緩存掃描文件的一些信息,,例如 last_modified 等,。這個 FileCache 是從 MediaProvider 中已有信息構建出來的,,也就是歷史信息。后面根據(jù)掃描得到的新信息來對應更新歷史信息,。 postScan, 這個函數(shù)做一些清除工作,,例如以前有 video 生成了一些縮略圖,現(xiàn)在 video 文件被干掉了,,則對應的縮略圖也要被干掉,。 另外還有一個 mClient ,這個是從 MediaScannerClient 派生下來的一個東西,,里邊保存了一個文件的一些信息,。后續(xù)再分析。
剛才說到,,具體掃描工作是在 processDirectory 函數(shù)中完成的,。這個是一個 native 函數(shù)。 在 frameworks\base\media\jni\android_media_MediaScanner.cpp 中,。
三 MediaScanner JNI 層分析 MediaScanner JNI 層內容比較多,,單獨搞一節(jié)分析吧。 先看看 android_media_MediaScanner 這個文件,。 1. native_init 函數(shù),, jni 對應的函數(shù)如下 static void android_media_MediaScanner_native_init(JNIEnv *env) { jclass clazz; clazz = env->FindClass("android/media/MediaScanner"); // 得都JAVA 類中mNativeContext 這個成員id fields.context = env->GetFieldID(clazz, "mNativeContext", "I"); // 不熟悉JNI 的自己去學習下吧 } 3. native_setup 函數(shù), jni 對應函數(shù)如下: android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz) { // 創(chuàng)建MediaScanner 對象 MediaScanner *mp = createMediaScanner(); // 太變態(tài)了,,自己不保存這個對象指針. // 卻把它設置到java 對象的mNativeContext 去保存 env->SetIntField(thiz, fields.context, (int)mp); } // 創(chuàng)建MediaScanner 函數(shù) static MediaScanner *createMediaScanner() { #if BUILD_WITH_FULL_STAGEFRIGHT .. // 使用google 自己的 return new StagefrightMediaScanner; #endif #ifndef NO_OPENCORE // 使用opencore 提供的 …. return new PVMediaScanner(); #endif
4. processDirectories 函數(shù),, jni 對應如下: android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client) { MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context); // 每次都要回調到JAVA 中去取這個Scanner !,! ……… const char *pathStr = env->GetStringUTFChars(path, NULL); const char *extensionsStr = env->GetStringUTFChars(extensions, NULL); ……. // 又在C++ 這里搞一個client ,,然后把java 的client 放到C++Client 中去保存 |
|