android_9.0 MediaScanner 媒體掃描詳解

1. 概述

MediaScanner 是 Android 多媒體系統中重要的一員,MediaScanner 與媒體文件預掃描相關。我們知道,Android 系統每次開機或者重新插拔 SD 卡之後都會去掃描系統存儲空間中的媒體文件,並將媒體文件相關的信息存儲到媒體數據庫中。這樣後續 Gallery、Music、VideoPlayer 等應用便可以直接查詢媒體數據庫,根據需要提取信息做顯示。

如果進入音樂播放器應用之後再去掃描,可想而知,你會厭惡這個應用,因爲我們會覺得它反應太慢了。有了 MediaScanner 的預掃描,一打開音樂播放器就能直接去數據庫中讀取歌曲的時長、演唱者、專輯等信息顯示給用戶,這樣用戶體驗會好很多。

下面就來分析媒體文件掃描的原理。

2. android.process.media 分析

多媒體系統的媒體掃描功能,是通過一個 apk 應用程序提供的,它位於 packages/providers/MediaProvider 目錄下。查看該應用程序的的 AndroidManifest.xml 文件可知,該 apk 運行時指定了一個進程名:android.process.media

<application android:process="android.process.media"

通過 ps 命令可以看到這個進程,另外,從該 apk 的 AndroidManifest 文件可以看到,android.process.media 使用了 Android 應用程序四大組件中的其中三個組件:

  • MediaScannerService(從 Service 派生)模塊負責掃描媒體文件,然後將掃描得到的信息插入到媒體數據庫中。
  • MediaProvider(從 ContentProvider 派生)模塊負責處理針對這些媒體文件的數據庫操作請求,例如查詢、刪除、更新等。
  • MediaScannerReceiver(從 BroadcastReceiver 派生)模塊負責接收外界發來的掃描請求。

下面將從 android.procee.media 程序的這幾個模塊觸發,分析媒體文件掃描的相關工作流程。

2.1 MediaScannerReceiver 模塊分析

<receiver android:name="MediaScannerReceiver">
    <intent-filter>
        // 開機完成
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        // 系統地區改變
        <action android:name="android.intent.action.LOCALE_CHANGED" />
        // 系統外部存儲掛載狀態改變,如插拔 U 盤、SD 卡
        <action android:name="android.os.storage.action.VOLUME_STATE_CHANGED" />
    </intent-filter>

MediaScannerReceiver 模塊的核心類 MediaScannerReceiver.java 從 BroadcastReceiver 派生,它是專門用來接收廣播的,它感興趣的廣播從上面可以看出,BOOT_COMPLETED action 說明它是開機自啓動。

file: packages/providers/MediaProvider/.../MediaScannerReceiver.java

public class MediaScannerReceiver extends BroadcastReceiver {
    private final static String TAG = "MediaScannerReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        final Uri uri = intent.getData();
        if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
            // 如果收到 BOOT_COMPLETE 開機完成廣播,則啓動內存存儲區掃描工作
            scan(context, MediaProvider.INTERNAL_VOLUME);
        } else {
            // 如果外部存儲掛載成功,則啓動外部存儲的掃描工作:EXTERNAL_VOLUME
            if ((VolumeInfo.ACTION_VOLUME_STATE_CHANGED.equals(action))) {
                int state = intent.getIntExtra(VolumeInfo.EXTRA_VOLUME_STATE, -1);
                if (VolumeInfo.STATE_MOUNTED == state) {
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                }
            }
            ....
        }
    }
    ...
}

大部分的的媒體文件都是在外部存儲中,讓我們來看一下存儲狀態 STATE_MOUNTED 掛載上之後 MediaScannerReceiver 的工作。scan 代碼如下:

file: packages/providers/MediaProvider/.../MediaScannerReceiver.java

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

MediaScannerReceiver 攜帶掃描卷名信息 VolumeInfo 啓動了 MediaScannerService,下面來看 MediaScannerService 的工作。

2.2 MediaScannerService 模塊分析

MediaScannerService 從 Service 派生,並且實現了 Runnable 接口。

public class MediaScannerService extends Service implements Runnable
// MediaScannerService 實現了 Runnable,表明它會創建工作線程

