本人负责公司手机文件管理器模块的开发,经常跟MediaScanner打交道,本篇跟大家一起讨论一下MediaScanner的整体结构、MediaScanner的扫描流程、如何使用MediaScanner进行自定义扫描。

MediaScanner是Android系统Media的基础,系统启动之初,就扫描出Media文件供后续使用,有新媒体加入或者删除掉媒体文件,也需要更新相应的媒体库。Android的Music、Gallery等播放或呈现媒体文件的程序也都基于稳定的MediaScanner扫描媒体文件的结果,否则,会发现程序操作的Media文件的URI根本不是实际对应的Media文件,或者甚至来电铃声和闹铃等都会有问题。


一、MediaScanner的整体结构

在整个Android源代码中,MediaScanner整体结构的代码分布在两个地方:packages\providers\MediaProvider 和frameworks\base\media\下,前者作为一个单独的package,用来接收扫描广播和操作contentProvider、调用扫描服务接口完成扫描;后者中包含了MediaScanner的 jni 和 java 文件,扫描的大部分工作都在这里。

首先来看一下MediaProvider这个package:

MediaProvider包括五个类:

  • com.android.providers.media.MediaProvider
  • com.android.providers.media.MediaScannerCursor
  • com.android.providers.media.MediaScannerReceiver
  • com.android.providers.media.MediaScannerService
  • com.android.providers.media.MediaThumbRequest
1.MediaProvider

此类继承ContentProvider,实现一个内容提供者。主要用于创建媒体库的数据库表。有自己创建过ContentProvider的同学相信都比较清楚的。
特别说明一下在MediaProvider中有个广播接收者,代码如下:

   1: private BroadcastReceiver mUnmountReceiver = new BroadcastReceiver() {
   2:         @Override
   3:         public void onReceive(Context context, Intent intent) {
   4:             if (intent.getAction().equals(Intent.ACTION_MEDIA_EJECT)) {
   5:                 // Remove the external volume and then notify all cursors backed by
   6:                 // data on that volume
   7:                 detachVolume(Uri.parse("content://media/external"));
   8:                 sFolderArtMap.clear();
   9:                 MiniThumbFile.reset();
  10:             }
  11:         }
  12:     };

此接收者是用来接收Sdcard卸载的广播。当Sdcard从手机中分离出来的时候,Sdcard中的媒体文件相对应的数据库将无法操作。

   1: private void detachVolume(Uri uri) {
   2:        //判断是否是同一个进程
   3:        if (Process.supportsProcesses() && Binder.getCallingPid() != Process.myPid()) {
   4:            throw new SecurityException(
   5:                    "Opening and closing databases not allowed.");
   6:        }
   7:         //此方法只是操作Sdcard的媒体数据库,不支持手机内存的媒体数据库
   8:        String volume = uri.getPathSegments().get(0);
   9:        if (INTERNAL_VOLUME.equals(volume)) {
  10:            throw new UnsupportedOperationException(
  11:                    "Deleting the internal volume is not allowed");
  12:        } else if (!EXTERNAL_VOLUME.equals(volume)) {
  13:            throw new IllegalArgumentException(
  14:                    "There is no volume named " + volume);
  15:        }
  16: 
  17:        synchronized (mDatabases) {
  18:            DatabaseHelper database = mDatabases.get(volume);
  19:            if (database == null) return;
  20: 
  21:            try {
  22:                // touch the database file to show it is most recently used
  23:                File file = new File(database.getReadableDatabase().getPath());
  24:                file.setLastModified(System.currentTimeMillis());
  25:            } catch (SQLException e) {
  26:                Log.e(TAG, "Can't touch database file", e);
  27:            }
  28:             //移除数据库
  29:            mDatabases.remove(volume);
  30:            database.close();
  31:        }
  32: 
  33:        getContext().getContentResolver().notifyChange(uri, null);
  34:        if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume);
  35:    }

注意移除数据库并非删除数据库文件(*.db),mDatabases是一个HashMap<String,DatabaseHelper>,移除的含义是暂时无法操作,也可以说说是查询返回的数据都是空的。

2.MediaScannerCursor

一个自定义游标,用来查询媒体文件的扫描状态。主要有一个volume字段,用来区分是内置媒体数据库还是Sdcard的媒体数据库。

3.MediaScannerReceiver

