mediascanner详解
OK, 我们现在开始来大概分析一下android framework中MediaScanner部分的流程,若大家发现分析过程中有错误,欢迎拍砖指正。
分析流程之前,我们先给自己定个要用MediaScanner解决的问题,这样我们才会有目标感,才知道我们要干什么。否则,干巴巴的分析流程,一般都会很容易的迷失在各种code的迷雾中。
我们这里要定的目标是: 获取某个 MP3 文件的 artist & album 。
我们可以假定,现在有个媒体播放器,在播放music的时候,需要在UI上显示出来artist& album.
从UI的角度来说,android提供了两套可以获取mediaInfo的方法:
1. 通过MediaScanner Scan文件后,从DB中查询相关信息;
2. 直接通过android提供的MediaMetadataRetriever类来获取相关信息,基本用法如下:
public void getMetaData(String filePath) {
String title, album,artist, composer, genre, mime;
MediaMetadataRetriever retriever = new MediaMetadataRetriever ();
retriever.setDataSource(filePath);
title =retriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_TITLE );
album =retriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_ALBUM );
artist =retriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_ARTIST );
composer =retriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_COMPOSER );
genre =retriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_GENRE );
mime =retriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_MIMETYPE );
Log.i(TAG, “title=” +title + “,album=” + album + “,artist=” + artist + “,composer=” + composer +“,genre=” + genre + “,mime=” + mime);
}
既然我们现在要说的是MediaScanner,那自然我们要介绍的是通过第一种方法获取相关信息的流程了。
OverView
我们这里会分为3个层次来详细解读一下MediaScanner的工作流程:
1. UI APK调用的接口;
2. Java层调用flow;
3. Native层调用flow;
UI调用接口
现在我们的目标是 获取某个 MP3 文件的 artist& album ,因此自然需要分为两步:
1. Scan media file
2. Query data
1. Scan Media File
public void scanfile(path) { String[] paths = new String[1]; // 设置需要扫描的文件路径,这里 path 就是我们所要扫描的 music file 的路径 paths[0] = path; // 调用 MediaScannerConnection 的 scanFile ,进行扫描。 //scanFile 的第三个参数是 mimeTypes, 若为 null ,则会根据文件后缀来判断。 //scanCb 是 MediaScannerConnection.OnScanCompletedListener MediaScannerConnection.scanFile(this.context, paths, null, scanCb); } |
2. Query data
Scan完成之后,就可以在DB中查询相关的audio信息。
private void getInfo() { String[] colume = { "_id", "album_id", "title", "artist", "album", "year", "duration", "_size", "_data" }; StringBuilder localStringBuilder = new StringBuilder(); localStringBuilder.append("_data"); localStringBuilder.append(" LIKE '%"); localStringBuilder.append(musicUtils.convertToQueryString(this.mPath)); localStringBuilder.append("%'"); localStringBuilder.append(" ESCAPE '\\'"); /*colume 是要选择的列, localStringBuilder 是选择的条件, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 是要查询的 URI*/ Cursor localCursor =query(this.context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, colume, localStringBuilder.toString(), null, null); if (localCursor != null); try { //查询到的结果会存储在localCursor中,随后可从localCursor中获取相关信息 if (localCursor.moveToFirst()) { mSongId = localCursor.getLong(0); mAlbumId = localCursor.getLong(1); mTitle = localCursor.getString(2); mSonger = localCursor.getString(3); mAlbum = localCursor.getString(4); mReleaseYear = localCursor.getInt(5); mDuration = localCursor.getLong(6); mSize = localCursor.getLong(7); mDataPath = localCursor.getString(8); } catch (RuntimeException localRuntimeException) { } finally { if (localCursor != null) localCursor.close(); } } |
这里查询的URI为MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,对应的db存储的路径为/data/data/com.android.providers.media/external.db。可用SQLiteSpy查看此DB的内容。
至此从UI的角度来看,已经完成了获取mp3文件info的任务。
Java层MediaScanflow
我们这里主要分析Scanfile的流程,不再对查询信息的流程进行解析。
OK,先看java层的时序图:
MediaScannerClient主要用途是给app提供一个和MediaScannerService交互的桥梁。App可以通过MediaScannerClient给MediaScanenrService传递需要scan的文件。Mediascanner service会对传入的文件进行scan,读取metadata,并将文件添加进mediacontent provider。MediaScannerConnectionClient也为mediascanner service提供了一个返回最近被scan的文件的Uri。
MediaScannerService类其实起到的主要作用就是创建出来MediaScanner,之后调用MediaScanner的ScanFile接口进行工作,最终做具体工作的其实是MediaScanner和MyMediaScannerClient。
下面我们从头追一下code。
Scan动作的发起者是MediaScannerConnection.
Code位于\frameworks\base\media\java\android\media\MediaScannerConnection.java:
public static void scanFile(Context context, String[] paths, String[] mimeTypes, OnScanCompletedListener callback) { ClientProxy client = new ClientProxy(paths, mimeTypes, callback); MediaScannerConnection connection = new MediaScannerConnection(context, client); client.mConnection = connection; //scanFile函数只是调用了一下MediaScannerConnection的connect()函数。 connection.connect(); } |
MediaScannerConnection中有两个名叫scanFile的函数,不过一个是public static的,另外一个则是普通的private函数。Static修饰的scanFile是对UI的接口。通过上面的code可以看出,其实scanFile并未做任何scan的动作,只是new了一个MediaScannerConnection,调用了一下它的connect()函数。
我们再来看一下MediaScannerConnection的connect()函数:
public void connect() { synchronized (this) { if (!mConnected) { Intent intent = new Intent(IMediaScannerService.class.getName()); mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); mConnected = true; } } } |
当Service绑定之后,会触发MediaScannerConnection的onServiceConnected函数,而在这个函数中,又调用了ClientProxy的onMediaScannerConnected函数(ClientProxy继承自MediaScannerConnectionClient,ClientProxy是在调用MediaScannerConnection的scanFile函数时new 出来的,大家可以回过头看看前面的scanFile函数),在onMediaScannerConnected中,会调用ClientProxy的scanNextPath(),scanNextPath()中会调用MediaScannerConnection的scanFile()【注意,这里的scanFile就是我们之前提到过的privatescanFile】。scanFile()中会调用到IMediaScannerService的requestScanFile。在requestScanFile中则会启动MediaScannerService.这部分代码比较简单,就不一一列举出来了。
到现在为止,看看我们有什么了。从scanFile开始一直到requestScanFile,我们最终启动了MediaScannerService。那么下面的任务就交给MediaScannerService来完成了。
MediaScannerService
MediaScannerService.java在packages\providers\MediaProvider\src\com\android\providers\media目录下。
在MediaScannerService启动的时候会new一个Looper,在Looper中又new了一个ServiceHandler。
下来我们看一下MediaScannerService的onStartCommand
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 发送了一个消息,其 handlerMessage 会收到这个消息。 mServiceHandler.sendMessage(msg); // Try again later if we are killed before we can finish scanning. return Service.START_REDELIVER_INTENT; } |
接着看下handlerMessage做了什么事情:
public void handleMessage(Message msg) { Bundle arguments = (Bundle) msg.obj; String filePath = arguments.getString("filepath"); try { // 我们传入了 MP3 文件路径,所以会走这个分支 if (filePath != null) { IBinder binder = arguments.getIBinder("listener"); IMediaScannerListener listener = (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder)); Uri uri = null; try { // 这里才开始了 scanFile 的流程。此时传入的 mimetype 为 null. uri = scanFile(filePath, arguments.getString("mimetype")); } catch (Exception e) { Log.e(TAG, "Exception scanning file", e); } if (listener != null) { //scan 完成之后,通知 listener listener.scanCompleted(filePath, uri); } } else { …… } } catch (Exception e) { Log.e(TAG, "Exception in handleMessage", e); } stopSelf(msg.arg1); } |
我们来看一下MediaScannerService的scanFile都干了什么。
private Uri scanFile(String path, String mimeType) { String volumeName = MediaProvider.EXTERNAL_VOLUME; // 打开数据库。这个 MediaProvider.EXTERNAL_VOLUME 为 external 。 openDatabase(volumeName); // 创建 MediaScanner ,并设置其 language&country MediaScanner scanner = createMediaScanner(); try { // make sure the file path is in canonical form String canonicalPath = new File(path).getCanonicalPath(); // 扫描文件 return scanner.scanSingleFile(canonicalPath, volumeName, mimeType); } catch (Exception e) { Log.e(TAG, "bad path " + path + " in scanFile()", e); return null; } } |
MediaScanner被创建时,先会设置language & country,之后会调用到jni层的接口native_init()& native_setup(),这里最主要的其实是在native_setup中创建native层的MediaScanner。这里我们创建的其实是一个StagefrightMediaScanner。这个我们后面再说。
MediaScanner的scanSingleFile会调用到MyMediaScannerClient(位置也在MediaScaner.java中)doScanFile。
MyMediaScannerClient
几乎所有的事情都是在MyMediaScannerClient中完成的。
public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { Uri result = null; …… boolean isaudio = MediaFile.isAudioFileType(mFileType); boolean isvideo = MediaFile.isVideoFileType(mFileType); boolean isimage = MediaFile.isImageFileType(mFileType); …… // we only extract metadata for audio and video files if (isaudio || isvideo) { /* processFile 是一个 native 方法,最终实现的地方在 native 层的 StagefrightMediaScanner 中 , 获取 media file 的metadata ,都是在这个函数中完成的 */ processFile(path, mimeType, this); } if (isimage) { processImageFile(path); }
/* 获取到的 Metadata 存入 db ,是由 endFile 完成的。 */ result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
…… return result; } |
获取到的metaData会被暂时保存在MyMediaScannerClient的成员变量中,之后通过endFile,将这些成员变量的值通过MediaProvider存入DB中。
这里还要提到一个函数:MyMediaScannerClient的handleStringTag():
public void handleStringTag(String name, String value) { if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { // Don't trim() here, to preserve the special \001 character // used to force sorting. The media provider will trim() before // inserting the title in to the database. mTitle = value; } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { mArtist = value.trim(); } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") || name.equalsIgnoreCase("band") || name.startsWith("band;")) { mAlbumArtist = value.trim(); } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { mAlbum = value.trim(); } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { mComposer = value.trim(); } else if (mProcessGenres && (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { mGenre = getGenreName(value); } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { mYear = parseSubstring(value, 0, 0); } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { // track number might be of the form "2/12" // we just read the number before the slash int num = parseSubstring(value, 0, 0); mTrack = (mTrack / 1000) * 1000 + num; } else if (name.equalsIgnoreCase("discnumber") || name.equals("set") || name.startsWith("set;")) { // set number might be of the form "1/3" // we just read the number before the slash 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(); } else if (name.equalsIgnoreCase("compilation")) { mCompilation = parseSubstring(value, 0, 0); } else if (name.equalsIgnoreCase("isdrm")) { mIsDrm = (parseSubstring(value, 0, 0) == 1); } else if (name.equalsIgnoreCase("width")) { mWidth = parseSubstring(value, 0, 0); } else if (name.equalsIgnoreCase("height")) { mHeight = parseSubstring(value, 0, 0); } else { //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")"); } } |
此函数最终调用的地方,其实是通过JNI被native层的MediaScannerClient的endfile调用。用来将通过MediaMetadataRetriever获取到的metadata,送给java层MyMediaScannerClient的成员变量中。
至此,java层MediaScan的流程就算全部结束了。
Native 层MediaScanner flow
废话不多说,先上时序图:
在上一节我们提到过在MyMediaScannerClient的doScanFile中,会调用到一个native层的函数processFile.
这个processFile实现的地方在StagefrightMediaScanner类中。位置在:frameworks\av\media\libstagefright\StagefrightMediaScanner.cpp中。
MediaScanResult StagefrightMediaScanner::processFile( const char *path, const char *mimeType, MediaScannerClient &client) { ALOGV("processFile '%s'.", path); // 设置 mLocaleEncoding ,此变量用以在 endfile 中判断编码格式。 client.setLocale(locale()); // beginFile 很简单,就是 new 了两个 buffer ,用以存放 metadata 的名称和值 client.beginFile(); /* processFileInternal 中通过 MediaMetadataRetriever ,来获取到 metadata ,并将其一一对应的放入在 beginFile 中 new 出来的两个buffer(mNames&mValues) 之中。 */ MediaScanResult result = processFileInternal(path, mimeType, client); /* endFile 中将得到的 metadata ,先转码成 UTF8 ,之后通过 JNI 调用到 java 层 handleStringTag 函数,将 metadata 相关信息保存到MyMediaScannerClient 的成员变量中,前面已经有提到过了 */ client.endFile(); return result; } |
我们来看一下processFileInternal:
MediaScanResult StagefrightMediaScanner::processFileInternal( const char *path, const char *mimeType, MediaScannerClient &client) { …… sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever); int fd = open(path, O_RDONLY | O_LARGEFILE); status_t status; // 1. 通过 MediaMetadataRetriever , setDataSource if (fd < 0) { // couldn't open it locally, maybe the media server can? status = mRetriever->setDataSource(path); } else { status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL); close(fd); } if (status) { return MEDIA_SCAN_RESULT_ERROR; } const char *value; if ((value = mRetriever->extractMetadata( METADATA_KEY_MIMETYPE)) != NULL) { status = client.setMimeType(value); if (status) { return MEDIA_SCAN_RESULT_ERROR; } } struct KeyMap { const char *tag; int key; }; static const KeyMap kKeyMap[] = { { "tracknumber", METADATA_KEY_CD_TRACK_NUMBER }, { "discnumber", METADATA_KEY_DISC_NUMBER }, { "album", METADATA_KEY_ALBUM }, { "artist", METADATA_KEY_ARTIST }, { "albumartist", METADATA_KEY_ALBUMARTIST }, { "composer", METADATA_KEY_COMPOSER }, { "genre", METADATA_KEY_GENRE }, { "title", METADATA_KEY_TITLE }, { "year", METADATA_KEY_YEAR }, { "duration", METADATA_KEY_DURATION }, { "writer", METADATA_KEY_WRITER }, { "compilation", METADATA_KEY_COMPILATION }, { "isdrm", METADATA_KEY_IS_DRM }, { "width", METADATA_KEY_VIDEO_WIDTH }, { "height", METADATA_KEY_VIDEO_HEIGHT }, }; static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]); for (size_t i = 0; i < kNumEntries; ++i) { const char *value; // 2. 通过 extractMetadata 获取相关 key 的值 if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) { // 3. 存值。将获取到的值添加进在 begin 中 new 出来的 buffer 中 status = client.addStringTag(kKeyMap[i].tag, value); if (status != OK) { return MEDIA_SCAN_RESULT_ERROR; } } } return MEDIA_SCAN_RESULT_OK; } |
至此,metadata就已经保存在了native层MediaScannerClient的mNames和mValues中。
接下来我们看一下MediaScannerClient的endFile。
此endFile的主要作用有两个:
1. 将mValues中的值转成UTF8格式;
2. 将转码后的值,通过JNI保存到java层的MyMediaScannerClient中
void MediaScannerClient::endFile() { if (mLocaleEncoding != kEncodingNone) { int size = mNames->size(); uint32_t encoding = kEncodingAll; // 获取 mValues 中各个值可能的编码格式 for (int i = 0; i < mNames->size(); i++) { encoding &= possibleEncodings(mValues->getEntry(i)); } // if the locale encoding matches, then assume we have a native encoding. // 转码成 UTF8 if (encoding & mLocaleEncoding) convertValues(mLocaleEncoding); // finally, push all name/value pairs to the client for (int i = 0; i < mNames->size(); i++) { // 将转码后的 value 值存放至 java 层的 MyMediaScannerClient 中。 /* handleStringTag 函数是通过 JNI 调用到 java 层的。有兴趣追一下此函数的话,可以看一下 android_media_MediaScanner.cpp文件。此函数最终实现的地方在 MediaScaner.java 的 MyMediaScannerClient 类中,前面已经将此函数 code 列出来了,此处不再做过多解释 */ status_t status = handleStringTag(mNames->getEntry(i), mValues->getEntry(i)); if (status) { break; } } } // else addStringTag() has done all the work so we have nothing to do delete mNames; delete mValues; mNames = NULL; mValues = NULL; } |
OK,到这里,MediaScanner的所有流程就已经走完了。
我们再过来回过头整理一下scanfile的几个重要地方:
1. Native_setup(): 在此函数中要创建出来真正干活的MediaScanner出来。
2. setLocal():要设置正确的language
3. processFile:Scan file
4. native层的endFile:要将scan到的value值进行正确的转码,否则存入db中的数据就可能是乱码了。
5. java层的endFile:此处是真正的将scan到的metadata存入了db中。