Android媒體掃描詳細解析之一(MediaScanner & MediaProvider)

用過Android手機的同學都知道,每次開機的時候系統會先掃描sdcard,sdcard重新插拔(掛載)也會掃描一次sdcard。

爲什麼要掃描sdcard,其實是爲了給系統的其他應用提供便利,比如,Gallary、Music、VideoPlayer等應用,進入Gallary後會顯示sdcard中的所有圖片,

如果進入Gallary後再去掃描,可想而知,你會厭惡這個應用,因爲我們會覺得它反應太慢了。還有Music你看到播放列表的時候實際能看到這首歌曲的時長、演唱者、專輯

等信息,這個也不是你進入應用後一下子可以讀出來的。

所以Android使用sdcard掛載後掃描的機制,先將這些媒體相關的信息掃描出來保存在數據庫中,當打開應用的時候直接去數據庫讀取(或者所通過MediaProvider去從數據庫讀取)並show給用戶,這樣用戶體驗會好很多,下面我們分析這種掃描機制是如何實現的。

在源碼目錄的\packages\providers\MediaProvider下面是MediaProvider的源碼,它就是完成掃描並將數據保存於數據庫中的程序。

先看下它的AndroidManifest.xml文件

application android:process="android.process.media",也就是應用程序名稱爲android.process.media,我們用adb 連接到android 設備,並且進入shell後輸入ps可以看到的確有應用程序app_4     2796  2075  165192 19420 ffffffff 6fd0eb58 S android.process.media 在運行。另外此程序中有三個部分,分別是provider - MediaProvider 、receiver - MediaScannerReceiver、service - MediaScannerService,它並沒有activity,說明它是一直運行於後臺的程序。並且從receiver中的

<intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>

可以看出它是開機自啓動的。下面從這個廣播開始看代碼。

public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Uri uri = intent.getData();
        String externalStoragePath = Environment.getExternalStorageDirectory().getPath();

        if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
            // scan internal storage
            scan(context, MediaProvider.INTERNAL_VOLUME);
        } else {
     。 。 。
     }
  }
}

收到開機廣播後,首先執行scan函數STEP 1,

private void scan(Context context, String volume) {
        Bundle args = new Bundle();
        args.putString("volume", volume);
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    }  

scan函數主要傳進來一個volume卷名,MediaProvider.INTERNAL_VOLUME實際就是內置存儲卡"internal",在此我們首先理解爲開機後首先掃描內置存儲卡。

然後啓動services MediaScannerService,這也是此服務第一次被啓動。

service的啓動流程就不說了,onCreate肯定是首先被調用的

 public void onCreate()
    {
        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

        // Start up the thread running the service.  Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block.
        Thread thr = new Thread(null, this, "MediaScannerService");
        thr.start();
    }

此處前面是申請了一把wake lock ,主要是防止CPU休眠的,然後啓動了一個線程實際就是 MediaScannerService自身的線程,它繼承自Runnable,下面主要看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.prepare();

        mServiceLooper = Looper.myLooper();
        mServiceHandler = new ServiceHandler();

        Looper.loop();
    }

可以看出此線程的目的是爲了處理hander消息ServiceHandler

執行完onCreate後就會執行onStartCommand

 public int onStartCommand(Intent intent, int flags, int startId)
    {
        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();
        mServiceHandler.sendMessage(msg);

        // Try again later if we are killed before we can finish scanning.
        return Service.START_REDELIVER_INTENT;
    }

在這裏我們通過STEP 1中傳入進來的volume字符串就作爲了msg.obj通過handler來處理了

 private final class ServiceHandler extends Handler
    {
        @Override
        public void handleMessage(Message msg)
        {
            Bundle arguments = (Bundle) msg.obj;
            String filePath = arguments.getString("filepath");
            String folder = arguments.getString("folder");
            try {
                if (filePath != null) {
                    IBinder binder = arguments.getIBinder("listener");
                    IMediaScannerListener listener = 
                            (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
                    Uri uri = scanFile(filePath, arguments.getString("mimetype"));
                    if (listener != null) {
                        listener.scanCompleted(filePath, uri);
                    }
                } else if(folder != null) {
                    String volume = arguments.getString("volume");
                    String[] directories = null;
                    directories = new String[] {
                        new File(folder).getPath(),
                    };
                    if (directories != null) {
                        if (Config.LOGD) Log.d(TAG, "start scanning volume " + volume + " ; path = " + folder);
                        scan(directories, volume);
                        if (Config.LOGD) Log.d(TAG, "done scanning volume " + volume + " ; path = " + folder);
                    }
                }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)) {
                        // scan external storage
                        directories = new String[] {
                                Environment.getExternalStorageDirectory().getPath(),
                                };
                    }
                    
                    if (directories != null) {
                        if (Config.LOGD) Log.d(TAG, "start scanning volume " + volume);
                        scan(directories, volume);
                        if (Config.LOGD) Log.d(TAG, "done scanning volume " + volume);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception in handleMessage", e);
            }

            stopSelf(msg.arg1);
        }
    };
}
很顯然我們會執行到標記爲紅色的else中,我們是先掃描內置sdcard,很顯然directories的值爲/system/media ,然後調用 scan(directories, volume);函數,應該是內置sdcard中所有的媒體文件都幾種存儲在/system/media下面所以只需要掃描這一個路徑就行了。STEP2