根據 Service 服務的生命週期,Service 剛創建時會調用 onCreate 函數,接着就是 onStartCommand 函數,之後外界每次調用 startService 都會觸發 onStartCommand。

所以我們先來看一下 onCreate 函數

file: packages/providers/MediaProvider/.../MediaScannerService.java

@Override
public void onCreate() {
    // 獲取電源鎖,防止掃描過程中休眠
    PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
    
    // 獲取外部存儲掃描路徑
    StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
    mExternalStoragePaths = storageManager.getVolumePaths();

    Thread thr = new Thread(null, this, "MediaScannerService");
    thr.start();
}

掃描工作是一個漫長的過程,所以這裏單獨創建了一個工作線程,線程函數就是 MediaScannerService 實現的 Run 函數。

file: packages/providers/MediaProvider/.../MediaScannerService.java

@Override
public void run() {
    // 設置進程優先級,媒體掃描比較費時,防止 CPU 一直被 MediaScannerService 
    // 佔用,這會導致用戶感覺系統變得很慢
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
            Process.THREAD_PRIORITY_LESS_FAVORABLE);
    Looper.prepare();

    // 創建一個 Handler,處理工作線程的消息
    mServiceLooper = Looper.myLooper();
    mServiceHandler = new ServiceHandler();

    Looper.loop();
}

onCreate 之後,MediaScannerService 就創建了一個帶消息處理機制的工作線程,下面來看看消息是怎麼傳遞到這個線程中的。

onCreate 之後外部每次調用 startService 都會觸發 onStartCommand。上一小節中,MediaScannerReceiver scan 函數中啓動了 MediaScannerService,並在 Intent 中攜帶了掃描卷名。

Bundle args = new Bundle();
args.putString("volume", volume);

這個 Intent 發出後,最終由 MediaScannerService 的 onStartCommand 做處理。

file: packages/providers/MediaProvider/.../MediaScannerService.java

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    ...

    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent.getExtras();
    // 往這個 Handler 中投遞消息,最終由工作線程做處理
    mServiceHandler.sendMessage(msg);

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

下面看一下工作線程中的掃描請求消息處理。

file: packages/providers/MediaProvider/.../MediaScannerService.java

private final class ServiceHandler extends Handler {
     @Override
        public void handleMessage(Message msg) {
            Bundle arguments = (Bundle) msg.obj;
            if (arguments == null) {
                Log.e(TAG, "null intent, b/20953950");
                return;
            }
            String filePath = arguments.getString("filepath");
            String folderPath = arguments.getString("folderpath");

            try {
                if (filePath != null) {
                    ...
                } else {
                    // 攜帶掃描卷名 Intent 最終在在這邊做處理
                    String volume = arguments.getString("volume");
                    String[] directories = null;

                    if (folderPath != null){
                        directories = new String[] {folderPath};
                    } else if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
                        // 如果是掃描內部存儲,實際掃描目錄爲 ---
                        directories = new String[] {
                                Environment.getRootDirectory() + "/media",
                                Environment.getOemDirectory() + "/media",
                                Environment.getProductDirectory() + "/media",
                        };
                    }
                    else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                        // 如果是掃描外部存儲,實際掃描目錄爲 ---
                        if (getSystemService(UserManager.class).isDemoUser()) {
                            directories = ArrayUtils.appendElement(String.class,
                                    mExternalStoragePaths,
                                    Environment.getDataPreloadsMediaDirectory().getAbsolutePath());
                        } else {
                            directories = mExternalStoragePaths;
                        }
                    }

                    // 調用 scan 函數開展文件夾掃描工作
                    if (directories != null) {
                        if (false) Log.d(TAG, "start scanning volume " + volume + ": "
                                + Arrays.toString(directories));
                        scan(directories, volume);
                        if (false) Log.d(TAG, "done scanning volume " + volume);
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception in handleMessage", e);
            }

            stopSelf(msg.arg1);
        }
    }
}

MediaScannerService 類 scan 攜帶掃描卷名和掃描文件夾參數。掃描邏輯如下:

file: packages/providers/MediaProvider/.../MediaScannerService.java

