美文网首页
MediaProvider 多媒体扫描流程

MediaProvider 多媒体扫描流程

作者: Nothing_655f | 来源:发表于2021-06-09 15:50 被阅读0次

    MediaProvider

    MediaProvider 与媒体文件预扫描相关,Android 系统每次开机或者重新插拔 SD 卡之后都会去扫描系统存储空间中的媒体文件,并将媒体文件相关的信息存储到媒体数据库中。这样后续 Gallery、Music、VideoPlayer 等应用便可以直接查询媒体数据库,根据需要提取信息做显示。

    MediaProvider 的初始化进程是在 android.process.media 的入口提供的

    多媒体系统的媒体扫描功能,是通过一个 apk 应用程序提供的,它位于 packages/providers/MediaProvider 目录下。查看该应用程序的的 AndroidManifest.xml 文件可知,该 apk 运行时指定了一个进程名:android.process.media

    该 apk 的 AndroidManifest 文件可以看到,android.process.media 使用了 Android 应用程序四大组件中的其中三个组件:

    MediaScannerService(从 Service 派生)模块负责扫描媒体文件,然后将扫描得到的信息插入到媒体数据库中。

    MediaProvider(从 ContentProvider 派生)模块负责处理针对这些媒体文件的数据库操作请求,例如查询、删除、更新等。

    MediaScannerReceiver(从 BroadcastReceiver 派生)模块负责接收外界发来的扫描请求。

    MediaScannerReceiver 分析

    MediaScannerReceiver 主要是捕获如下广播并进行相应的处理,具体处理流程可以看onReceive 方法的处理

    <action android:name="android.intent.action.BOOT_COMPLETED" />
    <action android:name="android.intent.action.LOCALE_CHANGED" />
    <action android:name="android.intent.action.MEDIA_MOUNTED" />
    <action android:name="android.intent.action.MEDIA_UNMOUNTED" />
    <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
    

    我们主要关注外部存储的

                    if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) {
                        // scan whenever any volume is mounted
                        scan(context, MediaProvider.EXTERNAL_VOLUME);
    

    其传递了 static final String EXTERNAL_VOLUME = "external";,

    如果是内部存储则是 static final String INTERNAL_VOLUME = "internal";

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

    从这个函数可以看到接下来转到 MediaScannerService 中

    MediaScannerService 模块分析

    MediaScannerService 从 Service 派生,并且实现了 Runnable 接口。

    // MediaScannerService 实现了 Runnable,表明它会创建工作线程
    public class MediaScannerService extends Service implements Runnable
    

    根据 Service 服务的生命周期,Service 刚创建时会调用 onCreate 函数,接着就是 onStartCommand 函数,之后外界每次调用 startService 都会触发 onStartCommand。

    @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();
    }
    

    如下代码可以看到Service onCreate后创建一个 Handler,处理工作线程的消息

        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();
        }
    

    接下往下走到onStartCommand,前面MediaScannerReceiver调用new startService 的时候传递了一个带volume 值的 Bundle 参数

        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;
        }
    

    sendMessage 消息在 ServiceHandler handleMessage 中处理

    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);
            }
        }
    }
    
    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 会走一些特殊的处理,例如打开数据库文件等。

    MediaProvider 分析

    MediaProvider 数据库操作 后面再另外分析

    MediaScanner Framework 流程

    从MediaScannerService new MediaScanner 就转到Framework的接口了

    public class MediaScanner implements AutoCloseable {
        static {
            // 加载 libmedia_jni 库
            System.loadLibrary("media_jni");
            native_init();
        }
        
        // 创建媒体扫描器
        public MediaScanner(Context c, String volumeName) {
            // 调用 JNI 函数做一些初始化操作
            native_setup();
            ...
        }
        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 {
            // ...
        }
    }
    

    1、prescan 函数的主要作用就是在扫描之前把上次扫描获取的数据库信息取出遍历并检测是否丢失,如果丢失,则从数据库中删除。

    2、数据库预处理完成之后,processDirectory 就是媒体扫描的关键函数,这是一个 native 函数,

    static void
    android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
    {
        ALOGV("native_setup");
        MediaScanner *mp = new StagefrightMediaScanner;
    
        if (mp == NULL) {
            jniThrowException(env, kRunTimeException, "Out of memory");
            return;
        }
    
        env->SetLongField(thiz, fields.context, (jlong)mp);
    }
    

    StagefrightMediaScanner 继承自 MediaScanner

    进入MediaScanner.cpp 看到processDirectory 处理如下

    processDirectory
    
        doProcessDirectory
    
            doProcessDirectoryEntry
    
                    1、如果该文件为目录,继续递归调用doProcessDirectory
    
                     2、 client.scanFile(path, statbuf.st_mtime, statbuf.st_size,false /*isDirectory*/, noMedia);
    

    如果是第2点的话,在调用 processDirectory 时所传入的真实类型为 MyMediaScannerClient,下面来看看它的 scanFile 函数

    frameworks\base\media\jni\android_media_MediaScanner.cpp

    static void
    android_media_MediaScanner_processDirectory(
            JNIEnv *env, jobject thiz, jstring path, jobject client)
    {
        ALOGV("processDirectory");
        MediaScanner *mp = getNativeScanner_l(env, thiz);
        // ...
    
        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);
    }
    
        virtual status_t scanFile(const char* path, long long lastModified,
                long long fileSize, bool isDirectory, bool noMedia)
        {
            ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
                path, lastModified, fileSize, isDirectory);
    
            jstring pathStr;
            if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
                mEnv->ExceptionClear();
                return NO_MEMORY;
            }
    
            mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                    fileSize, isDirectory, noMedia);
    
            mEnv->DeleteLocalRef(pathStr);
            return checkAndClearExceptionFromCallback(mEnv, "scanFile");
        }
    

    所以这里又调回到了Java 层 scanFile --> doScanFile

     doScanFile 函数中有三个比较重要的函数,beginFile、processFile 和 endFile,弄懂这三个函数之后,我们就知道 doScanFile 函数主要做些什么操作。
    
    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 去解析元数据,获取歌手、专辑、日期等等信息。

    其实 processfile 分成两部分

                            // we only extract metadata for audio and video files
                            if (isaudio || isvideo) {
                                mScanSuccess = processFile(path, mimeType, this);
                            }
    
                            if (isimage) {
                                mScanSuccess = processImageFile(path);
                            }
    

    这两个办法都是native 方法,以 processFile 为例,方法在StagefrightMediaScanner中定义

    frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp

    processFile --> processFileInternal

    其主要是如下几个方法

    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
    status = mRetriever->setDataSource(nullService, path);
    mRetriever->extractMetadata
    

    new MediaMetadataRetriever 跟MediaPlayer 类似,同样会到MediaPlayerService中获取初始话的client

    sp<IMediaMetadataRetriever> MediaPlayerService::createMetadataRetriever()
    {
        pid_t pid = IPCThreadState::self()->getCallingPid();
        sp<MetadataRetrieverClient> retriever = new MetadataRetrieverClient(pid);
        ALOGV("Create new media retriever from pid %d", pid);
        return retriever;
    }
    
    static sp<MediaMetadataRetrieverBase> createRetriever(player_type playerType)
    {
        sp<MediaMetadataRetrieverBase> p;
        switch (playerType) {
            case STAGEFRIGHT_PLAYER:
            case NU_PLAYER:
            {
                p = new StagefrightMetadataRetriever;
                break;
            }
            default:
                // TODO:
                // support for TEST_PLAYER
                ALOGE("player type %d is not supported",  playerType);
                break;
        }
        if (p == NULL) {
            ALOGE("failed to create a retriever object");
        }
        return p;
    }
    

    接下来就是player解析Meta信息并且返回存储

    从processfile返回后,最后解析完之后调用 endFile 将这些解析到的数据打包插入或更新到数据库中。

    这一个整个流程还是挺绕了,综合MediaScanner.java 文件其实有份注释总结

     * Internal service helper that no-one should use directly.
     *
     * The way the scan currently works is:
     * - The Java MediaScannerService creates a MediaScanner (this class), and calls
     *   MediaScanner.scanDirectories on it.
     * - scanDirectories() calls the native processDirectory() for each of the specified directories.
     * - the processDirectory() JNI method wraps the provided mediascanner client in a native
     *   'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner
     *   object (which got created when the Java MediaScanner was created).
     * - native MediaScanner.processDirectory() calls
     *   doProcessDirectory(), which recurses over the folder, and calls
     *   native MyMediaScannerClient.scanFile() for every file whose extension matches.
     * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile,
     *   which calls doScanFile, which after some setup calls back down to native code, calling
     *   MediaScanner.processFile().
     * - MediaScanner.processFile() calls one of several methods, depending on the type of the
     *   file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA.
     * - each of these methods gets metadata key/value pairs from the file, and repeatedly
     *   calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java
     *   counterparts in this file.
     * - Java handleStringTag() gathers the key/value pairs that it's interested in.
     * - once processFile returns and we're back in Java code in doScanFile(), it calls
     *   Java MyMediaScannerClient.endFile(), which takes all the data that's been
     *   gathered and inserts an entry in to the database.
     *
     * In summary:
     * Java MediaScannerService calls
     * Java MediaScanner scanDirectories, which calls
     * Java MediaScanner processDirectory (native method), which calls
     * native MediaScanner processDirectory, which calls
     * native MyMediaScannerClient scanFile, which calls
     * Java MyMediaScannerClient scanFile, which calls
     * Java MediaScannerClient doScanFile, which calls
     * Java MediaScanner processFile (native method), which calls
     * native MediaScanner processFile, which calls
     * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
     * native MyMediaScanner handleStringTag, which calls
     * Java MyMediaScanner handleStringTag.
     * Once MediaScanner processFile returns, an entry is inserted in to the database.
     *
     * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner.
     *
     * {@hide}
     */
    

    实例处理

    播放本地视频“3D食物/3D上下/谍影重重.mp4”,拔出另1U盘,视频黑屏后恢复,提示格式不支持

    调试打印

    packages/providers/MediaProvider/src/com/android/providers/media/MediaScannerService.java

    -            if (false) {
    +            if (mDebug) {
                     Log.d(TAG, "IMediaScannerService.scanFile: " + path + " mimeType: " + mimeType);
                 }
    
    
    -                        if (false) Log.d(TAG, "start scanning volume " + volume + ": "
    +                        if (mDebug) Log.d(TAG, "start scanning volume " + volume + ": "
                                     + Arrays.toString(directories));
                             scan(directories, volume);
    -                        if (false) Log.d(TAG, "done scanning volume " + volume);
    +                        if (mDebug) Log.d(TAG, "done scanning volume " + volume);
    
    

    frameworks/av/media/libstagefright/StagefrightMediaScanner.cpp

    -//#define LOG_NDEBUG 0
    +#define LOG_NDEBUG 0
    

    通过打印可以看到接入的硬盘中文件很多其扫描时间很长,有一分钟

    05-31 11:33:35.912  4696  4696 D MediaScannerReceiver: action: android.intent.action.MEDIA_MOUNTED path: /storage/3EC0700FC06FCBA9
    05-31 11:33:35.917  3475  3487 I BroadcastQueue: Delay finish: com.android.providers.media/.MediaScannerReceiver
    05-31 11:33:36.064  4696  5888 D MediaScannerService: start scanning volume external: [/storage/emulated/0, /storage/3EC0700FC06FCBA9]
    05-31 11:33:51.318  4696  5888 V StagefrightMediaScanner: processFile '/storage/3EC0700FC06FCBA9/ChannelCheck_321_ddp.mp4'.
    05-31 11:33:51.366  4696  5888 V StagefrightMediaScanner: result: 0
    05-31 11:33:52.658  4696  5888 V StagefrightMediaScanner: processFile '/storage/3EC0700FC06FCBA9/Silent_Movie_JOC_5_1_2_ddp.mp4'.
    05-31 11:33:52.725  4696  5888 V StagefrightMediaScanner: result: 0
    // .....
    05-31 11:34:39.216  4696  5888 D MediaScannerService: done scanning volume external
    

    出现问题时由于是vold 发送了 signal interrupt kill 了对应的mediaserver 进程

    06-01 17:23:10.669  2796  2812 W vold    : Found symlink /proc/8597/fd/44 referencing /storage/3EC0700FC06FCBA9 
    06-01 17:23:10.670  2796  2812 W vold    : Found symlink /proc/8597/fd/47 referencing /storage/3EC0700FC06FCBA9/log
    06-01 17:23:10.676  2796  2812 W vold    : Failed to open /proc/9148/fd: No such file or directory
    06-01 17:23:10.676  2796  2812 W vold    : Failed to open /proc/9149/fd: No such file or directory
    06-01 17:23:10.676  2796  2812 W vold    : Sending Interrupt to 8597
    06-01 17:23:10.676  2796  2812 W vold    : Sending Interrupt to 3331
    06-01 17:23:10.692  3480  4842 W ActivityManager: Scheduling restart of crashed service com.android.providers.media/.MediaScannerService in 18918ms
    

    相关文章

      网友评论

          本文标题:MediaProvider 多媒体扫描流程

          本文链接:https://www.haomeiwen.com/subject/xbqteltx.html