private void scan(String[] directories, String volumeName) {
        // don't sleep while scanning
        mWakeLock.acquire();

        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]);
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
        
        try {
            if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                 openDatabase(volumeName);    
            }

            MediaScanner scanner = createMediaScanner();
            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();
    }

開始掃描和結束掃描時都會發送一個全局的廣播,第三方應用程序也可以通過註冊這兩個廣播來避開在media 掃描的時候往改掃描文件夾裏面寫入或刪除文件,這個我在項目中就遇到過這種bug。在這一步驟中創建了MediaScanner並調用它的scanDirectories方法   STEP3

  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++) {
                processDirectory(directories[i], MediaFile.sFileExtensions, mClient);
            }
            long scan = System.currentTimeMillis();
            postscan(directories);
            long end = System.currentTimeMillis();

            if (Config.LOGD) {
                Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
                Log.d(TAG, "    scan time: " + (scan - prescan) + "ms\n");
                Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
                Log.d(TAG, "   total time: " + (end - start) + "ms\n");
            }
        } catch (SQLException e) {
            // this might happen if the SD card is removed while the media scanner is running
            Log.e(TAG, "SQLException in MediaScanner.scan()", e);
        } catch (UnsupportedOperationException e) {
            // this might happen if the SD card is removed while the media scanner is running
            Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
        }
    }

其中initialize   prescan   processDirectory  postscan這四個函數都比較重要。STEP 4

 private void initialize(String volumeName) {
        mMediaProvider = mContext.getContentResolver().acquireProvider("media");

        mAudioUri = Audio.Media.getContentUri(volumeName);
        mVideoUri = Video.Media.getContentUri(volumeName);
        mImagesUri = Images.Media.getContentUri(volumeName);
        mThumbsUri = Images.Thumbnails.getContentUri(volumeName);

        if (!volumeName.equals("internal")) {
            // we only support playlists on external media
            mProcessPlaylists = true;
            mProcessGenres = true;
            mGenreCache = new HashMap<String, Uri>();
            mGenresUri = Genres.getContentUri(volumeName);
            mPlaylistsUri = Playlists.getContentUri(volumeName);
            // assuming external storage is FAT (case insensitive), except on the simulator.
            if ( Process.supportsProcesses()) {
                mCaseInsensitivePaths = true;
            }
        }
    }

做一些初始化的動作,得到MediaProvider和一些URI實際也就是操作數據庫的一些表名。Audio.Media.getContentUri可以在MediaStore.java中找到,此類保存了所有的媒體格式URI等信息,此處獲得的mAudioUri的值爲“content://media/internal//audio/media"

STEP5

  private void prescan(String filePath) throws RemoteException {
    。 。 。
}
此函數比較長,在此省略代碼,有興趣的可以看源碼,這裏所做的操作是對於之前有掃描過的,就將數據庫中現有的媒體信息放到幾個數據結構中臨時存儲起來。

然後最重要的STEP 6 processDirectory是一個native函數,先注意幾個傳入參數directories[i]爲STEP2中傳入的路徑/system/media ,MediaFile.sFileExtensions 這個你可以跟到MediaFile中看看這個是如何賦值的,實際就是所有支持的媒體格式後綴以‘,’的方式串在一起的字符串”MP3,M4A,3GA,WAV。。。“最重要的mClient是MyMediaScannerClient的一個實例,此對象將是native層回調函數的接口,所有掃描完後的媒體都會通過此對象來存儲到數據庫中。

下面進入Native層對應文件是android_media_MediaScanner.cpp       STEP 6

static void
android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)
{
    MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

    if (path == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return;
    }
    if (extensions == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return;
    }
    
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }
    const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
    if (extensionsStr == NULL) {  // Out of memory
        env->ReleaseStringUTFChars(path, pathStr);
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    MyMediaScannerClient myClient(env, client);
    mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
    env->ReleaseStringUTFChars(path, pathStr);
    env->ReleaseStringUTFChars(extensions, extensionsStr);
}
這裏比較重要的一個點時MyMediaScannerClient myClient(env, client);定義了一個客戶端,並將java層的client傳入進去,很顯然,是想通過MyMediaScannerClient 再來回調client。

