10.3 MediaScanner分析
现在分析媒体扫描器MediaScanner的工作原理,它将纵跨Java层、JNI层,以及Native层。先看它在Java层中的内容。
10.3.1 Java层分析
1.创建MediaScanner
认识一下MediaScanner,它的代码如下所示:
[—>MediaScanner.java]
public class MediaScanner
{
static{
/*
加载libmedia_jni.so,这么重要的库竟然放在如此不起眼的MediaScanner类中加载。个人觉得,可能是因为开机后多媒体系统中最先启动的就是媒体扫描工作吧。
*/
System.loadLibrary("media_jni");
native_init();
}
//创建媒体扫描器。
public MediaScanner(Context c){
native_setup();//调用JNI层的函数做一些初始化工作。
……
}
在上面的MS中,比较重要的几个调用函数是:
native_init和native_setup,关于它们的故事,在分析JNI层时再做介绍。
MS创建好后,MSS将调用它的scanDirectories开展扫描工作,下面来看这个函数。
2.scanDirectories分析
scanDirectories的代码如下所示:
[—>MediaScanner.java]
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是一个native函数,调用它来对目标文件夹进行扫描,其中MediaFile.sFileExtensions是一个字符串,包含了当前多媒体系统所支持的媒体文件的后缀名,例如.MP3、.MP4等。mClient为MyMediaScannerClient类型,它是从MediaScannerClient类派生的。它的作用我们后面再做分析。
*/
processDirectory(directories[i],MediaFile.sFileExtensions,
mClient);
}
long scan=System.currentTimeMillis();
postscan(directories);//④扫描后处理。
long end=System.currentTimeMillis();
……//统计扫描时间等。
}
上面一共列出了四个关键点(即①~④),下面逐一对其分析。
(1)initialize分析
initialize主要是初始化一些Uri,因为扫描时需把文件的信息插入媒体数据库中,而媒体数据库针对Video、Audio、Image文件等都有对应的表,这些表的地址则由Uri表示。下面是initialize的代码:
[—>MediaScanner.java]
private void initialize(String volumeName){
//得到IMediaProvider对象,通过这个对象可以对媒体数据库进行操作。
mMediaProvider=
mContext.getContentResolver().acquireProvider("media");
//初始化Uri,下面分别介绍一下。
//音频表的地址,也就是数据库中的audio_meta表。
mAudioUri=Audio.Media.getContentUri(volumeName);
//视频表地址,也就是数据库中的video表。
mVideoUri=Video.Media.getContentUri(volumeName);
//图片表地址,也就是数据库中的images表。
mImagesUri=Images.Media.getContentUri(volumeName);
//缩略图表地址,也就是数据库中的thumbs表。
mThumbsUri=Images.Thumbnails.getContentUri(volumeName);
//如果扫描的是外部存储,则支持播放列表、音乐的流派等内容。
if(!volumeName.equals("internal")){
mProcessPlaylists=true;
mProcessGenres=true;
mGenreCache=new HashMap<String,Uri>();
mGenresUri=Genres.getContentUri(volumeName);
mPlaylistsUri=Playlists.getContentUri(volumeName);
if(Process.supportsProcesses()){
//SD卡存储区域一般使用FAT文件系统,所以文件名与大小写无关。
mCaseInsensitivePaths=true;
}
}
}
下面看第二个关键函数prescan。
(2)prescan分析
在媒体扫描过程中,有个令人头疼的问题,来举个例子,这个例子会贯穿在对这个问题的整体分析过程中。例子:假设某次扫描之前SD卡中有100个媒体文件,数据库中有100条关于这些文件的记录,现因某种原因删除了其中的50个媒体文件,那么媒体数据库什么时候会被更新呢?
说明 读者别小瞧这个问题。现在有很多文件管理器支持删除文件和文件夹,它们用起来很方便,却没有对应地更新数据库,这导致了查询数据库时还能得到这些媒体文件的信息,但这个文件实际上已不存在了,而且后面所有和此文件有关的操作都会因此而失败。
其实,MS已经考虑到这一点了,prescan函数的主要作用是在扫描之前把数据库中和文件相关的信息取出并保存起来,这些信息主要是媒体文件的路径,所属表的Uri。就上面这个例子来说,它会从数据库中取出这100个文件的文件信息。
prescan的代码如下所示:
[—>MediaScanner.java]
private void prescan(String filePath)throws RemoteException{
Cursor c=null;
String where=null;
String[]selectionArgs=null;
//mFileCache保存从数据库中获取的文件信息。
if(mFileCache==null){
mFileCache=new HashMap<String,FileCacheEntry>();
}else{
mFileCache.clear();
}
……
try{
//从Audio表中查询其中和音频文件相关的文件信息。
if(filePath!=null){
where=MediaStore.Audio.Media.DATA+"=?";
selectionArgs=new String[]{filePath};
}
//查询数据库的Audio表,获取对应的音频文件信息。
c=mMediaProvider.query(mAudioUri,AUDIO_PROJECTION,where,
selectionArgs,null);
if(c!=null){
try{
while(c.moveToNext()){
long rowId=c.getLong(ID_AUDIO_COLUMN_INDEX);
//音频文件的路径。
String path=c.getString(PATH_AUDIO_COLUMN_INDEX);
long lastModified=
c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX);
if(path.startsWith("/")){
String key=path;
if(mCaseInsensitivePaths){
key=path.toLowerCase();
}
//把文件信息存到mFileCache中。
mFileCache.put(key,
new FileCacheEntry(mAudioUri,rowId,path,
lastModified));
}
}
}finally{
c.close();
c=null;
}
}
……//查询其他表,取出数据中关于视频、图像等文件的信息并存入到mFileCache中。
finally{
if(c!=null){
c.close();
}
}
}
读懂了前面的例子,在阅读prescan函数时可能就比较轻松了。prescan函数执行完后,mFileCache保存了扫描前所有媒体文件的信息,这些信息是从数据库中查询得来的,也就是旧有的信息。
接下来看最后两个关键函数。
(3)processDirectory和postscan分析
processDirectory是一个native函数,其具体功能放到JNI层再分析,这里先简单介绍一下,它在解决上一节那个例子中的问题时所做的工作。答案是:
processDirectory将扫描SD卡,每扫描一个文件,都会设置mFileCache中对应文件的一个叫mSeenInFileSystem的变量为true。这个值表示这个文件目前还存在于SD卡上。这样,待整个SD卡扫描完后,mFileCache的那100个文件中就会有50个文件的mSeenInFileSystem为true,而剩下的另50个文件其初始值则为false。
看到上面的内容,可以知道postscan的作用了吧?就是它把不存在于SD卡的文件信息从数据库中删除,而使数据库得以彻底地更新。来看postscan函数是否是这样处理的:
[—>MediaScanner.java]
private void postscan(String[]directories)throws RemoteException{
Iterator<FileCacheEntry>iterator=mFileCache.values().iterator();
while(iterator.hasNext()){
FileCacheEntry entry=iterator.next();
String path=entry.mPath;
boolean fileMissing=false;
if(!entry.mSeenInFileSystem){
if(inScanDirectory(path,directories)){
fileMissing=true;//这个文件确实丢失了。
}else{
File testFile=new File(path);
if(!testFile.exists()){
fileMissing=true;
}
}
}
//如果文件确实丢失,则需要把数据库中和它相关的信息删除。
if(fileMissing){
MediaFile.MediaFileType mediaFileType=MediaFile.getFileType(path);
int fileType=(mediaFileType==null?0:mediaFileType.fileType);
if(MediaFile.isPlayListFileType(fileType)){
……//处理丢失文件是播放列表的情况。
}else{
/*
由于文件信息中还携带了它在数据库中的相关信息,所以从数据库中删除对应的信息会非常快。
*/
mMediaProvider.delete(ContentUris.withAppendedId(
entry.mTableUri,entry.mRowId),null,null);
iterator.remove();
}
}
}
……//删除缩略图文件等工作。
}
Java层中的四个关键点,至此已介绍了三个,另外一个processDirectory是媒体扫描的关键函数,由于它是一个native函数,所以下面将转战到JNI层来进行分析。