• Android媒体扫描详细解析之一(MediaScanner & MediaProvider)


    用过Android手机的同学都知道,每次开机的时候系统会先扫描sdcard,sdcard重新插拔(挂载)也会扫描一次sdcard。

    为什么要扫描sdcard,其实是为了给系统的其他应用提供便利,比如,Gallary、Music、VideoPlayer等应用,进入Gallary后会显示sdcard中的所有图片,

    如果进入Gallary后再去扫描,可想而知,你会厌恶这个应用,因为我们会觉得它反应太慢了。还有Music你看到播放列表的时候实际能看到这首歌曲的时长、演唱者、专辑

    等信息,这个也不是你进入应用后一下子可以读出来的。

    所以Android使用sdcard挂载后扫描的机制,先将这些媒体相关的信息扫描出来保存在数据库中,当打开应用的时候直接去数据库读取(或者所通过MediaProvider去从数据库读取)并show给用户,这样用户体验会好很多,下面我们分析这种扫描机制是如何实现的。

    在源码目录的packagesprovidersMediaProvider下面是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
    ");
                    Log.d(TAG, "    scan time: " + (scan - prescan) + "ms
    ");
                    Log.d(TAG, "postscan time: " + (end - scan) + "ms
    ");
                    Log.d(TAG, "   total time: " + (end - start) + "ms
    ");
                }
            } 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










  • 相关阅读:
    多线程:C#.NET中使用BackgroundWorker在模态对话框中显示进度条
    通过外接程序将Outlook邮件导出成Word文档
    [轉]FusionChartsFree参数说明
    MSIL学习资源
    FastCGI Error 2147467259 (0x80004005)
    编程实现双击某个文件用指定程序打开
    Excel api Enumerations 常量
    [轉]全面认识页面设置之PageSetup 对象
    AjaxFileUploaderV2.1增加可上传多个文件
    [轉]VB.NET and C# Comparison
  • 原文地址:https://www.cnblogs.com/keanuyaoo/p/3260655.html
Copyright © 2020-2023  润新知