private void scan(String[] directories, String volumeName) {
    Uri uri = Uri.parse("file://" + directories[0]);
    // don't sleep while scanning
    mWakeLock.acquire();

    try {
        ContentValues values = new ContentValues();
        values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
        // 通過 insert 這個特殊的 uri,讓 MeidaProvider 做一些準備工作
        Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);

        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));

        try {
            if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                openDatabase(volumeName);
            }
            
            // 創建媒體掃描器,並調用 scanDirectories 掃描目標文件夾
            try (MediaScanner scanner = new MediaScanner(this, volumeName)) {
                scanner.scanDirectories(directories);
            }
        } catch (Exception e) {
            Log.e(TAG, "exception in MediaScanner.scan()", e);
        }

        // 通過特殊的 uri,讓 MeidaProvider 做一些清理工作
        getContentResolver().delete(scanUri, null, null);

    } finally {
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
        mWakeLock.release();
    }
}

開始掃描和結束掃描時都會發送一個全局的廣播,第三方應用程序也可以通過註冊這兩個廣播來避開在 media 掃描的時候往改掃描文件夾裏面寫入或刪除文件。

上面的代碼中,比較複雜的是 MediaScannerService 和 MediaProvider 的交互。MediaScannerService 經常使用一些特殊 Uri 做數據庫操作,而 MediaProvider 針對這些 Uri 會走一些特殊的處理,例如打開數據庫文件等。

PS:MediaProvider 數據庫操作不做重點分析。

至此,MediaScannerService 的掃描工作就結束了,下面輪到主角 MediaScanner 登場。

2.3 android.process.media 掃描工作總結

  • MediaScannerReceiver 接收外部發來的的掃描請求,並通過 startService 的方式啓動 MediaScannerService。
  • MediaScannerService 主線程接收到 MediaScannerReceiver 的掃描請求,然後投遞給工作線程去處理。
  • 工作線程做一些前期處理工作後(例如向系統廣播發送掃描開始的消息),就創建 MediaScanner 來處理掃描目標。
  • MediaScanner 掃描結束後,工作線程在做一些後期數據庫處理,然後向系統發送掃描完畢的廣播。

3. MediaScanner 分析

現在來分析媒體掃描器 MediaScanner 的工作原理,它將跨 Java 層、JNI 層以及 Naitive 層。

代碼路徑:frameworks/base/media/MediaScanner*

3.1 Java 層分析

(1)創建 MediaScanner

file: framework/base/media/MediaScanner.java

public class MediaScanner implements AutoCloseable {
    static {
        // 加載 libmedia_jni 庫
        System.loadLibrary("media_jni");
        native_init();
    }
    
    // 創建媒體掃描器
    public MediaScanner(Context c, String volumeName) {
        // 調用 JNI 函數做一些初始化操作
        native_setup();
        ...
    }

native_init 和 native_setup 函數在下一節 JNI 層做分析。

MediaScanner 創建成功之後,將調用 scanDirectories 函數掃描目標文件夾。

(2)scanDirectories 函數分析

file: framework/base/media/MediaScanner.java

public void scanDirectories(String[] directories) {
    try {
        long start = System.currentTimeMillis();
        prescan(null, true); // ① 掃描前預準備
        long prescan = System.currentTimeMillis();

        ...

        for (int i = 0; i < directories.length; i++) {
            // ② processDirectory 是一個 native 函數,調用它來對目標文件夾進行掃描
            // mClient 爲 MyMediaScannerClient 類型,從 MediaScannerClient 
            //派生,它的作用後面分析。
            processDirectory(directories[i], mClient); 
        }

        ...

        long scan = System.currentTimeMillis();
        postscan(directories); // ③ 掃描後處理
        long end = System.currentTimeMillis();
    } catch (SQLException e) {
        ...
    } finally {
        ...
    }
}

上面 prescan 函數比較關鍵,首先讓我們試想一個問題。

在媒體掃描過程中,有個令人頭疼的問題,來舉個例子:假設某次掃描前 SD 卡中有 100 個媒體文件,數據庫中會有 100 條關於這些文件的記錄。現刪除其中的 50 個文件,那麼媒體數據庫什麼時候會被更新呢?

MediaScanner 考慮到了這一點,prescan 函數的主要作用就是在掃描之前把上次掃描獲取的數據庫信息取出遍歷並檢測是否丟失,如果丟失,則從數據庫中刪除。

file: framework/base/media/MediaScanner.java

private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
    Cursor c = null;
    String where = null;
    String[] selectionArgs = null;

    mPlayLists.clear();