STEP 7

status_t MediaScanner::processDirectory(
        const char *path, const char *extensions,
        MediaScannerClient &client,
        ExceptionCheck exceptionCheck, void *exceptionEnv) {
    int pathLength = strlen(path);
    if (pathLength >= PATH_MAX) {
        return UNKNOWN_ERROR;
    }
    char* pathBuffer = (char *)malloc(PATH_MAX + 1);
    if (!pathBuffer) {
        return UNKNOWN_ERROR;
    }

    int pathRemaining = PATH_MAX - pathLength;
    strcpy(pathBuffer, path);
    if (pathLength > 0 && pathBuffer[pathLength - 1] != '/') {
        pathBuffer[pathLength] = '/';
        pathBuffer[pathLength + 1] = 0;
        --pathRemaining;
    }

    client.setLocale(locale());

    status_t result =
        doProcessDirectory(
                pathBuffer, pathRemaining, extensions, client,
                exceptionCheck, exceptionEnv);

    free(pathBuffer);

    return result;
}

此函數沒幹什麼事,具體工作是在doProcessDirectory中做的 STEP 8

status_t MediaScanner::doProcessDirectory(
        char *path, int pathRemaining, const char *extensions,
        MediaScannerClient &client, ExceptionCheck exceptionCheck,
        void *exceptionEnv) {
     . . . 
}

此函數太長,在此不粘出來了,這裏首先要解釋下這些參數,path - 要掃描文件夾路徑以'/'結尾,pathRemaining爲路徑長度與路徑最大長度之間的差值,也就是防止掃描時路徑超出範圍,extensions 前面已經解釋過是後綴,client是是STEP6中實例化的MyMediaScannerClient對象,後面兩個參數是一些異常處理不用關心。

大家仔細看這個函數的代碼就可以知道,它完成的是遍歷文件夾並找到有相應extensions 裏面後綴的文件fileMatchesExtension(path, extensions),如果文件大小大於0就調用client.scanFile(path, statbuf.st_mtime, statbuf.st_size);來進行文件讀取掃描  注意這裏纔會讀文件的實際內容。

STEP 9

 virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
    {
        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);

        mEnv->DeleteLocalRef(pathStr);
        return (!mEnv->ExceptionCheck());
    }
看看,終於用到了mClient,java層傳進來的client ,這就是回調到了java 類MyMediaScannerClient裏面的STEP 10

 public void scanFile(String path, long lastModified, long fileSize) {
            // This is the callback funtion from native codes.
            // Log.v(TAG, "scanFile: "+path);
            doScanFile(path, null, lastModified, fileSize, false);
        }
主要看doScanFile     STEP 11

  public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
            Uri result = null;
//            long t1 = System.currentTimeMillis();
            try {
                FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
                // rescan for metadata if file was modified since last scan
                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
                    String lowpath = path.toLowerCase();
                    boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
                    boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
                    boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
                    boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
                    boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
                        (!ringtones && !notifications && !alarms && !podcasts);

                    if (!MediaFile.isImageFileType(mFileType)) {
                        processFile(path, mimeType, this);
                    }

                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                }
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
            }
//            long t2 = System.currentTimeMillis();
//            Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
            return result;
        }
此函數裏面又有三個比較重要的函數beginFile   processFile   endFile
先看beginFile    STEP 12

  public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) {
     . . .
}
構建一個FileCacheEntry對象,存儲文件的一些基本信息,並且放入mFileCache HashMap中。

根據此文件是否修改來覺得是否processFile   ,又進入到native中

在此插入一段代碼

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    MediaScanner *mp = NULL;
    char value[PROPERTY_VALUE_MAX];
    if (property_get("media.framework.option", value, NULL) && (!strcmp(value, "1"))){
#ifndef NO_OPENCORE
        mp = new PVMediaScanner();
#else
        mp = new StagefrightMediaScanner;
#endif
    }else{
        mp = new StagefrightMediaScanner;
    }

    if (mp == NULL) {
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    env->SetIntField(thiz, fields.context, (int)mp);
}

android2.2以上mediascanner使用StagefrightMediaScanner

STEP 13

static void
android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
    MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

    if (path == NULL) {
        jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
        return;
    }
    
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }
    const char *mimeTypeStr = (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        env->ReleaseStringUTFChars(path, pathStr);
        jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
        return;
    }

    MyMediaScannerClient myClient(env, client);
    mp->processFile(pathStr, mimeTypeStr, myClient);
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

不再累述,直接進入 STEP 14

status_t StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
     . . . 
}

由於StagefrightMediaScanner又進入到了stagefright 框架,比較複雜,鑑於篇幅限制,在下一篇blog中繼續分析STEP 14










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章