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 将这些解析到的数据打包插入或更新到数据库中。

  • 11
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值