zoukankan      html  css  js  c++  java
  • Android MediaScanner 详尽分析

    [Innost]: http://blog.csdn.net/Innost/article/details/6083467

    =============================================

    MediaScanner分析

    一 MediaScannerService

    多媒体扫描是从MediaScannerService开始的。这是一个单独的package。位于

    packages/providers/MediaProvider:含以下java文件

    l         MediaProvider.java

    l         MediaScannerReceiver.java

    l         MediaScannerService.java

    l         MediaThumbRequest.java

    分析这个目录的Android.mk文件,发现它运行的进程名字就是android.process.media

    application android:process=android.process.media

    1.1    MediaScannerReceiver

    这个类从BroadcastReceiver中派生,用来接收任务的。

    MediaScannerReceiver extends BroadcastReceiver

    在它重载的onRecieve函数内有以下几种走向:

    if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {

                // 收到”启动完毕“广播后,扫描内部存储

                scan(context, MediaProvider.INTERNAL_VOLUME);

            } else {

                ……….

                    if (action.equals(Intent.ACTION_MEDIA_MOUNTED) &&

                            externalStoragePath.equals(path)) {

                   /收到MOUNT信息后,扫描外部存储

                        scan(context, MediaProvider.EXTERNAL_VOLUME);

                    }

     else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) &&

                            path != null && path.startsWith(externalStoragePath + "/")) {

                   //收到请求要求扫描某个文件,注意不会扫描内部存储上的文件

                        scanFile(context, path);

            …………………………..

            }

    ……下面是它调用的scan函数:

    scan(Context context, String volume)

    Bundle args = new Bundle();

            args.putString("volume", volume);

    //直接启动MediaScannerService了,

            context.startService(

                    new Intent(context, MediaScannerService.class).putExtras(args));

    总结:

    MediaScannerReceiver是用来接收任务的,它收到广播后,会启动MediaService进行扫描工作。

    下面看看MediaScannerService.

    1.2    MediaScannerService

    MSS标准的从Service中派生下来,

    MediaScannerService extends Service implements Runnable

    //注意:是一个Runnable…,可能有线程之类的东西存在

    下面从Service的生命周期的角度来看看它的工作。

    1. onCreate

    public void onCreate()

           PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);

            mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

          //获得电源锁,防止在扫描过程中休眠

          //单独搞一个线程去跑扫描工作,防止ANR

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

            thr.start();

    2. onStartCommand

    @Override

        public int onStartCommand(Intent intent, int flags, int startId)

        {

          //注意这个handler,是在另外一个线程中创建的,往这个handler里sendMessage

         //都会在那个线程里边处理

      //不明白的可以去查看handler和Looper机制

    //这里就是同步机制,等待mServiceHandler在另外那个线程创建完毕

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

    //把MediaScannerReceiver发出的消息传递到另外那个线程去处理。

            mServiceHandler.sendMessage(msg);

          ………….

    基本上MSR(MediaScannerReceiver)发出的请求都会传到onStartCommand中处理。如果有多个存储的话,也只能一个一个扫描了。

    下面看看那个线程的主函数

    3. 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和handler的实现

            Looper.prepare();//把这个looper对象设置到线程本地存储

            mServiceLooper = Looper.myLooper();

            mServiceHandler = new ServiceHandler();//创建handler,默认会把这个looper

    //的消息队列赋值给handler的消息队列,这样往handler中发送消息就是往这个线程的looper发

            Looper.loop();//消息循环,内部会处理消息队列中的消息

    //也就是handleMessage函数

    }

    上面handler中加入了一个扫描请求(假设是外部存储的),所以要分析handleMessage函数。

    4. handleMessage

    private final class ServiceHandler extends Handler

        {

            @Override

            public void handleMessage(Message msg)

            {

                Bundle arguments = (Bundle) msg.obj;

                String filePath = arguments.getString("filepath");

               

                try {

    ……… 这里不讲了

                    } 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)) {

                          //是扫描外部存储的请求?获取外部存储的路径 

                           directories = new String[] {

                                    Environment.getExternalStorageDirectory().getPath(),

                                    };

                        }

                        if (directories != null) {

    //真正的扫描开始了,上面只不过是把存储路径取出来罢了.

                            scan(directories, volume);

                             …..

    //扫描完了,就把service停止了

                stopSelf(msg.arg1);

            }

    };

    5. scan函数

    private void scan(String[] directories, String volumeName) {

           mWakeLock.acquire();

    //下面这三句话很深奥…

    //从 getContentResolver获得一个ContentResover,然后直接插入

    //根据AIDL,这个ContentResover的另一端是MediaProvider。只要去看看它的

    //insert函数就可以了

    //反正这里知道获得了一个扫描URI即可。

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

    //交给扫描器去扫描文件夹  scanDirectories

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

    }

    说说上面那个深奥的地方,在MediaProvider中重载了insert函数,insert函数会调用insertInternal函数。

    如下:

    private Uri insertInternal(Uri uri, ContentValues initialValues) {

            long rowId;

            int match = URI_MATCHER.match(uri);

            // handle MEDIA_SCANNER before calling getDatabaseForUri()

    //刚才那个insert只会走下面这个分支,其实就是获得一个地址….

    //太绕了!!!!!

            if (match == MEDIA_SCANNER) {

                mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);

                return MediaStore.getMediaScannerUri();

            }

    ……..

    再看看它创建了什么样的Scanner,这就是MSS中的createMediaScanner

    private MediaScanner createMediaScanner() {

    //下面这个MediaScanner在framework/base/中,待会再分析

            MediaScanner scanner = new MediaScanner(this);

    //设置当前的区域,这个和字符编码有重大关系。

            Locale locale = getResources().getConfiguration().locale;

            if (locale != null) {

                String language = locale.getLanguage();

                String country = locale.getCountry();

                String localeString = null;

                if (language != null) {

                    if (country != null) {

    //给扫描器设置当前国家和语言。

                        scanner.setLocale(language + "_" + country);

                    } else {

                        scanner.setLocale(language);

                    }

                }   

            }

            return scanner;

    }

    至此,MSS的任务完成了。接下来是MediaScanner的工作了。

    6. 总结

    MSS的工作流程如下:

    l         1 单独启动一个带消息循环的工作线程。

    l         2 主线程接收系统发来的任务,然后发送给工作线程去处理。

    l         3 工作线程接收任务,创建一个MediaScanner去扫描。

    l         4 MSS顺带广播一下扫描工作启动了,扫描工作完毕了。

    二 MediaScanner

    MediaScanner位置在

    frameworks/base/media/下,包括jni和java文件。

    先看看java实现。

    这个类巨复杂,而且和MediaProvider交互频繁。在分析的时候要时刻回到MediaProvider去看看。

    1.       初始化

    public class MediaScanner

    {

    static {

    //libmedia_jni.so的加载是在MediaScanner类中完成的

    //这么重要的so为何放在如此不起眼的地方加载???

            System.loadLibrary("media_jni");

            native_init();

    }

    public MediaScanner(Context c) {

            native_setup();//调用jni层的初始化,暂时不用看了,无非就是一些

    //初始化工作,待会在再进去看看

    ……..

        }

    刚才MSS中是调用scanDirectories函数,我们看看这个。

    2.       scanDirectories

    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++) {

    //扫描文件夹,这里有一个很重要的参数 mClient

    // processDirectory是一个native函数

                    processDirectory(directories[i], MediaFile.sFileExtensions, mClient);

                }

                long scan = System.currentTimeMillis();

                postscan(directories);//扫描后处理

                long end = System.currentTimeMillis();

               …..打印时间,异常处理…没了…

    下面简单讲讲initialize ,preScan和postScan都干嘛了。

    private void initialize(String volumeName) {

    //打开MediaProvider,获得它的一个实例

            mMediaProvider = mContext.getContentResolver().acquireProvider("media");

    //得到一些uri

            mAudioUri = Audio.Media.getContentUri(volumeName);

            mVideoUri = Video.Media.getContentUri(volumeName);

            mImagesUri = Images.Media.getContentUri(volumeName);

            mThumbsUri = Images.Thumbnails.getContentUri(volumeName);

    //外部存储的话,可以支持播放列表之类的东西,搞了一些个缓存池之类的

    //如mGenreCache等

            if (!volumeName.equals("internal")) {

                // we only support playlists on external media

                mProcessPlaylists = true;

               mGenreCache = new HashMap<String, Uri>();

             …

    preScan,这个函数很复杂:

    大概就是创建一个FileCache,用来缓存扫描文件的一些信息,例如last_modified等。这个FileCache是从MediaProvider中已有信息构建出来的,也就是历史信息。后面根据扫描得到的新信息来对应更新历史信息。

    postScan,这个函数做一些清除工作,例如以前有video生成了一些缩略图,现在video文件被干掉了,则对应的缩略图也要被干掉。

    另外还有一个mClient,这个是从MediaScannerClient派生下来的一个东西,里边保存了一个文件的一些信息。后续再分析。

    刚才说到,具体扫描工作是在processDirectory函数中完成的。这个是一个native函数。

    在frameworks/base/media/jni/android_media_MediaScanner.cpp中。

    三  MediaScanner JNI层分析

    MediaScanner JNI层内容比较多,单独搞一节分析吧。

    先看看android_media_MediaScanner这个文件。

    1. native_init函数,jni对应的函数如下

    static void

    android_media_MediaScanner_native_init(JNIEnv *env)

    {

         jclass clazz;

    clazz = env->FindClass("android/media/MediaScanner");

    //得都JAVA类中mNativeContext这个成员id

        fields.context = env->GetFieldID(clazz, "mNativeContext", "I");

       //不熟悉JNI的自己去学习下吧

    }

    3.       native_setup函数,jni对应函数如下:

    android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)

    {

    //创建MediaScanner对象

        MediaScanner *mp = createMediaScanner();

    //太变态了,自己不保存这个对象指针.

    //却把它设置到java对象的mNativeContext去保存

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

    }

    //创建MediaScanner函数

    static MediaScanner *createMediaScanner() {

    #if BUILD_WITH_FULL_STAGEFRIGHT

       ..

    //使用google自己的

      return new StagefrightMediaScanner;

       #endif

    #ifndef NO_OPENCORE

    //使用opencore提供的

    ….

        return new PVMediaScanner();

    #endif

    4.       processDirectories函数,jni对应如下:

    android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)

    {

        MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

    //每次都要回调到JAVA中去取这个Scanner!!

     ………

        const char *pathStr = env->GetStringUTFChars(path, NULL);

        const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);

    …….

    //又在C++这里搞一个client,然后把java的client放到C++Client中去保存

    //而且还是栈上的临时变量..

    MyMediaScannerClient myClient(env, client);

    //scanner扫描文件夹,用得是C++的client

        mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);

        env->ReleaseStringUTFChars(path, pathStr);

        env->ReleaseStringUTFChars(extensions, extensionsStr);

    }

    到这里似乎就没有了,那么扫描后的数据库是怎么更新的呢?为什么要传入一个client进去呢?看来必须得trace到scanner中去才知道了。

    四 PVMediaScanner

    这个在frameworks/av/media/libmedia/MediaScanner.cpp中。

    1.       processDirectory

    status_t MediaScanner::processDirectory(const char *path, const char* extensions,

            MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)

    {

        InitializeForThread();

        int error = 0;

    status_t result = PVMFSuccess;

    ….

    //调用client的设置区域函数

       client.setLocale(mLocale);

    //扫描文件夹,咋还没开始??

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

    ..

    2.       doProcessDirectory

    status_t MediaScanner::doProcessDirectory(char *path, int pathRemaining, const char* extensions,

            MediaScannerClient& client, ExceptionCheck exceptionCheck, void* exceptionEnv)

    {

    …终于看到点希望了

    //打开这个文件夹,枚举其中的内容。

    //题外话,这个时候FileManager肯定删不掉这个文件夹!!

        DIR* dir = opendir(path);

        while ((entry = readdir(dir))) {

            const char* name = entry->d_name;

    //不处理.和..文件夹

     if (isDirectory) {

    ……..

    //不处理.开头的文件夹。如果是文件夹,递归调用doProcessDirectory

    //深度优先啊!

                int err = doProcessDirectory(path, pathRemaining - nameLength - 1, extensions, client, exceptionCheck, exceptionEnv);

                    if (err) {

                        LOGE("Error processing '%s' - skipping/n", path);

                        continue;

                    }

                } else if (fileMatchesExtension(path, extensions)) {

    //是一个可以处理的文件,交给client处理

    //彻底疯掉了….这是干嘛呢???

                  client.scanFile(path, statbuf.st_mtime, statbuf.st_size);

    这里要解释下,刚才createMediaScanner中,明明创建的是PVMediaScanner,为何这里看得是MediaScanner代码呢?

    l         因为PVMediaScanner从MediaScanner中派生下来的,而且没有重载processDirectory函数

    l         Eclaire没有PVMediaScanner这样的东西,估计是froyo又改了点啥吧。

    FT,processDirctory无非是列举一个目录内容,然后又反回去调用client的scanFile处理了。为何搞这么复杂?只有回去看看C++的client干什么了。

    3.       MediaScannerClient---JNI层

    JNI中的这个类是这样的:

    class MyMediaScannerClient : public MediaScannerClient

    //这是它的scanFile实现

    virtual bool scanFile(const char* path, long long lastModified, long long fileSize)

        {

    //再次崩溃了,C++的client调用了刚才传进去的java Client的

    //scanFile函数…不过这次还传进去了该文件的路径,最后修改时间和文件大小。     

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

    …..想死,,,

        }

    没办法了,只能再去看看MediaScanner.java传进去的那个client了。

    4.       MediaScannerClient----JAVA层

    这个类在MediaScanner.java中实现。

    private class MyMediaScannerClient implements MediaScannerClient:

    public void scanFile(String path, long lastModified, long fileSize) {

    //调用doScanFile..很烦..

               doScanFile(path, null, lastModified, fileSize, false);

    //下面是doScanFile

    public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {

    //预处理,看看之前创建的文件缓存中有没有这个文件

       FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);

                    // rescan for metadata if file was modified since last scan

                    if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {

    //如果事先有这个文件的信息,则需要修改一些信息,如长度,最后修改时间等

           …..

    //真正的扫描文件

                            processFile(path, mimeType, this);

    //扫描完了,做最后处理

                        endFile(entry, ringtones, notifications, alarms, music, podcasts);

    processFile又是jni层的。

    对应android_media_MediaScanner_processFile函数

    android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)

    {

        MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

      //无语了,又搞一个  MyMediaScannerClient

    MyMediaScannerClient myClient(env, client);

        mp->processFile(pathStr, mimeTypeStr, myClient);

    …}

    第一次到PVMediaScanner中来了

    status_t PVMediaScanner::processFile(const char *path, const char* mimeType, MediaScannerClient& client)

    {

        status_t result;

        InitializeForThread();

    //调用client的beginFile,估计是做一些啥预处理

        client.setLocale(locale());

        client.beginFile();

       

        //LOGD("processFile %s mimeType: %s/n", path, mimeType);

        const char* extension = strrchr(path, '.');

        if (extension && strcasecmp(extension, ".mp3") == 0) {

            result = parseMP3(path, client);//client又传进去了

    …根据后缀名去扫描….

    }

    client.endFile();

    到parseMP3去看看。这个在

    static PVMFStatus parseMP3(const char *filename, MediaScannerClient& client)

    {//这个函数太专业了,和编解码有关,我们重点关注client在这里干什么了

    //原来,解析器从文件中解析出一个信息,就调用client的addStringTag

    if (!client.addStringTag("duration", buffer))

    ….

    addStringTag在JNI的client中处理。

    这个MediaScannerClient是在opencore中的那个MediaScanner.cpp中实现的,而android_media_MediaScanner.cpp中的是MyMediaScannerClient,从MediaScannerClient派生下来的

        bool MediaScannerClient::addStringTag(const char* name, const char* value)

    {

        if (mLocaleEncoding != kEncodingNone) {

            //字符串编码之类的转换。不详述了

            bool nonAscii = false;

            const char* chp = value;

            char ch;

            while ((ch = *chp++)) {

                if (ch & 0x80) {

                    nonAscii = true;

                    break;

                }

            }

    //如果不是ASCII编码的话,内部先保存一下这些个tag信息

    //待会扫描完后再集中做一次字符串编码转换

            if (nonAscii) {

                // save the strings for later so they can be used for native encoding detection

                mNames->push_back(name);

                mValues->push_back(value);

                return true;

            }

            // else fall through

        }

    //调用子类的handleStringTag

        return handleStringTag(name, value);

    }

    class MyMediaScannerClient : public MediaScannerClient{

    //调用到子类的handleStringTag了

    virtual bool handleStringTag(const char* name, const char* value)

    {

    //又传递到JAVA层的handleStringTag来处理

    //麻木了..

        mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr, valueStr);

           }

    JAVA层

    MediaScannerService中的MyMediaScannerClient类

    public void handleStringTag(String name, String value) {

    //下层扫描的文件tag信息,全部处理后赋值给java层这个MyScannerClient了

    例如MP3的title,专辑名等等。

                   ….

                  int num = parseSubstring(value, 0, 0);

                    mTrack = (num * 1000) + (mTrack % 1000);

                } else if (name.equalsIgnoreCase("duration")) {

                    mDuration = parseSubstring(value, 0, 0);

                } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {

                    mWriter = value.trim();

    到这里了,还没写到数据库呢?啥时候更新数据库?看来是在client.endFile()中了。

    但是这个endClient并没有调用到JAVA层去。那在哪里结束呢?

    还记得JAVA中的doScanFile函数吗,对了,这个endFile就是在那里直接由JAVA调用的。

    private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,

                    boolean alarms, boolean music, boolean podcasts)

                    throws RemoteException {

                // update database

                Uri tableUri;

                boolean isAudio = MediaFile.isAudioFileType(mFileType);

                boolean isVideo = MediaFile.isVideoFileType(mFileType);

                boolean isImage = MediaFile.isImageFileType(mFileType);

              ….

    //来了一个新文件,直接插入数据库

                    result = mMediaProvider.insert(tableUri, values);

    //或者更新数据库

    mMediaProvider.update(result, values, null, null);

    这回真算是完了。

    5.流程总结

    l         MediaScanner(MS)调用scanDirectories中的processDirectory,进入到JNI层

    l         JNI调用PVMediaScanner的processDirectory

    l         PVMediaScanner的processDirectory为目录下的文件调用MyMediaScannerClient的scanFile

    l         MyMediaScannerClient

  • 相关阅读:
    dfs模板
    24点
    个人阅读三
    个人阅读作业2:关于项目经历的心得
    代码复审
    Pair Project1:电梯控制程序
    第二次个人项目——阅读经典教材
    THE First Individual Project
    个人阅读作业3
    个人阅读作业2
  • 原文地址:https://www.cnblogs.com/senior-engineer/p/4708321.html
Copyright © 2011-2022 走看看