    if (filePath != null) {
        // query for only one file
        where = MediaStore.Files.FileColumns._ID + ">?" +
            " AND " + Files.FileColumns.DATA + "=?";
        selectionArgs = new String[] { "", filePath };
    } else {
        where = MediaStore.Files.FileColumns._ID + ">?";
        selectionArgs = new String[] { "" };
    }
    
    ...

    try {
        if (prescanFiles) {
            long lastId = Long.MIN_VALUE;
            Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();

            while (true) {
                selectionArgs[0] = "" + lastId;
                if (c != null) {
                    c.close();
                    c = null;
                }
                c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
                        where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
                if (c == null) {
                    break;
                }

                int num = c.getCount();

                if (num == 0) {
                    break;
                }
                // 遍歷上次掃描存儲在數據庫中的媒體文件
                while (c.moveToNext()) {
                    long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
                    String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
                    int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
                    long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
                    lastId = rowId;

                    if (path != null && path.startsWith("/")) {
                        boolean exists = false;
                        try {
                            exists = Os.access(path, android.system.OsConstants.F_OK);
                        } catch (ErrnoException e1) {
                        }
                        // 查詢媒體文件路徑是否能訪問到
                        if (!exists && !MtpConstants.isAbstractObject(format)) {
                            MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
                            int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);

                            if (!MediaFile.isPlayListFileType(fileType)) {
                                // 文件不存在且不是支持的播放列表類型
                                // 從數據庫中刪除
                                deleter.delete(rowId);
                                if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
                                    deleter.flush();
                                    String parent = new File(path).getParent();
                                    mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    finally {
        if (c != null) {
            c.close();
        }
        deleter.flush();
    }
    ...
}

數據庫預處理完成之後,processDirectory 就是媒體掃描的關鍵函數。由於它是一個 native 函數,我們跳入 JNI 層做分析。

3.2 JNI 層分析

在 Java 層,有三個函數涉及 JNI 層,它們是:

  1. native_init,這個函數由 MediaScanner 類的 static 塊調用。
  2. native_setup,這個函數由 MediaScanner 類的構造方法調用。
  3. processDirectory,這個函數是 MediaScanner 掃描文件夾時調用。

下面分別來看看。

3.2.1 native_init 分析

file: frameworks/base/media/jni/android_media_MediaScanner.cpp

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    // kClassMediaScanner = android/media/MediaScanner 
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

可以看到,在 native_init 函數中,取得 Java 層 MediaScanner 類的 mNativeContext 對象(long 類型)保存在 JNI 層 fields.context 對象中。

3.2.2 native_setup 分析

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    ALOGV("native_setup");
    // 創建 native 層的 MediaScanner 對象 —— StagefrightMediaScanner(frameworks/av 中定義)
    MediaScanner *mp = new StagefrightMediaScanner;

    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }

    // 將 mp 指針保存在 Java 層 MediaScanner 類 mNativeContext 對象中
    env->SetLongField(thiz, fields.context, (jlong)mp);
}

3.2.3 processDirectory 分析

static void
android_media_MediaScanner_processDirectory(
        JNIEnv *env, jobject thiz, jstring path, jobject client)
{
    ALOGV("processDirectory");
    // (MediaScanner *) env->GetLongField(thiz, fields.context)
    // 剛剛 native_setup 函數中創建的 Native 層 MediaScanner 對象
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }

    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }

    // 獲取掃描路徑
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }

    // 創建一個 Native 層的 MyMediaScannerClient 對象,並用 Java 層 client 對象做參數
    // 調用 native 層的 processDirectory 函數做媒體掃描
    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processDirectory(pathStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning directory '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
}

至此,又轉入了 Native 層 StagefrightMediaScanner 類的 processDirectory。

傳遞的兩個參數一個是掃描路徑,一個是 native 層創建的 MyMediaScannerClient 對象。到這裏比較困惑的是 MyMediaScannerClient 對象有什麼作用,後續轉入 naitive 層分析我們慢慢就會知道了。

3.3 StagefrightMediaScanner 處理

3.3.1 StagefrightMediaScanner processDirectory 分析

Native 層 StagefrightMediaScanner 的 processDirectory 函數是由基類 MediaScanner 實現的。

file: frameworks/av/media/libmedia/MediaScanner.cpp

MediaScanResult MediaScanner::processDirectory(
        const char *path, MediaScannerClient &client) {
    ... // 做一些準備工作
    client.setLocale(locale()); // 給 Native 層 Client 設置 locale 信息  
 
    MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);

    free(pathBuffer);

    return result;
}

// 下面直接看 doProcessDirectory 函數

MediaScanResult MediaScanner::doProcessDirectory(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
   // fileSpot 指向了路徑字符串的末尾
    char* fileSpot = path + strlen(path);
    struct dirent* entry;

    // 忽略一些屬性服務中設置的需要忽略的文件夾
    // property_get("testing.mediascanner.skiplist", mSkipList, "")
    if (shouldSkipDirectory(path)) {
        ALOGD("Skipping: %s", path);
        return MEDIA_SCAN_RESULT_OK;
    }

    // 剩下的地址空間大於 8,是爲了存入 ".nomedia"
    if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
        // 路徑字符串末尾再加上 .nomedia
        // /mnt/ -> /mnt/.nomedia
        // 如果該文件夾能找到這個(.nomedia)標識,也直接忽略
        strcpy(fileSpot, ".nomedia");
        if (access(path, F_OK) == 0) {
            ALOGV("found .nomedia, setting noMedia flag");
            noMedia = true;
        }

        // restore path
        fileSpot[0] = 0;
    }

