本章主要內容
·? 介紹多媒體系統(tǒng)中媒體文件掃描的工作原理。
本章涉及的源代碼文件名及位置
下面是本章分析的源碼文件名及其位置。
·? MediaProvider.java
packages/providers/MediaProvider/MediaProvider.java
·? MediaScannerReceiver.java
packages/providers/MediaProvider/MediaScannerReceiver.java
·? MediaScannerService.java
packages/providers/MediaProvider/MediaScannerService.java
·? MediaScanner.java
framework/base/media/java/com/android/media/MediaScanner.java
·? MediaThumbRequest.java
packages/providers/MediaProvider/MediaThumbRequest.java
·? android_media_MediaScanner.cpp
framework/base/media/jni/android_media_MediaScanner.cpp
·? MediaScanner.cpp
framework/base/media/libmedia/MediaScanner.cpp
·? PVMediasScanner.cpp
external/opencore/android/PVMediasScanner.cpp
多媒體系統(tǒng),是Android平臺中非常龐大的一個系統(tǒng)。不過由于篇幅所限,本章只介紹多媒體系統(tǒng)中的重要一員MediaScanner。MediaScanner有什么用呢?可能有些讀者還不是很清楚。MediaScanner和媒體文件掃描有關,例如,在Music應用程序中見到的歌曲專輯名、歌曲時長等信息,都是通過它掃描對應的歌曲而得到的。另外,通過MediaStore接口查詢媒體數據庫,從而得到系統(tǒng)中所有媒體文件的相關信息也和MediaScanner有關,因為數據庫的內容就是由MediaScanner添加的。所以MediaScanner是多媒體系統(tǒng)中很重要的一部分。
伴隨著Android的成長,多媒體系統(tǒng)也發(fā)生了非常大的變化。這對開發(fā)者來說,一個非常好的消息,就是從Android 2.3開始那個令人極度郁悶的OpenCore,終于有被干掉的可能了。從此,也迎來了Stagefright時代。但Android 2.2在很長一段時間內還會存在,所以希望以后能有機會深入地剖析這個OpenCore。
下面,就來分析媒體文件掃描的工作原理。
多媒體系統(tǒng)的媒體掃描功能,是通過一個APK應用程序提供的,它位于package/providers/MediaProvider目錄下。通過分析APK的Android.mk文件可知,該APK運行時指定了一個進程名,如下所示:
application android:process=android.process.media
原來,通過ps命令經常看到的進程就是它?。×硗?,從這個APK程序所處的package\providers目錄也可知道,它還是一個ContentProvider。事實上從Android應用程序的四大組件來看,它使用了其中的三個組件:
·? MediaScannerService(從Service派生)模塊負責掃描媒體文件,然后將掃描得到的信息插入到媒體數據庫中。
·? MediaProvider(從ContentProvider派生)模塊負責處理針對這些媒體文件的數據庫操作請求,例如查詢、刪除、更新等。
·? MediaScannerReceiver(從BroadcastReceiver派生)模塊負責接收外界發(fā)來的掃描請求。也就是MS對外提供的接口。
除了支持通過廣播發(fā)送掃描請求外,MediaScannerService也支持利用Binder機制跨進程調用掃描函數。這部分內容,將在本章的拓展部分中介紹。
本章僅關注android.process.media進程中的MediaScannerService和MediaScannerReceiver模塊,為書寫方便起見,將這兩個模塊簡稱為MSS和MSR,另外將MediaScanner簡稱MS,將MediaProvider簡稱MP。
下面,開始分析android.process.media中和媒體文件掃描相關的工作流程。
MSR模塊的核心類MediaScannerReceiver從BroadcastReceiver派生,它是專門用來接收廣播的,那么它感興趣的廣播有哪幾種呢?其代碼如下所示:
[-->MediaScannerReceiver.java]
public class MediaScannerReceiver extendsBroadcastReceiver
{
private final static String TAG ="MediaScannerReceiver";
???@Override? //MSR在onReceive函數中處理廣播
??? publicvoid onReceive(Context context, Intent intent) {
???????String action = intent.getAction();
???????Uri uri = intent.getData();
??????? //一般手機外部存儲的路徑是/mnt/sdcard
???????String externalStoragePath =
????????????????????? Environment.getExternalStorageDirectory().getPath();
????????
??????? //為了簡化書寫,所有Intent的ACTION_XXX_YYY字串都會簡寫為XXX_YYY。
??????? if(action.equals(Intent.ACTION_BOOT_COMPLETED)) {
????????????//如果收到BOOT_COMPLETED廣播,則啟動內部存儲區(qū)的掃描工作,內部存儲區(qū)
???????????//實際上掃描的是/system/media目錄,這里存儲了系統(tǒng)自帶的鈴聲等媒體文件。
????????????scan(context, MediaProvider.INTERNAL_VOLUME);
??????? }else {
???????????if (uri.getScheme().equals("file")) {
????????????????String path = uri.getPath();
?????????????/*
注意下面這個判斷,如果收到MEDIA_MOUNTED消息,并且外部存儲掛載的路徑
???????????????和“/mnt/sdcard“一樣,則啟動外部存儲也就是SD卡的掃描工作
???????????????*/
???????????????if (action.equals(Intent.ACTION_MEDIA_MOUNTED) &&
???????????????????????externalStoragePath.equals(path)) {
??????????????????? scan(context,MediaProvider.EXTERNAL_VOLUME);
????? ??????????} else if(action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
&& path != null
&& path.startsWith(externalStoragePath +"/")) {
??????????????????? /*
外部應用可以發(fā)送MEDIA_SCANNER_SCAN_FILE廣播讓MSR啟動單個文件
的掃描工作。注意這個文件必須位于SD卡上。
*/
??????????????????? scanFile(context, path);
???????????????}
???????????}
??????? }
??? }
從上面代碼中發(fā)現MSR接收的三種請求,也就是說,它對外提供三個接口函數:
·? 接收BOOT_COMPLETED請求,這樣MSR會啟動內部存儲區(qū)的掃描工作,注意這個內部存儲區(qū)實際上是/system/media這個目錄。
·? 接收MEDIA_MOUNTED請求,并且該請求攜帶的外部存儲掛載點路徑必須是/mnt/sdcard,通過這種方式MSR會啟動外部存儲區(qū)也就是SD卡的掃描工作,掃描目標是文件夾/mnt/sdcard。
·? 接收MEDIA_SCANNER_SCAN_FILE請求,并且該請求必須是SD卡上的一個文件,即文件路徑須以/mnt/sdcard開頭,這樣,MSR會啟動針對這個文件的掃描工作。
讀者是否注意到,MSR和跨Binder調用的接口(在本章拓展內容中將介紹)都不支持對目錄的掃描(除了SD卡的根目錄外)。實現這個功能并不復雜,有興趣的讀者可自行完成該功能,如果方便,請將自己實現的代碼與大家共享。
大部分的媒體文件都已放在SD卡上了,那么來看收到MEDIA_MOUNTED請求后MSR的工作。還記得第9章中對Vold的分析嗎?這個MEDIA_MOUNTED廣播就是由MountService發(fā)送的,一旦有SD卡被掛載,MSR就會被這個廣播喚醒,接著SD卡的媒體文件就會被掃描了。真是一氣呵成!
SD卡根目錄掃描時調用的函數scan的代碼如下:
[-->MediaScannerReceiver.java]
private void scan(Context context, Stringvolume) {
???????//volume的值為/mnt/sdcard
??????? Bundleargs = new Bundle();
???????args.putString("volume", volume);
??????? //啟動MSS。
???????context.startService(
???????????????new Intent(context, MediaScannerService.class).putExtras(args));
??? }?
scan將啟動MSS服務。下面來看MSS的工作。
MSS從Service派生,并且實現了Runnable接口。下面是它的定義:
[-->MediaScannerService.java]
MediaScannerService extends Service implementsRunnable
//MSS實現了Runnable接口,這表明它可能會創(chuàng)建工作線程
根據SDK中對Service生命周期的描述,Service剛創(chuàng)建時會調用onCreate函數,接著就是onStartCommand函數,之后外界每調用一次startService都會觸發(fā)onStartCommand函數。接下來去了解一下onCreate函數及onStartCommand函數。
onCreate函數的代碼如下所示:(這是MSS被系統(tǒng)創(chuàng)建時調用的,在它的整個生命周期內僅調用一次。)
[-->MediaScannerService.java]
public void onCreate(){
?? //獲得電源鎖,防止在掃描過程中休眠
??PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
??mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
//掃描工作是一個漫長的工程,所以這里單獨創(chuàng)建一個工作線程,線程函數就是
//MSS實現的Run函數
??? Threadthr = new Thread(null, this, "MediaScannerService");
???thr.start();
|
onCreate將創(chuàng)建一個工作線程:
?publicvoid run()
??? {
????? ??/*
設置本線程的優(yōu)先級,這個函數的調用有很重要的作用,因為媒體掃描可能會耗費很長
????????? 時間,如果不調低優(yōu)先級的話,CPU將一直被MSS占用,導致用戶感覺系統(tǒng)變得很慢
????????*/
???????Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
????????????????????????????????Process.THREAD_PRIORITY_LESS_FAVORABLE);
???? ???Looper.prepare();
?
???????mServiceLooper = Looper.myLooper();
??????? /*
創(chuàng)建一個Handler,以后發(fā)送給這個Handler的消息都會由工作線程處理。
這一部分內容,已在第5章Handler中分析過了。
*/
???????mServiceHandler = new ServiceHandler();
?
???????Looper.loop();
}
onCreate后,MSS將會創(chuàng)建一個帶消息處理機制的工作線程,那么消息是怎么投遞到這個線程中的呢?
還記得MSR的scan函數嗎?如下所示:
[-->MediaScannerReceiver.java::scan函數]
context.startService(
???????????????new Intent(context, MediaScannerService.class).putExtras(args));
其中Intent包含了目錄掃描請求的目標/mnt/sdcard。這個Intent發(fā)出后,最終由MSS的onStartCommand收到并處理,其代碼如下所示:
[-->MediaScannerService.java]
@Override
?publicint onStartCommand(Intent intent, int flags, int startId)
?{
???? /*
等待mServiceHandler被創(chuàng)建。耕耘這段代碼的碼農難道不知道
HandlerThread這個類嗎?不熟悉它的讀者請再閱讀第5章的5.4節(jié)。
???? */
???? while(mServiceHandler == null) {
???????????synchronized (this) {
???????????????try {
??????????????????? wait(100);
???????????????} catch (InterruptedException e) {
???????????????}
???????????}
??????? }
?????? ......
???????Message msg = mServiceHandler.obtainMessage();
???????msg.arg1 = startId;
???????msg.obj = intent.getExtras();
//往這個Handler投遞消息,最終由工作線程處理。
???????mServiceHandler.sendMessage(msg);
?? ????? ......
}
onStartCommand將把掃描請求信息投遞到工作線程去處理。
掃描請求由ServiceHandler的handleMessage函數處理,其代碼如下所示:
[-->MediaScannerService.java]
private final class ServiceHandler extendsHandler
{
???? @Override
????public void handleMessage(Message msg)
??????? {
???????????Bundle arguments = (Bundle) msg.obj;
???????????String filePath = arguments.getString("filepath");
???????????
???????????try {
?????????????????......
???????????????} else {
??????????????????? String volume =arguments.getString("volume");
??????????????????? String[] directories =null;
??????????????????? if(MediaProvider.INTERNAL_VOLUME.equals(volume)) {
???????????????????? //如果是掃描內部存儲的話,實際上掃描的目錄是/system/media??
????????????????????? directories = newString[] {
???????????????????????????????Environment.getRootDirectory() + "/media",
??????????????????????? };
??????????????????? }
??????????????????? else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)){
????????????????????? //掃描外部存儲,設置掃描目標位/mnt/sdcard?
???????????????? ??????directories = new String[]{
?Environment.getExternalStorageDirectory().getPath()};
??????????????????? }
??????????????????? if (directories != null) {
/*
調用scan函數開展文件夾掃描工作,可以一次為這個函數設置多個目標文件夾,
不過這里只有/mnt/sdcard一個目錄
*/
??????????????????? scan(directories, volume);
???????????????????? ......
??????? ????????????stopSelf(msg.arg1);
??????? ???????}
}
下面,單獨用一小節(jié)來分析這個scan函數。
scan的代碼如下所示:
[-->MediaScannerService.java]
private void scan(String[] directories, StringvolumeName) {
??? mWakeLock.acquire();
?
? ContentValuesvalues = new ContentValues();
??values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
?? //MSS通過insert特殊Uri讓MediaProvider做一些準備工作
?? UriscanUri = getContentResolver().insert(
MediaStore.getMediaScannerUri(), values);
?
?? Uri uri= Uri.parse("file://" + directories[0]);
?? //向系統(tǒng)發(fā)送一個MEDIA_SCANNER_STARTED廣播。
??sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
???????try {
??????????//openDatabase函數也是通過insert特殊Uri讓MediaProvider打開數據庫
???????????if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
????????????????openDatabase(volumeName);???
???????????}
??????? //創(chuàng)建媒體掃描器,并調用它的scanDirectories函數掃描目標文件夾
???????MediaScanner scanner = createMediaScanner();
? ????????scanner.scanDirectories(directories,volumeName);
??????? }
?????????......
//通過特殊Uri讓MediaProvider做一些清理工作
???????getContentResolver().delete(scanUri, null, null);
//向系統(tǒng)發(fā)送MEDIA_SCANNER_FINISHED廣播
???????sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
?
???????mWakeLock.release();
}
上面代碼中,比較復雜的是MSS和MP的交互。除了后文中即將看到的正常數據庫操作外,MSS還經常會使用一些特殊的Uri來做數據庫操作,而MP針對這些Uri會做一些特殊處理,例如打開數據庫文件等。
本章不擬對MediaProvider做過多的討論,這部分知識對那些讀完前9章的讀者來說,應該不是什么難題。如有可能,請讀者自己整理MediaProvider的工作流程,然后提供給大家一起學習,探討。
看MSS中創(chuàng)建媒體掃描器的函數createMediaScanner:
private MediaScanner createMediaScanner() {
//下面這個MediaScanner是在framework/base/中,稍后再分析
???????MediaScanner scanner = new MediaScanner(this);
//獲取當前系統(tǒng)使用的區(qū)域信息,掃描的時候將把媒體文件中的信息轉換成當前系統(tǒng)使用的語言
???????Locale locale = getResources().getConfiguration().locale;
??????? if(locale != null) {
???????????String language = locale.getLanguage();
???????????String country = locale.getCountry();
???????????String localeString = null;
???????????if (language != null) {
???????????????if (country != null) {
//為掃描器設置當前系統(tǒng)使用的國家和語言。
??????????????????? scanner.setLocale(language+ "_" + country);
???????????????} else {
???????????????????scanner.setLocale(language);
???????????????}
???????????}???
??????? }
???????return scanner;
}
MSS模塊掃描的工作就到此為止了,下面輪到主角MediaScanner登場了。在介紹主角之前,不妨先總結一下本節(jié)的內容。
媒體掃描工作流程涉及MSR和MSS的交互,來總結一下相關的流程:
·? MSR接收外部發(fā)來的掃描請求,并通過startService方式啟動MSS處理。
·? MSS的主線程接收MSR所收到的請求,然后投遞給工作線程去處理。
·? 工作線程做一些前期處理工作后(例如向系統(tǒng)廣播掃描開始的消息),就創(chuàng)建媒體掃描器MediaScanner來處理掃描目標。
·? MS掃描完成后,工作線程再做一些后期處理,然后向系統(tǒng)發(fā)送掃描完畢的廣播。
?
現在分析媒體掃描器MediaScanner的工作原理,它將縱跨Java層、JNI層,以及Native層。先看它在Java層中的內容。
認識一下MediaScanner,它的代碼如下所示:
[-->MediaScanner.java]
public class MediaScanner
{
static {
?????? /*
加載libmedia_jni.so,這么重要的庫竟然放在如此不起眼的MediaScanner類中加載。
個人覺得,可能是因為開機后多媒體系統(tǒng)中最先啟動的就是媒體掃描工作吧。
?????? */
?????? System.loadLibrary("media_jni");
???????native_init();
}
//創(chuàng)建媒體掃描器
public MediaScanner(Context c) {
???????native_setup();//調用JNI層的函數做一些初始化工作
???????......
}
在上面的MS中,比較重要的幾個調用函數是:
·? native_init和native_setup,關于它們的故事,在分析JNI層時再做介紹。
MS創(chuàng)建好后,MSS將調用它的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是一個字符串,包含了當前多媒體系統(tǒng)所支持的
媒體文件的后綴名,例如.MP3、.MP4等。mClient為MyMediaScannerClient類型,
它是從MediaScannerClient類派生的。它的作用我們后面再做分析。
?
*/
???????????processDirectory(directories[i], MediaFile.sFileExtensions,
?mClient);
????????? ?}
???????????long scan = System.currentTimeMillis();
???????????postscan(directories);//④掃描后處理
???????????long end = System.currentTimeMillis();
????????? ......//統(tǒng)計掃描時間等
?}
上面一共列出了四個關鍵點,下面逐一對其分析。
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卡存儲區(qū)域一般使用FAT文件系統(tǒng),所以文件名與大小寫無關
???????????????mCaseInsensitivePaths = true;
???????????}
??????? }
}
下面看第二個關鍵函數prescan。
在媒體掃描過程中,有個令人頭疼的問題,來舉個例子,這個例子會貫穿在對這個問題整體分析的過程中。例子:假設某次掃描之前SD卡中有100個媒體文件,數據庫中有100條關于這些文件的記錄,現因某種原因刪除了其中的50個媒體文件,那么媒體數據庫什么時候會被更新呢?
讀者別小瞧這個問題?,F在有很多文件管理器支持刪除文件和文件夾,它們用起來很方便,卻沒有對應地更新數據庫,這導致了查詢數據庫時還能得到這些媒體文件信息,但這個文件實際上已不存在了,而且后面所有和此文件有關的操作都會因此而失敗。
其實,MS已經考慮到這一點了,prescan函數的主要作用是在掃描之前把數據庫中和文件相關的信息取出并保存起來,這些信息主要是媒體文件的路徑,所屬表的Uri。就上面這個例子來說,它會從數據庫中取出100個文件的文件信息。
prescan的代碼如下所示:
[-->MediaScanner.java]
?privatevoid 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函數執(zhí)行完后,mFileCache保存了掃描前所有媒體文件的信息,這些信息是從數據庫中查詢得來的,也就是舊有的信息。
接下來,看最后兩個關鍵函數。
processDirectory是一個native函數,其具體功能放到JNI層再分析,這里先簡單介紹,它在解決上一節(jié)那個例子中提出的問題時,所做的工作。答案是:
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 = newFile(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函數,所以下面將轉戰(zhàn)到JNI層來進行分析。
?
現在分析MS的JNI層。在Java層中,有三個函數涉及JNI層,它們是:
·? native_init,這個函數由MediaScanner類的static塊調用。
·? native_setup,這個函數由MediaScanner的構造函數調用。
·? processDirectory,這個函數由MS掃描文件夾時調用。
分別來分析它們。
下面是native_init對應的JNI函數,其代碼如下所示:
[-->android_media_MediaScanner.cpp]
static void
android_media_MediaScanner_native_init(JNIEnv*env)
{
????jclass clazz;
clazz =env->FindClass("android/media/MediaScanner");
//取得Java中MS類的mNativeContext信息。待會創(chuàng)建Native對象的指針會保存
//到JavaMS對象的mNativeContext變量中。
??? ?fields.context = env->GetFieldID(clazz,"mNativeContext", "I");
?? ??......
}
native_init函數沒什么新意,這種把Native對象的指針保存到Java對象中的做法,已經屢見不鮮。下面看第二個函數native_setup。
native_setup對應的JNI函數如下所示:
[-->android_media_MediaScanner.cpp]
android_media_MediaScanner_native_setup(JNIEnv*env, jobject thiz)
{
//創(chuàng)建Native層的MediaScanner對象
MediaScanner*mp = createMediaScanner();
......
//把mp的指針保存到Java MS對象的mNativeContext中去
env->SetIntField(thiz,fields.context, (int)mp);
}
//下面的createMediaScanner這個函數將創(chuàng)建一個Native的MS對象
static MediaScanner *createMediaScanner() {
#if BUILD_WITH_FULL_STAGEFRIGHT
??? charvalue[PROPERTY_VALUE_MAX];
??? if(property_get("media.stagefright.enable-scan", value, NULL)
???????&& (!strcmp(value, "1") || !strcasecmp(value,"true"))) {
???????return new StagefrightMediaScanner; //使用Stagefright的MS
??? }
#endif
#ifndef NO_OPENCORE
??? returnnew PVMediaScanner(); //使用Opencore的MS,我們會分析這個
#endif
??? returnNULL;
}
native_setup函數將創(chuàng)建一個Native層的MS對象,不過可惜的是,它使用的還是Opencore提供的PVMediaScanner,所以后面還不可避免地會和Opencore“正面交鋒”。
看processDirectories函數,它對應的JNI函數代碼如下所示:
[-->android_media_MediaScanner.cpp]
android_media_MediaScanner_processDirectory(JNIEnv*env, jobject thiz,
jstring path, jstring extensions, jobject client)
{
?? /*
注意上面?zhèn)魅氲膮?path為目標文件夾的路徑,extensions為MS支持的媒體文件后綴名集合,
client為Java中的MediaScannerClient對象。
*/
?
MediaScanner *mp = (MediaScanner*)env->GetIntField(thiz, fields.context);
?
??? constchar *pathStr = env->GetStringUTFChars(path, NULL);
constchar *extensionsStr = env->GetStringUTFChars(extensions, NULL);
......
??
?? //構造一個Native層的MyMediaScannerClient,并使用Java那個Client對象做參數。
?? //這個Native層的Client簡稱為MyMSC。
MyMediaScannerClient myClient(env, client);
//調用Native的MS掃描文件夾,并且把Native的MyMSC傳進去。
mp->processDirectory(pathStr,extensionsStr, myClient,
ExceptionCheck, env);
??? ......
???env->ReleaseStringUTFChars(path, pathStr);
env->ReleaseStringUTFChars(extensions,extensionsStr);
......
}
processDirectory函數本身倒不難,但又冒出了幾個我們之前沒有接觸過的類型,下面先來認識一下它們。
圖10-1展示了MediaScanner所涉及的相關類和它們之間的關系:
http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter10/image001.png" alt="image" />
圖10-1? MS相關類示意圖
為了便于理解,便將Java和Native層的對象都畫于圖中。從上圖可知:
·? Java MS對象通過mNativeContext指向Native的MS對象。
·? Native的MyMSC對象通過mClient保存Java層的MyMSC對象。
·? Native的MS對象調用processDirectory函數的時候會使用Native的MyMSC對象。
·? 另外,圖中Native MS類的processFile是一個虛函數,需要派生類來實現。
其中比較費解的是MyMSC對象。它們有什么用呢?這個問題真是一言難盡。下面通過processDirectory來探尋其中原因,這回得進入PVMediaScanner的領地了。
來看PVMediaScanner(以后簡稱為PVMS,它就是Native層的MS)的processDirectory函數。這個函數是由它的基類MS實現的。注意,源碼中有兩個MediaScanner.cpp,它們的位置分別是:
·? framework/base/media/libmedia
·? external/opencore/android/
看libmedia下的那個MediaScanner.cpp,其中processDirectory函數的代碼如下所示:
[-->MediaScanner.cpp]
status_t MediaScanner::processDirectory(constchar *path,
const char *extensions, MediaScannerClient&client,
??????? ??????????????????ExceptionCheckexceptionCheck, void *exceptionEnv) {
??? ?
???......//做一些準備工作
???client.setLocale(locale()); //給Native的MyMSC設置locale信息
?? //調用doProcessDirectory函數掃描文件夾
status_tresult =? doProcessDirectory(pathBuffer,pathRemaining,
extensions, client,exceptionCheck, exceptionEnv);
?
???free(pathBuffer);
?
??? returnresult;
}
//下面直接看這個doProcessDirectory函數
status_t MediaScanner::doProcessDirectory(char*path, int pathRemaining,
const char *extensions,MediaScannerClient&client,
ExceptionCheck exceptionCheck,void *exceptionEnv) {
???
?? ......//忽略.nomedia文件夾
?
??? DIR*dir = opendir(path);
??? ......
?
while((entry = readdir(dir))) {
??? //枚舉目錄中的文件和子文件夾信息
???????const char* name = entry->d_name;
??????? ......
???????int type = entry->d_type;
??????? ?......
??????? if(type == DT_REG || type == DT_DIR) {
???????????int nameLength = strlen(name);
???????????bool isDirectory = (type == DT_DIR);
??????????......
???????????strcpy(fileSpot, name);
???????????if (isDirectory) {
???????????????......
????????????????//如果是子文件夾,則遞歸調用doProcessDirectory
???????????????int err = doProcessDirectory(path, pathRemaining - nameLength - 1,
extensions, client, exceptionCheck, exceptionEnv);
???????????????......
???????????} else if (fileMatchesExtension(path, extensions)) {
???????????????//如果該文件是MS支持的類型(根據文件的后綴名來判斷)
????????? ??????struct stat statbuf;
???????????????stat(path, &statbuf); //取出文件的修改時間和文件的大小
???????????????if (statbuf.st_size > 0) {
??????????????????? //如果該文件大小非零,則調用MyMSC的scanFile函數?。??
??????????????????? client.scanFile(path,statbuf.st_mtime, statbuf.st_size);
???????????????}
???????????????if (exceptionCheck && exceptionCheck(exceptionEnv)) gotofailure;
???????????}
??????? }
??? }
......
}
假設正在掃描的媒體文件的類型是屬于MS支持的,那么,上面代碼中最不可思議的是,它竟然調用了MSC的scanFile來處理這個文件,也就是說,MediaScanner調用MediaScannerClient的scanFile函數。這是為什么呢?還是來看看這個MSC的scanFile吧。
其實,在調用processDirectory時,所傳入的MSC對象的真實類型是MyMediaScannerClient,下面來看它的scanFile函數,代碼如下所示:
[-->android_media_MediaScanner.cpp]
virtual bool scanFile(const char* path, longlong lastModified,
long long fileSize)
??? {
???????jstring pathStr;
??????? if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
???????//mClient是Java層的那個MyMSC對象,這里調用它的scanFile函數
???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,
lastModified, fileSize);
?
???????mEnv->DeleteLocalRef(pathStr);
???????return (!mEnv->ExceptionCheck());
}
太沒有天理了!Native的MyMSCscanFile主要的工作就是調用Java層MyMSC的scanFile函數。這又是為什么呢?
現在只能來看Java層的這個MyMSC對象了,它的scanFile代碼如下所示:
[-->MediaScanner.java]
public void scanFile(String path, longlastModified, long fileSize) {
???????????......
???????????//調用doScanFile函數
???????????doScanFile(path, null, lastModified, fileSize, false);
??????? }
//直接來看doScanFile函數
?publicUri doScanFile(String path, String mimeType, long lastModified,
long fileSize, boolean scanAlways) {
? /*
上面參數中的scanAlways用于控制是否強制掃描,有時候一些文件在前后兩次掃描過程中沒有
發(fā)生變化,這時候MS可以不處理這些文件。如果scanAlways為true,則這些沒有變化
的文件也要掃描。
? */
?? Uriresult = null;
long t1 = System.currentTimeMillis();
try{
???? /*
????? beginFile的主要工作,就是將保存在mFileCache中的對應文件信息的
mSeenInFileSystem設為true。如果這個文件之前沒有在mFileCache中保存,
則會創(chuàng)建一個新項添加到mFileCache中。另外它還會根據傳入的lastModified值
做一些處理,以判斷這個文件是否在前后兩次掃描的這個時間段內被修改,如果有修改,則
需要重新掃描
*/
????????? FileCacheEntryentry = beginFile(path, mimeType,
lastModified, fileSize);
? ???????if(entry != null && (entry.mLastModifiedChanged || scanAlways)) {
?????????????String lowpath = path.toLowerCase();
?????????????......
?
?????????????if (!MediaFile.isImageFileType(mFileType)) {
//如果不是圖片,則調用processFile進行掃描,而圖片不需要掃描就可以處理
//注意在調用processFile時把這個Java的MyMSC對象又傳了進去。
???????????????processFile(path, mimeType, this);
?????????????}
//掃描完后,需要把新的信息插入數據庫,或者要將原有的信息更新,而endFile就是做這項工作的。
????????????result = endFile(entry, ringtones, notifications,
alarms, music, podcasts);
?????? ?????????}
???????????} ......
???????????return result;
??????? }
下面看這個processFile,這又是一個native的函數。
上面代碼中的beginFile和endFile函數比較簡單,讀者可以自行研究。
MediaScanner的代碼有點繞,是不是?總感覺我們像追兵一樣,追著MS在赤水來回地繞,現在應該是二渡赤水了。來看這個processFile函數,代碼如下所示:
[-->android_media_MediaScanner.cpp]
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)
{
???//Native的MS還是那個MS,其真實類型是PVMS。
???MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);
? //又構造了一個新的Native的MyMSC,不過它指向的Java層的MyMSC沒有變化。
MyMediaScannerClient myClient(env, client);
//調用PVMS的processFile處理這個文件。
mp->processFile(pathStr,mimeTypeStr, myClient);
}
看