此类实现广播接收者。接收到广播的时候对手机的媒体文件进行扫描。

   1: public class MediaScannerReceiver extends BroadcastReceiver
   2: {
   3:     private final static String TAG = "MediaScannerReceiver";
   4: 
   5:     @Override
   6:     public void onReceive(Context context, Intent intent) {
   7:         String action = intent.getAction();
   8:         Uri uri = intent.getData();
   9:         String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
  10:         //系统启动完毕
  11:         if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
  12:             // scan internal storage
  13:             scan(context, MediaProvider.INTERNAL_VOLUME);
  14:         } else {
  15:             if (uri.getScheme().equals("file")) {
  16:                 // handle intents related to external storage
  17:                 String path = uri.getPath();
  18:                 if (action.equals(Intent.ACTION_MEDIA_MOUNTED/*Sdcard挂载广播*/) && 
  19:                         externalStoragePath.equals(path)) {
  20:                     scan(context, MediaProvider.EXTERNAL_VOLUME);
  21:                 } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE/*单个文件扫描广播*/) &&
  22:                         path != null && path.startsWith(externalStoragePath + "/")) {
  23:                     scanFile(context, path);
  24:                 }
  25:             }
  26:         }
  27:     }

扫描分为两种三种情况:

a,启动完毕扫面手机内存中的媒体文件

b.sdcard挂载完毕扫描扩展卡的媒体文件

c,扫描单个文件

应用实例:我们可以发送不同的广播让系统去扫描媒体文件。当需要扫描单个文件的时候需要设置一些参数,如下:

   1: /**
   2:      * 扫描文件
   3:      * 
   4:      * @param filePath 文件路径
   5:      * @author http://t.sina.com.cn/halzhang
   6:      */
   7:     public void scanOneFile(final String filePath) {
   8:         Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
   9:         Uri uri = Uri.parse("file://" + filePath);
  10:         intent.setData(uri);
  11:         sendBroadcast(intent);
  12:     }

接着看一下scanscenFile两个方法:

   1: private void scan(Context context, String volume/*内置卡或者外置卡*/) {
   2:        Bundle args = new Bundle();
   3:        args.putString("volume", volume);
   4:        context.startService(
   5:                new Intent(context, MediaScannerService.class).putExtras(args));
   6:    }    
   7: 
   8:    private void scanFile(Context context, String path/*文件路径*/) {
   9:        Bundle args = new Bundle();
  10:        args.putString("filepath", path);
  11:        context.startService(
  12:                new Intent(context, MediaScannerService.class).putExtras(args));
 13:    }  

两个方法都是启动MediaScannerService去扫描媒体文件的。


4.MediaScannerService (MSS)

MSS实现了Runnable,所以必然的需要实现run方法了,代码如下:

   1: public void run()
   2:    {
   3:        // reduce priority below other background threads to avoid interfering
   4:        // with other services at boot time.
   5:        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
   6:                Process.THREAD_PRIORITY_LESS_FAVORABLE);
   7:        Looper.prepare();
   8: 
   9:        mServiceLooper = Looper.myLooper();
  10:        mServiceHandler = new ServiceHandler();
  11: 
  12:        Looper.loop();
  13:    }
在run方法中设置了线程的优先级,优先级比较低,主要为了避免跟其他服务抢夺资源。还有就是利用looper对ServiceHandler的消息进行循环控制。

接着看一下ServiceHandler的实现代码:

   1: private final class ServiceHandler extends Handler
   2:    {
   3:        @Override
   4:        public void handleMessage(Message msg)
   5:        {
   6:            Bundle arguments = (Bundle) msg.obj;
   7:            //获取文件路径
   8:            String filePath = arguments.getString("filepath");
   9:            
  10:            try {
  11:                if (filePath != null) {
  12:                     //文件路径不为空,则调用扫面当个文件的方法
  13:                    IBinder binder = arguments.getIBinder("listener");
  14:                    IMediaScannerListener listener = 
  15:                            (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
  16:                    Uri uri = scanFile(filePath, arguments.getString("mimetype"));//扫描单个文件
  17:                    if (listener != null) {
  18:                         //执行扫描完成方法
  19:                        listener.scanCompleted(filePath, uri);
  20:                    }
  21:                } else {
  22:                     //如果文件路径为空,则获取扫面手机内存或者sdcard
  23:                    String volume = arguments.getString("volume");
  24:                    String[] directories = null;
  25:                    //内置卡
  26:                    if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
  27:                        // scan internal media storage
  28:                        directories = new String[] {
  29:                                Environment.getRootDirectory() + "/media",
  30:                        };
  31:                    }//外置卡
  32:                    else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
  33:                        // scan external storage
  34:                        directories = new String[] {
  35:                                Environment.getExternalStorageDirectory().getPath(),
  36:                                };
  37:                    }
  38:                    
  39:                    if (directories != null) {
  40:                        if (Config.LOGD) Log.d(TAG, "start scanning volume " + volume);
  41:                         //扫描
  42:                        scan(directories, volume);
  43:                        if (Config.LOGD) Log.d(TAG, "done scanning volume " + volume);
  44:                    }
  45:                }
  46:            } catch (Exception e) {
  47:                Log.e(TAG, "Exception in handleMessage", e);
  48:            }
  49: 
  50:            stopSelf(msg.arg1);
  51:        }
  52:    };