    DIR* dir = opendir(path);
    if (!dir) {
        ALOGW("Error opening directory '%s', skipping: %s.", path, strerror(errno));
        return MEDIA_SCAN_RESULT_SKIPPED;
    }

    MediaScanResult result = MEDIA_SCAN_RESULT_OK;
    // 遍歷文件夾中的文件和子文件夾
    while ((entry = readdir(dir))) {
        if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
                == MEDIA_SCAN_RESULT_ERROR) {
            result = MEDIA_SCAN_RESULT_ERROR;
            break;
        }
    }
    closedir(dir);
    return result;
}

可以看到,最終通過調用 doProcessDirectoryEntry 函數來處理子文件夾和子文件,下面來看看這個函數。

file: frameworks/av/media/libmedia/MediaScanner.cpp

MediaScanResult MediaScanner::doProcessDirectoryEntry(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
        struct dirent* entry, char* fileSpot) {
    struct stat statbuf;
    const char* name = entry->d_name; // 獲取子文件或子文件夾的名稱

    ... // 忽略一些不合法文件或文件夾 
    
    strcpy(fileSpot, name); 
    // 原先 fileSpot 指向的是搜索路徑的末尾
    // 現在 fileSpot 的值爲子文件或子文件夾的全路徑

    int type = entry->d_type;
    ...
    
    // 文件夾
    if (type == DT_DIR) {
        bool childNoMedia = noMedia;
        // set noMedia flag on directories with a name that starts with '.'
        // for example, the Mac ".Trashes" directory
        if (name[0] == '.')
            childNoMedia = true;

        // report the directory to the client
        if (stat(path, &statbuf) == 0) {
            // 文件夾掃描
            status_t status = client.scanFile(path, statbuf.st_mtime, 0,
                    true /*isDirectory*/, childNoMedia);
            if (status) {
                return MEDIA_SCAN_RESULT_ERROR;
            }
        }

        // 在子文件夾全路徑後面加 "/",循環遍歷
        strcat(fileSpot, "/");
        MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
                client, childNoMedia);
        if (result == MEDIA_SCAN_RESULT_ERROR) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    } else if (type == DT_REG) {
        stat(path, &statbuf);
        // 文件掃描
        status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
                false /*isDirectory*/, noMedia);
        if (status) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    }

    return MEDIA_SCAN_RESULT_OK;
}

至此,上一節 JNI 層傳入的 MyMediaScannerClient 就被調用到了。client.scanFile,MediaScanner 調用 MeidaScannerClient 的 scanfile 函數來處理這個文件。

3.3.2 MyMediaScannerClient 的 scanfile 分析

我們在調用 processDirectory 時所傳入的真實類型爲 MyMediaScannerClient,下面來看看它的 scanFile 函數:

file: frameworks/base/media/jni/android_media_MediaScanner.cpp

