前言
MediaProvider继承自ContentProvider,是Android用于存储图片、音频、视频和文档等多媒体信息,提供给其他需要的应用使用。
今天就对MediaProvider应用代码进行一定学习。参考网上大佬文章,记录一下过程。
这里是用了Android P源码分析
正文
MediaProvider存在路径:
\packages\providers\MediaProvider
扫描后存在的数据目录
/data/data/com.android.providers.media/databases # 或 /data/user_de/0/com.android.providers.media/databases
一般是在第一个目录,但也存在放在第二个目录,这个具体看平台是否有改动。
MediaProvider存在两个数据库
internal.db #内部数据库 external.db #外部数据库
重点
internal.db是查询指定目录的数据
//[/system/media, /oem/media, /product/media] directories = new String[] { Environment.getRootDirectory() + "/media", Environment.getOemDirectory() + "/media", Environment.getProductDirectory() + "/media", };
external.db 是包括sd目录和插入的磁盘目录中所有数据。
进入扫描
在继续跟进时,我们需要知道有几个入口让MediaProvider进入扫描,先看大佬们整理的
从上面看,可以知道,进入扫描于两种方式
-
通过广播监听
-
通过服务绑定
通过这两种方式,我们进入分析MediaProvider。
广播监听
一般广播监听有两种,一种是动态广播,一种是静态广播,各有优点。一般看一个应用会先看AndroidManifest.xml配置。
AndroidManifest.xml
我们关注需要的,比如磁盘挂载等广播,我们会发现MediaScannerReceiver就是我们需要找的广播。
<receiver android:name="MediaScannerReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.LOCALE_CHANGED" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.MEDIA_MOUNTED" /> <data android:scheme="file" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.MEDIA_UNMOUNTED" /> <data android:scheme="file" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" /> <data android:scheme="file" /> </intent-filter> </receiver>
上面监听了好些重要的广播,我们直接进入看MediaScannerReceiver怎么处理的。
MediaScannerReceiver.java
public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final Uri uri = intent.getData(); if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { //开机广播,只扫描内置磁盘,内置的磁盘一定存在 //INTERNAL_VOLUME = "internal" scan(context, MediaProvider.INTERNAL_VOLUME); } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { //语言切换 scanTranslatable(context); } else { if (uri.getScheme().equals("file")) { String path = uri.getPath(); // 我这平台上 externalStoragePath = "/storage/emulated/0"; String externalStoragePath = Environment.getExternalStorageDirectory().getPath(); // 我这平台上 legacyPath = "/sdcard"; String legacyPath = Environment.getLegacyExternalStorageDirectory().getPath(); try { path = new File(path).getCanonicalPath(); } catch (IOException e) { return; } if (path.startsWith(legacyPath)) { //需要用绝对路径,确保统一 path = externalStoragePath + path.substring(legacyPath.length()); } if (Intent.ACTION_MEDIA_MOUNTED.equals(action)) { //根据磁盘挂载进行扫描外置磁盘 //EXTERNAL_VOLUME = "external" scan(context, MediaProvider.EXTERNAL_VOLUME); } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) && path != null && path.startsWith(externalStoragePath + "/")) { //只对内置sdcard多媒体更新进行扫描 scanFile(context, path); } } } }
上面涉及如下几个方法
-
scan(Context context, String volume) //扫描内置或外置磁盘
-
void scanFile(Context context, String path) //扫描指定文件[只能是文件!]
-
scanTranslatable(Context context) // 设备语言变化了调用
上面是三个方法有以个共同点,都是startService()启动了MediaScannerService
context.startService(new Intent(context, MediaScannerService.class).putExtras(args));
MediaScannerService.java
MediaScannerService是个服务,一般都先看onCreate()和onStartCommand()。
-
onCreate()
public void onCreate() { //防止扫描过程中CPU睡死过去,扫描中需要 PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); //略 //启动子线程,第二个参数Runnable传入的是this,也就是说MediaScannerService实现了run() Thread thr = new Thread(null, this, "MediaScannerService"); thr.start(); }
-
onStartCommand()
public int onStartCommand(Intent intent, int flags, int startId) { //等待mServiceHandler初始化 while (mServiceHandler == null) { synchronized (this) { try { wait(100); } catch (InterruptedException e) { } } } //略 //放入消息队列,让ServiceHandler处理。 Message msg = mServiceHandler.obtainMessage(); msg.arg1 = startId; msg.obj = intent.getExtras(); mServiceHandler.sendMessage(msg); return Service.START_REDELIVER_INTENT; }
这里主要是把intent中参数丢入消息队列,让mServiceHandler处理。
-
run()
上面启动了子线程,MediaScannerService也实现了run()
public void run() { //将优先级降低到其他后台线程以下以避免干扰其他服务。 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE); //很熟悉吧,启动Looper,这样就可以使用ServiceHandler Looper.prepare(); mServiceLooper = Looper.myLooper(); mServiceHandler = new ServiceHandler(); Looper.loop(); }
-
ServiceHandler
private final class ServiceHandler extends Handler { @Override public void handleMessage(Message msg) { Bundle arguments = (Bundle) msg.obj; if (arguments == null) { return; } String filePath = arguments.getString("filepath"); try { //文件扫描 if (filePath != null) { IBinder binder = arguments.getIBinder("listener"); //如果是从广播来的,binder= null IMediaScannerListener listener = (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder)); Uri uri = null; try { //【重点】scanFile uri = scanFile(filePath, arguments.getString("mimetype")); } catch (Exception e) { Log.e(TAG, "Exception scanning file", e); } if (listener != null) { listener.scanCompleted(filePath, uri); } } else if (arguments.getBoolean(MediaStore.RETRANSLATE_CALL)) { //设备语言改变后的 ContentProviderClient mediaProvider = getBaseContext().getContentResolver() .acquireContentProviderClient(MediaStore.AUTHORITY); mediaProvider.call(MediaStore.RETRANSLATE_CALL, null, null); } else { //扫描挂载的磁盘 String volume = arguments.getString("volume"); String[] directories = null; //内置磁盘 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; } } if (directories != null) { //【重点】scan scan(directories, volume); } } } catch (Exception e) { Log.e(TAG, "Exception in handleMessage", e); } //扫描结束,自动销毁 stopSelf(msg.arg1); } }
上面涉及scanFile()和scan()方法。主要是的区别是前者扫描文件,后者是扫描磁盘。
-
scan()
private void scan(String[] directories, String volumeName) { Uri uri = Uri.parse("file://" + directories[0]); //扫描中不让休眠 mWakeLock.acquire(); try { ContentValues values = new ContentValues(); //MEDIA_SCANNER_VOLUME = "volume" values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName); //让MeidaProvider做一些准备工作??(没有具体看,网上大佬这么说的) //scanUri = content://media/none/media_scanner Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values); //发出开始扫描广播 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri)); try { if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) { // 打开EXTERNAL数据库[为啥要判断,可以看文章中【重点】部分] openDatabase(volumeName); } //创建MediaScanner,调用scanDirectories扫描目标文件夹。 try (MediaScanner scanner = new MediaScanner(this, volumeName)) { //directories可能是[/storage/emulated/0, /storage/udisk0] scanner.scanDirectories(directories); } } catch (Exception e) { Log.e(TAG, "exception in MediaScanner.scan()", e); } // 通过 delete 这个 uri,让 MeidaProvider 做一些清理工作 getContentResolver().delete(scanUri, null, null); } finally { //发送扫描完成的广播 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri)); Log.d(TAG, "scan +++++ ACTION_MEDIA_SCANNER_FINISHED +++++ : "); mWakeLock.release(); } }
上面正在扫描工作是在MediaScanner中。
-
scanFile()
private Uri scanFile(String path, String mimeType) { String volumeName = MediaProvider.EXTERNAL_VOLUME; try (MediaScanner scanner = new MediaScanner(this, volumeName)) { String canonicalPath = new File(path).getCanonicalPath(); return scanner.scanSingleFile(canonicalPath, mimeType); } catch (Exception e) { return null; } }
scanFile()指定扫描某个文件,最终的扫描逻辑依旧在MediaScanner中。
MediaScanner后续单独分析,这里不做过多的介绍。从上面看,从广播进入扫描的就分析完了。
服务绑定
除了ACTION_MEDIA_SCANNER_SCAN_FILE广播,Android还可以通过bindService机制得到IMediaScannerService代理接口,再通过该接口的requestScanFile()或scanFile()请求扫描,不过这个只能扫描文件。
先看看MediaScannerService返回的IBinder
@Override public IBinder onBind(Intent intent) { //返回binder代理 return mBinder; } private final IMediaScannerService.Stub mBinder = new IMediaScannerService.Stub() { //方法一 public void requestScanFile(String path, String mimeType, IMediaScannerListener listener) { Bundle args = new Bundle(); args.putString("filepath", path); args.putString("mimetype", mimeType); if (listener != null) { args.putIBinder("listener", listener.asBinder()); } startService(new Intent(MediaScannerService.this, MediaScannerService.class).putExtras(args)); } //方法二 public void scanFile(String path, String mimeType) { requestScanFile(path, mimeType, null); } };
有两个方法requestScanFile()和scanFile(),最终还是调用了requestScanFile,也就是带上参数并启动服务。
其中IMediaScannerService定义的的目录
frameworks\base\media\java\android\media\IMediaScannerService.aidl
由于一般不涉及这块,懒得继续看。推荐最后的参考文章,此文对这块讲解比较详细。
参考文章
-
《》
-
《》
-
《》
-
《》
-
《》[推荐看这个]