在ServiceHandler中主要根据相关参数来调用不同的扫描方法。

那是在哪里调用ServiceHandler发送消息的呢?请看如下代码:

   1: @Override
   2: public void onCreate() {
   3:     PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
   4:     mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
   5:     //启用新线程,这样就可以避免阻塞,执行run,初始化成员变量loop和handler
   6:     Thread thr = new Thread(null, this, "MediaScannerService");
   7:     thr.start();
   8: }
   9: 
  10: @Override
  11: public int onStartCommand(Intent intent, int flags, int startId) {
  12:     while (mServiceHandler == null) {
  13:         synchronized (this) {
  14:             try {
  15:                 wait(100);
  16:             } catch (InterruptedException e) {
  17:             }
  18:         }
  19:     }
  20: 
  21:     if (intent == null) {
  22:         Log.e(TAG, "Intent is null in onStartCommand: ", new NullPointerException());
  23:         return Service.START_NOT_STICKY;
  24:     }
  25: 
  26:     Message msg = mServiceHandler.obtainMessage();
  27:     msg.arg1 = startId;
  28:     msg.obj = intent.getExtras();
  29:     //ServiceHandler发送消息
  30:     mServiceHandler.sendMessage(msg);
  31: 
  32:     // Try again later if we are killed before we can finish scanning.
  33:     return Service.START_REDELIVER_INTENT;
  34: }
  35: 
  36: @Override
  37: public void onDestroy() {
  38:     // Make sure thread has started before telling it to quit.
  39:     while (mServiceLooper == null) {
  40:         synchronized (this) {
  41:             try {
  42:                 wait(100);
  43:             } catch (InterruptedException e) {
  44:             }
  45:         }
  46:     }
  47:     mServiceLooper.quit();
  48: }
以上三个方法是属于Service的生命周期的。当我们调用startService的时候,如果对应的Service还未创建就会调用onCreate方法===方法。每次startService的时候就调用onStartCommand,所以ServiceHandler就在此发送消息了。

最后,稍微看一下MSS里面扫描方面。主要是调用MediaScanner对媒体文件进行扫描分析的。至于MediaScanner的实现以后在分析。

   1: private void openDatabase(String volumeName) {
   2:        try {
   3:            ContentValues values = new ContentValues();
   4:            values.put("name", volumeName);
   5:            getContentResolver().insert(Uri.parse("content://media/"), values);
   6:        } catch (IllegalArgumentException ex) {
   7:            Log.w(TAG, "failed to open media database");
   8:        }         
   9:    }
  10: 
  11:    private void closeDatabase(String volumeName) {
  12:        try {
  13:            getContentResolver().delete(
  14:                    Uri.parse("content://media/" + volumeName), null, null);
  15:        } catch (Exception e) {
  16:            Log.w(TAG, "failed to close media database " + volumeName + " exception: " + e);
  17:        }
  18:    }
  19: //创建扫描器
  20:    private MediaScanner createMediaScanner() {
  21:        MediaScanner scanner = new MediaScanner(this);
  22:        Locale locale = getResources().getConfiguration().locale;
  23:        if (locale != null) {
  24:            String language = locale.getLanguage();
  25:            String country = locale.getCountry();
  26:            String localeString = null;
  27:            if (language != null) {
  28:                if (country != null) {
  29:                    scanner.setLocale(language + "_" + country);
  30:                } else {
  31:                    scanner.setLocale(language);
  32:                }
  33:            }    
  34:        }
  35:        
  36:        return scanner;
  37:    }
  38: //扫描目录
  39:    private void scan(String[] directories, String volumeName) {
  40:        // don't sleep while scanning
  41:        mWakeLock.acquire();
  42: 
  43:        ContentValues values = new ContentValues();
  44:        values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
  45:        Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
  46: 
  47:        Uri uri = Uri.parse("file://" + directories[0]);
  48:        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
  49:        
  50:        try {
  51:            if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
  52:                 openDatabase(volumeName);    
  53:            }
  54: 
  55:            MediaScanner scanner = createMediaScanner();
  56:            scanner.scanDirectories(directories, volumeName);
  57:        } catch (Exception e) {
  58:            Log.e(TAG, "exception in MediaScanner.scan()", e); 
  59:        }
  60: 
  61:        getContentResolver().delete(scanUri, null, null);
  62: 
  63:        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
  64:        mWakeLock.release();
  65:    }
  66: //扫描文件
  67: private Uri scanFile(String path, String mimeType) {
  68:         String volumeName = MediaProvider.INTERNAL_VOLUME;
  69:         String externalStoragePath = Environment.getExternalStorageDirectory().getPath();
  70: 
  71:         if (path.startsWith(externalStoragePath)) {
  72:             volumeName = MediaProvider.EXTERNAL_VOLUME;
  73:             openDatabase(volumeName);
  74:         }
  75:         MediaScanner scanner = createMediaScanner();
  76:         //扫描单个文件
  77:         return scanner.scanSingleFile(path, volumeName, mimeType);
  78:     }