virtual status_t scanFile(const char* path, long long lastModified,
        long long fileSize, bool isDirectory, bool noMedia)
{
    jstring pathStr;
    if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }

    // mClient 是 Java 層傳入的 MyMediaScannerClient 對象,這裏調用它的 scanFile 函數
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
            fileSize, isDirectory, noMedia);

    mEnv->DeleteLocalRef(pathStr);
    return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}

Java 層 MyMediaScannerClient 對象實現如下:

file: frameworks/base/media/java/.../MediaScanner.java

@Override
public void scanFile(String path, long lastModified, long fileSize,
        boolean isDirectory, boolean noMedia) {
    // This is the callback funtion from native codes.
    // Log.v(TAG, "scanFile: "+path);
    doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}

// 直接看 doScanFile
public Uri doScanFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
    Uri result = null;
    try {
        ① ---beginFile
        FileEntry entry = beginFile(path, mimeType, lastModified,
                fileSize, isDirectory, noMedia);

        if (entry == null) {
            return null;
        }

        ...
        scanAlways = true;
        ...

        // rescan for metadata if file was modified since last scan
        if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
            if (noMedia) {
                result = endFile(entry, false, false, false, false, false);
            } else {
                // 正常文件處理走到這裏
                
                // 這邊決定掃描的媒體類型,MediaFile 記錄了一些 Video、Audio
                // image 本系統可以識別的類型格式
                boolean isaudio = MediaFile .isAudioFileType(mFileType);
                boolean isvideo = MediaFile.isVideoFileType(mFileType);
                boolean isimage = MediaFile.isImageFileType(mFileType);

                if (isaudio || isvideo || isimage) {
                    path = Environment.maybeTranslateEmulatedPathToInternal(new File(path))
                            .getAbsolutePath();
                }

                // we only extract metadata for audio and video files
                if (isaudio || isvideo) {
                    // processFile 這邊主要是解析媒體文件的元數據,以便後續存入到數據庫中
                    // 本文不做分析
                    mScanSuccess = processFile(path, mimeType, this);
                }

                if (isimage) {
                    mScanSuccess = processImageFile(path);
                }

                String lowpath = path.toLowerCase(Locale.ROOT);
                boolean ringtones = mScanSuccess && (lowpath.indexOf(RINGTONES_DIR) > 0);
                boolean notifications = mScanSuccess &&
                        (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
                boolean alarms = mScanSuccess && (lowpath.indexOf(ALARMS_DIR) > 0);
                boolean podcasts = mScanSuccess && (lowpath.indexOf(PODCAST_DIR) > 0);
                boolean music = mScanSuccess && ((lowpath.indexOf(MUSIC_DIR) > 0) ||
                    (!ringtones && !notifications && !alarms && !podcasts));

                result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
            }
        }
    } catch (RemoteException e) {
        Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
    }
    
    return result;
}

doScanFile 函數中有三個比較重要的函數,beginFile、processFile 和 endFile,弄懂這三個函數之後,我們就知道 doScanFile 函數主要做些什麼操作。

首先來看 beginFile 函數。

file: frameworks/base/media/java/.../MediaScanner.java

public FileEntry beginFile(String path, String mimeType, long lastModified,
        long fileSize, boolean isDirectory, boolean noMedia) {
        
    ...
    
    FileEntry entry = makeEntryFor(path);
    // add some slack to avoid a rounding error
    long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0;
    boolean wasModified = delta > 1 || delta < -1;
    if (entry == null || wasModified) {
        // 不管原來表中是否存在這個路徑文件數據,這裏面都會執行到
        if (wasModified) {
            // 更新最後編輯時間
            entry.mLastModified = lastModified;
        } else {
            // 原先數據庫表中不存在則新建
            entry = new FileEntry(0, path, lastModified,
                    (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0));
        }
        entry.mLastModifiedChanged = true;
    }

    ...

    return entry;
}

makeEntryFor 從數據庫表中查詢是否包含該文件,如果 entry 爲空,則說明條目不存在,新建 FileEntry,這個值後續會傳入後續 processFile 和 endFile 做處理。

FileEntry 新建之後,我們同時也知道了該文件是 video、Audio 還是 Image 圖片,調用 processFile 去解析元數據,獲取歌手、專輯、日期等等信息。這邊不做分析。

最後解析完之後調用 endFile 將這些解析到的數據打包插入或更新到數據庫中。

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