Android MediaScanner MediaProvider流程
源码解析
时序图
时序图是根据我自己修剪过的框架画的,有些地方跟源码不一样,但是大体是差不多的。
链接:
MediaScanner时序图.
MediaSacannerReeiver.java
接收android.intent.action.MEDIA_MOUNTED广播启动MediaScannerService
// An highlighted block
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.java
第一次被启动走onCreate,将自己的线程启动
// A code block
@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();
// 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();
}
第二次启动走onStartCommand,从广播里面获取信息发送给mServiceHandler
// An highlighted block
@Override
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;
}
mServiceHandler 解析路径和volume信息然后开始扫描:
// An highlighted block
private final class ServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
...
scan(directories, volume);
...
}
};
scan(String[] directories, String volumeName)方法中先 openDatabase(volumeName);发消息给MedaiProvider,让数据库先准备好,然后MediaScanner scanner = new MediaScanner(this, volumeName),scanner.scanDirectories(directories);MediaScanner.java开始扫描。
MediaProvider.java
MediaScanner.java
具体流程可以在上面提供的时序图查看,这里主要讲解几个重要的方法:
1、prescan
prescan主要是做老数据删除,先从数据库将数据读取出来,然后判断文件存不存在,不存在就删除。
// An highlighted block
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
Cursor c = null;
String where = null;
String[] selectionArgs = null;
mPlayLists.clear();//清除列表,这个列表后面用来保存每个媒体问的信息:id,修改时间等
if (filePath != null) {//获取单个数据
// query for only one file
where = MediaStore.Files.FileColumns._ID + ">?" +
" AND " + Files.FileColumns.DATA + "=?";
selectionArgs = new String[] { "", filePath };
} else {//从数据库files表获取所有数据
where = MediaStore.Files.FileColumns._ID + ">?";
selectionArgs = new String[] { "" };
}
mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE);
mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND);
mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT);
// Tell the provider to not delete the file.
// If the file is truly gone the delete is unnecessary, and we want to avoid
// accidentally deleting files that are really there (this may happen if the
// filesystem is mounted and unmounted while the scanner is running).
Uri.Builder builder = mFilesUri.buildUpon();
builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build());
// Build the list of files from the content provider
try {
if (prescanFiles) {
// First read existing files from the files table.
// Because we'll be deleting entries for missing files as we go,
// we need to query the database in small batches, to avoid problems
// with CursorWindow positioning.
long lastId = Long.MIN_VALUE;
//每次操作限制读取1000个数据
Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
while (true) {
selectionArgs[0] = "" + lastId;
if (c != null) {
c.close();
c = null;
}
c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION,
where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
if (c == null) {
break;
}
int num = c.getCount();
//获取到的数据个数判断是否为0,空的话就不用处理了
if (num == 0) {
break;
}
//对1000个数据进行处理
while (c.moveToNext()) {
long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
//数据库里获取文件最后修改时间
long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
lastId = rowId;
// Only consider entries with absolute path names.
// This allows storing URIs in the database without the
// media scanner removing them.
if (path != null && path.startsWith("/")) {
boolean exists = false;
try {
//查询文件在系统里是否存在
exists = Os.access(path, android.system.OsConstants.F_OK);
} catch (ErrnoException e1) {
}
if (!exists && !MtpConstants.isAbstractObject(format)) {
// do not delete missing playlists, since they may have been
// modified by the user.
// The user can delete them in the media player instead.
// instead, clear the path and lastModified fields in the row
MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);
int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);
//添加要删除的id
deleter.delete(rowId);
//如果.nomedia文件被删除了,那么就需要重新扫描这个文件夹,因为之前没有扫描。
if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
//开始删除老数据
deleter.flush();
String parent = new File(path).getParent();
mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null);
}
}
}
}
}
}
}
}
finally {
if (c != null) {
c.close();
}
//开始删除老数据
deleter.flush();
}
// compute original size of images
mOriginalCount = 0;
c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null);
if (c != null) {
mOriginalCount = c.getCount();
c.close();
}
}
2、beginFile
3、endFile
MediaScanner.cpp
StagefrightMediaScanner.cpp
配置修改
修改数据库路径
修改数据库WAL模式
- 设置db.enableWriteAheadLogging();可以在MediaProvider写数据的时候,UI读数据不会被阻塞。读写不会阻塞,但是只允许同时只有一用户写。设置WAL模式可以提高数据库写速度,降低磁盘IO,但是读数据就会变慢。具体原理可以参考
链接: SQLite分析之WAL机制.
存在的问题
- Android MediaProvider框架对于手机扫描来说是很贴切的,但是对于汽车车载系统来说就不是很友好,因为车机可能需要用USB来存储媒体数据,新USB扫描速度很慢,手机一般不需要插USB。这节和下一节是针对车载系统的一些关于USB扫描的探讨和优化。
问题:
1、对于不同USB,MediaProvider会保留不同的数据库,占用多余磁盘空间;
2、扫描时会读取音视频文件title等信息,读取文件磁盘IO会导致扫描速度变慢,原本需要5分钟,可能就变成20分钟;
3、prescan预扫描时候可以读取数据,并且prescan后也没有明确的广播通知,如果数据被大量删除,UI会读取到已经删除的数据;
4、扫描是顺序扫描,如果一直在扫描歌曲还没扫描到视频,那么视频要等好久才能检索到;
性能优化
一、扫描方案优化
-
对于IO读写慢的问题是无法回避的,为什么要读取文件信息,因为需要添加歌曲视频的专辑标题等信息,在UI侧才能做成专辑等列表。但是从用户的角度分析,如果我U盘插入车机,要听音乐,我大体上用打开歌曲列表,或者文件夹,就可以快速找到自己想要播放的歌曲,或者是收藏列表。可能专辑风格艺术家等列表被打开的概率只有20%,但是这20%的概率却占用了扫描80%的时间,我觉得是不合理的,但是又不能不做,所以我觉得,可以先用很短的时间做成一个快表,这个快表能够提供歌曲视频列表,文件夹信息和收藏列表,之后再做专辑列表。我自己也尝试去做这个快表,如果整体扫描时间是20分钟的话,做快表的时间在5-15秒就可以完成,理论上可以达到5秒。等这个快表做成,就开始走正常的扫描流程,或者说两者一起并行运行也是可以的。
-
这个方案的缺陷就是CPU占比在一瞬间会比较高,而且原来扫描流程会慢几秒(我感觉可以省略)。
-
快表的做成我大部分是用c++/c语言写的,生成一个so库,Java调用so库来扫描和做成数据库,同时提供一个Provider给客户端调用。快速扫描功能包括预扫描删除没用的老数据、扫描数据时会判别是否存在、是否更改过,每个文件都保存上级文件夹的id,有一个歌曲表一个视频表和一个文件夹表。
-
快表的做成我已经完成80%了,由于工作问题暂时停了,如果需要可以看我GitHub: 链接:
MediaScanner快表做成.
UI部分我做的很随意,不要吐槽,主要还是想快速实现,所以代码也很乱,之后会整理好。
二、数据库优化
-
wal模式可以提高数据的写速度;
-
同时也可以通过事务,来减少磁盘IO;
-
在插入数据时会判断数据是否存在是否更新过,这个时候需要通过路径去数据库query,所以建议做个绝对路径的索引,索引就做这个就够用了;
-
文件夹如果没有文件就不需要插入到数据库中了,所以在插入歌曲视频的时候才插入上级文件夹就可以了;
待续。。。