在MediaProvider中还有一个类:MediaThumbRequest,用来创建预览图的,比如视频的预览图,图片的预览图,音频的专辑图片…这些图片的信息也是保存在数据库的,有兴趣的同学可以自己打开数据库看看里面的表。如下图:



二、MediaScanner的扫描流程

这里绘制了一张MediaScanner从接收广播开始,进行扫描工作的整个流程图:



MediaScannerReceiver 会在任何的 ACTION_BOOT_COMPLETED, ACTION_MEDIA_MOUNTED 或 ACTION_MEDIA_SCANNER_SCAN_FILE 意图( intent )发出的时候启动。因为解析媒体文件 的元数据或许会需要很长时间 ,所以MediaScannerReceiver 会启动MediaScannerService 。

MediaScannerService 调用一个公用类 MediaScanner 去处理真正的工作。MediaScannerReceiver 维持两种扫描目录:一种是内部卷(internal volume )指向$(ANDROID_ROOT)/media. 另一种是外部卷(external volume )指向 $(EXTERNAL_STORAGE).

扫描和解析工作位于 JAVA 层和 C++ 层。 JAVA 层是启动器。 MediaScanner 扫描所有目录,如下步骤:


1.JAVA 层初始化

在这一步骤中,它会根据目录是在内部卷还是外部卷打开不同的数据库。

2.Java 层预扫描

首先清除文件和播放列表的缓存条目。然后根据 MediaProvider 返回的请求结果生成新文件和播放列表缓存条目。

3.C++ 层处理目录

列举出所有文件和特定的所有子目录(如果子目录包含一个 .nomedia 隐藏文件,则不会被列举出来。)。被列举的文件是根据文件扩展来判断文件是否被支持。如果支持这种文件扩展, C++ 层就会回调到 JAVA 层扫描文件。这种扩展就会被扫描到 MediaFile.java 中列出。下面是支持的文件扩展列表。


到此为止,MediaProvider算是讲完了,下一篇跟大家一起学习一下frameworks\base\media\下的MediaScanner相关文件。


更多相关文章

  1. FFMPEG移植到Android(ubuntu环境下)
  2. Android(安卓)Studio 中自带Android(安卓)Monitor监控内存溢出浅
  3. Android工程目录结构简介
  4. 从零学Android(十三)、Android中的数据存储方式简介
  5. [置顶] 【小超_Android】2014年框架类源码年度精品汇总
  6. android 截图
  7. 安卓文件读写操作方法汇总
  8. android monitor tool (7.0 监控文件系统 native层实现)
  9. Android中的各种Adapter

随机推荐

  1. Android通过PagerSnapHelper改造Recycler
  2. Android添加垂直滚动ScrollView
  3. Android(安卓)自定义控件打造史上最简单
  4. Android(安卓)中文 API (24) ―― MultiAut
  5. Android之网络请求7————OkHttp源码4:
  6. Cordova 3.x 基础(13) -- 为Android(安卓)A
  7. Android实现CBC加解密
  8. dx 工具的使用
  9. 获取Android设备电池电量状态
  10. 设置 TextView 文字居中