鍍金池/ 教程/ Android/ 第10章 深入理解MediaScanner
第4章 ?深入理解 Zygote
第10章 深入理解MediaScanner
第3章 ?深入理解init
第8章 ?深入理解Surface系統(tǒng)
第5章 深入理解常見類
第7章 ?深入理解Audio系統(tǒng)
第一章 ?閱讀前的準備工作
<span>第6章 深入理解Binder</span>
第9章 ?深入理解Vold和Rild
第2章? 深入理解JNI

第10章 深入理解MediaScanner

本章主要內容

·? 介紹多媒體系統(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

10.1 ?概述

多媒體系統(tǒng),是Android平臺中非常龐大的一個系統(tǒng)。不過由于篇幅所限,本章只介紹多媒體系統(tǒng)中的重要一員MediaScanner。MediaScanner有什么用呢?可能有些讀者還不是很清楚。MediaScanner和媒體文件掃描有關,例如,在Music應用程序中見到的歌曲專輯名、歌曲時長等信息,都是通過它掃描對應的歌曲而得到的。另外,通過MediaStore接口查詢媒體數(shù)據(jù)庫,從而得到系統(tǒng)中所有媒體文件的相關信息也和MediaScanner有關,因為數(shù)據(jù)庫的內容就是由MediaScanner添加的。所以MediaScanner是多媒體系統(tǒng)中很重要的一部分。

伴隨著Android的成長,多媒體系統(tǒng)也發(fā)生了非常大的變化。這對開發(fā)者來說,一個非常好的消息,就是從Android 2.3開始那個令人極度郁悶的OpenCore,終于有被干掉的可能了。從此,也迎來了Stagefright時代。但Android 2.2在很長一段時間內還會存在,所以希望以后能有機會深入地剖析這個OpenCore。

下面,就來分析媒體文件掃描的工作原理。

10.2 ?android.process.media的分析

多媒體系統(tǒng)的媒體掃描功能,是通過一個APK應用程序提供的,它位于package/providers/MediaProvider目錄下。通過分析APK的Android.mk文件可知,該APK運行時指定了一個進程名,如下所示:

application android:process=android.process.media

原來,通過ps命令經(jīng)常看到的進程就是它?。×硗?,從這個APK程序所處的package\providers目錄也可知道,它還是一個ContentProvider。事實上從Android應用程序的四大組件來看,它使用了其中的三個組件:

·? MediaScannerService(從Service派生)模塊負責掃描媒體文件,然后將掃描得到的信息插入到媒體數(shù)據(jù)庫中。

·? MediaProvider(從ContentProvider派生)模塊負責處理針對這些媒體文件的數(shù)據(jù)庫操作請求,例如查詢、刪除、更新等。

·? MediaScannerReceiver(從BroadcastReceiver派生)模塊負責接收外界發(fā)來的掃描請求。也就是MS對外提供的接口。

除了支持通過廣播發(fā)送掃描請求外,MediaScannerService也支持利用Binder機制跨進程調用掃描函數(shù)。這部分內容,將在本章的拓展部分中介紹。

本章僅關注android.process.media進程中的MediaScannerService和MediaScannerReceiver模塊,為書寫方便起見,將這兩個模塊簡稱為MSS和MSR,另外將MediaScanner簡稱MS,將MediaProvider簡稱MP。

下面,開始分析android.process.media中和媒體文件掃描相關的工作流程。

10.2.1 ?MSR模塊的分析

MSR模塊的核心類MediaScannerReceiver從BroadcastReceiver派生,它是專門用來接收廣播的,那么它感興趣的廣播有哪幾種呢?其代碼如下所示:

[-->MediaScannerReceiver.java]

public class MediaScannerReceiver extendsBroadcastReceiver

{

private final static String TAG ="MediaScannerReceiver";

???@Override? //MSR在onReceive函數(shù)中處理廣播

??? 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ā)現(xiàn)MSR接收的三種請求,也就是說,它對外提供三個接口函數(shù):

·? 接收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卡的根目錄外)。實現(xiàn)這個功能并不復雜,有興趣的讀者可自行完成該功能,如果方便,請將自己實現(xiàn)的代碼與大家共享。

大部分的媒體文件都已放在SD卡上了,那么來看收到MEDIA_MOUNTED請求后MSR的工作。還記得第9章中對Vold的分析嗎?這個MEDIA_MOUNTED廣播就是由MountService發(fā)送的,一旦有SD卡被掛載,MSR就會被這個廣播喚醒,接著SD卡的媒體文件就會被掃描了。真是一氣呵成!

SD卡根目錄掃描時調用的函數(shù)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的工作。

10.2.2 ?MSS模塊的分析

MSS從Service派生,并且實現(xiàn)了Runnable接口。下面是它的定義:

[-->MediaScannerService.java]

MediaScannerService extends Service implementsRunnable

//MSS實現(xiàn)了Runnable接口,這表明它可能會創(chuàng)建工作線程

根據(jù)SDK中對Service生命周期的描述,Service剛創(chuàng)建時會調用onCreate函數(shù),接著就是onStartCommand函數(shù),之后外界每調用一次startService都會觸發(fā)onStartCommand函數(shù)。接下來去了解一下onCreate函數(shù)及onStartCommand函數(shù)。

1. onCreate的分析

onCreate函數(shù)的代碼如下所示:(這是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)建一個工作線程,線程函數(shù)就是

//MSS實現(xiàn)的Run函數(shù)

??? Threadthr = new Thread(null, this, "MediaScannerService");

???thr.start();

|

onCreate將創(chuàng)建一個工作線程:

?publicvoid run()

??? {

????? ??/*

設置本線程的優(yōu)先級,這個函數(shù)的調用有很重要的作用,因為媒體掃描可能會耗費很長

????????? 時間,如果不調低優(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)建一個帶消息處理機制的工作線程,那么消息是怎么投遞到這個線程中的呢?

2. onStartCommand的分析

還記得MSR的scan函數(shù)嗎?如下所示:

[-->MediaScannerReceiver.java::scan函數(shù)]

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將把掃描請求信息投遞到工作線程去處理。

3. 處理掃描請求

掃描請求由ServiceHandler的handleMessage函數(shù)處理,其代碼如下所示:

[-->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函數(shù)開展文件夾掃描工作,可以一次為這個函數(shù)設置多個目標文件夾,

不過這里只有/mnt/sdcard一個目錄

*/

??????????????????? scan(directories, volume);

???????????????????? ......

??????? ????????????stopSelf(msg.arg1);

??????? ???????}

}

下面,單獨用一小節(jié)來分析這個scan函數(shù)。

4. MSS的scan函數(shù)分析

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函數(shù)也是通過insert特殊Uri讓MediaProvider打開數(shù)據(jù)庫

???????????if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {

????????????????openDatabase(volumeName);???

???????????}

??????? //創(chuàng)建媒體掃描器,并調用它的scanDirectories函數(shù)掃描目標文件夾

???????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的交互。除了后文中即將看到的正常數(shù)據(jù)庫操作外,MSS還經(jīng)常會使用一些特殊的Uri來做數(shù)據(jù)庫操作,而MP針對這些Uri會做一些特殊處理,例如打開數(shù)據(jù)庫文件等。

本章不擬對MediaProvider做過多的討論,這部分知識對那些讀完前9章的讀者來說,應該不是什么難題。如有可能,請讀者自己整理MediaProvider的工作流程,然后提供給大家一起學習,探討。

看MSS中創(chuàng)建媒體掃描器的函數(shù)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é)的內容。

10.2.3 ?android.process.media媒體掃描工作的流程總結

媒體掃描工作流程涉及MSR和MSS的交互,來總結一下相關的流程:

·? MSR接收外部發(fā)來的掃描請求,并通過startService方式啟動MSS處理。

·? MSS的主線程接收MSR所收到的請求,然后投遞給工作線程去處理。

·? 工作線程做一些前期處理工作后(例如向系統(tǒng)廣播掃描開始的消息),就創(chuàng)建媒體掃描器MediaScanner來處理掃描目標。

·? MS掃描完成后,工作線程再做一些后期處理,然后向系統(tǒng)發(fā)送掃描完畢的廣播。

?

10.3 ?MediaScanner的分析

現(xiàn)在分析媒體掃描器MediaScanner的工作原理,它將縱跨Java層、JNI層,以及Native層。先看它在Java層中的內容。

10.3.1 ?Java層的分析

1. 創(chuàng)建MediaScanner

認識一下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層的函數(shù)做一些初始化工作

???????......

}

在上面的MS中,比較重要的幾個調用函數(shù)是:

·? native_init和native_setup,關于它們的故事,在分析JNI層時再做介紹。

MS創(chuàng)建好后,MSS將調用它的scanDirectories開展掃描工作,下面來看這個函數(shù)。

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函數(shù),調用它來對目標文件夾進行掃描,

? 其中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)計掃描時間等

?}

上面一共列出了四個關鍵點,下面逐一對其分析。

(1)initialize的分析

initialize主要是初始化一些Uri,因為掃描時需把文件的信息插入媒體數(shù)據(jù)庫中,而媒體數(shù)據(jù)庫針對Video、Audio、Image文件等都有對應的表,這些表的地址則由Uri表示。下面是initialize的代碼:

[-->MediaScanner.java]

private void initialize(String volumeName) {

//得到IMediaProvider對象,通過這個對象可以對媒體數(shù)據(jù)庫進行操作。

? mMediaProvider=

?mContext.getContentResolver().acquireProvider("media");

//初始化Uri,下面分別介紹一下。

//音頻表的地址,也就是數(shù)據(jù)庫中的audio_meta表。

???? ?mAudioUri =Audio.Media.getContentUri(volumeName);

????? //視頻表地址,也就是數(shù)據(jù)庫中的video表。

?????mVideoUri = Video.Media.getContentUri(volumeName);

????? //圖片表地址,也就是數(shù)據(jù)庫中的images表。

?????mImagesUri = Images.Media.getContentUri(volumeName);

????? //縮略圖表地址,也就是數(shù)據(jù)庫中的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;

???????????}

??????? }

}

下面看第二個關鍵函數(shù)prescan。

(2)prescan的分析

在媒體掃描過程中,有個令人頭疼的問題,來舉個例子,這個例子會貫穿在對這個問題整體分析的過程中。例子:假設某次掃描之前SD卡中有100個媒體文件,數(shù)據(jù)庫中有100條關于這些文件的記錄,現(xiàn)因某種原因刪除了其中的50個媒體文件,那么媒體數(shù)據(jù)庫什么時候會被更新呢?

讀者別小瞧這個問題。現(xiàn)在有很多文件管理器支持刪除文件和文件夾,它們用起來很方便,卻沒有對應地更新數(shù)據(jù)庫,這導致了查詢數(shù)據(jù)庫時還能得到這些媒體文件信息,但這個文件實際上已不存在了,而且后面所有和此文件有關的操作都會因此而失敗。

其實,MS已經(jīng)考慮到這一點了,prescan函數(shù)的主要作用是在掃描之前把數(shù)據(jù)庫中和文件相關的信息取出并保存起來,這些信息主要是媒體文件的路徑,所屬表的Uri。就上面這個例子來說,它會從數(shù)據(jù)庫中取出100個文件的文件信息。

prescan的代碼如下所示:

[-->MediaScanner.java]

?privatevoid prescan(String filePath) throws RemoteException {

???????Cursor c = null;

???????String where = null;

???????String[] selectionArgs = null;

??????? //mFileCache保存從數(shù)據(jù)庫中獲取的文件信息。

??????? 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 };

???????????}

???????????//查詢數(shù)據(jù)庫的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;

???????????????}

???????????}

???????? ......//查詢其他表,取出數(shù)據(jù)中關于視頻,圖像等文件的信息并存入到mFileCache中。

???????finally {

???????????if (c != null) {

???????????????c.close();

???????????}

?????? ?}

??? }

懂了前面的例子,在閱讀prescan函數(shù)時可能就比較輕松了。prescan函數(shù)執(zhí)行完后,mFileCache保存了掃描前所有媒體文件的信息,這些信息是從數(shù)據(jù)庫中查詢得來的,也就是舊有的信息。

接下來,看最后兩個關鍵函數(shù)。

(3)processDirectory和postscan的分析

processDirectory是一個native函數(shù),其具體功能放到JNI層再分析,這里先簡單介紹,它在解決上一節(jié)那個例子中提出的問題時,所做的工作。答案是:

processDirectory將掃描SD卡,每掃描一個文件,都會設置mFileCache中對應文件的一個叫mSeenInFileSystem的變量為true。這個值表示這個文件目前還存在于SD卡上。這樣,待整個SD卡掃描完后,mFileCache的那100個文件中就會有50個文件的mSeenInFileSystem為true,而剩下的另50個文件則為初始值false。

看到上面的內容,可以知道postscan的作用了吧?就是它把不存在于SD卡的文件信息從數(shù)據(jù)庫中刪除,而使數(shù)據(jù)庫得以徹底更新的。來看postscan函數(shù)是否是這樣處理的:

[-->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;

??????????????????? }

???????????????}

???????????}

??????? //如果文件確實丟失,則需要把數(shù)據(jù)庫中和它相關的信息刪除。

??????? if(fileMissing) {

??????????MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path);

??????????int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType);

????????? if(MediaFile.isPlayListFileType(fileType)) {

??????????????????? ?......//處理丟失文件是播放列表的情況

????????? ??} else {

??????????????/*

由于文件信息中還攜帶了它在數(shù)據(jù)庫中的相關信息,所以從數(shù)據(jù)庫中刪除對應的信息會

非???。

??????????????*/

??????????????mMediaProvider.delete(ContentUris.withAppendedId(

entry.mTableUri, entry.mRowId), null, null);

????????????iterator.remove();

????????????}

????????? }

???? }

??? ......//刪除縮略圖文件等工作

}

Java層中的四個關鍵點,至此已介紹了三個,另外一個processDirectory是媒體掃描的關鍵函數(shù),由于它是一個native函數(shù),所以下面將轉戰(zhàn)到JNI層來進行分析。

?

10.3.2 ?JNI層的分析

現(xiàn)在分析MS的JNI層。在Java層中,有三個函數(shù)涉及JNI層,它們是:

·? native_init,這個函數(shù)由MediaScanner類的static塊調用。

·? native_setup,這個函數(shù)由MediaScanner的構造函數(shù)調用。

·? processDirectory,這個函數(shù)由MS掃描文件夾時調用。

分別來分析它們。

1. native_init函數(shù)的分析

下面是native_init對應的JNI函數(shù),其代碼如下所示:

[-->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函數(shù)沒什么新意,這種把Native對象的指針保存到Java對象中的做法,已經(jīng)屢見不鮮。下面看第二個函數(shù)native_setup。

2. native_setup函數(shù)的分析

native_setup對應的JNI函數(shù)如下所示:

[-->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這個函數(shù)將創(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函數(shù)將創(chuàng)建一個Native層的MS對象,不過可惜的是,它使用的還是Opencore提供的PVMediaScanner,所以后面還不可避免地會和Opencore“正面交鋒”。

4. processDirectory函數(shù)的分析

看processDirectories函數(shù),它對應的JNI函數(shù)代碼如下所示:

[-->android_media_MediaScanner.cpp]

android_media_MediaScanner_processDirectory(JNIEnv*env, jobject thiz,

jstring path, jstring extensions, jobject client)

{

?? /*

注意上面?zhèn)魅氲膮?shù),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對象做參數(shù)。

?? //這個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函數(shù)本身倒不難,但又冒出了幾個我們之前沒有接觸過的類型,下面先來認識一下它們。

5. 到底有多少種對象?

圖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函數(shù)的時候會使用Native的MyMSC對象。

·? 另外,圖中Native MS類的processFile是一個虛函數(shù),需要派生類來實現(xiàn)。

其中比較費解的是MyMSC對象。它們有什么用呢?這個問題真是一言難盡。下面通過processDirectory來探尋其中原因,這回得進入PVMediaScanner的領地了。

10.3.3 ?PVMediaScanner的分析

1. PVMS的processDirectory分析

來看PVMediaScanner(以后簡稱為PVMS,它就是Native層的MS)的processDirectory函數(shù)。這個函數(shù)是由它的基類MS實現(xiàn)的。注意,源碼中有兩個MediaScanner.cpp,它們的位置分別是:

·? framework/base/media/libmedia

·? external/opencore/android/

看libmedia下的那個MediaScanner.cpp,其中processDirectory函數(shù)的代碼如下所示:

[-->MediaScanner.cpp]

status_t MediaScanner::processDirectory(constchar *path,

const char *extensions, MediaScannerClient&client,

??????? ??????????????????ExceptionCheckexceptionCheck, void *exceptionEnv) {

??? ?

???......//做一些準備工作

???client.setLocale(locale()); //給Native的MyMSC設置locale信息

?? //調用doProcessDirectory函數(shù)掃描文件夾

status_tresult =? doProcessDirectory(pathBuffer,pathRemaining,

extensions, client,exceptionCheck, exceptionEnv);

?

???free(pathBuffer);

?

??? returnresult;

}

//下面直接看這個doProcessDirectory函數(shù)

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支持的類型(根據(jù)文件的后綴名來判斷)

????????? ??????struct stat statbuf;

???????????????stat(path, &statbuf); //取出文件的修改時間和文件的大小

???????????????if (statbuf.st_size > 0) {

??????????????????? //如果該文件大小非零,則調用MyMSC的scanFile函數(shù)!???

??????????????????? client.scanFile(path,statbuf.st_mtime, statbuf.st_size);

???????????????}

???????????????if (exceptionCheck && exceptionCheck(exceptionEnv)) gotofailure;

???????????}

??????? }

??? }

......

}

假設正在掃描的媒體文件的類型是屬于MS支持的,那么,上面代碼中最不可思議的是,它竟然調用了MSC的scanFile來處理這個文件,也就是說,MediaScanner調用MediaScannerClient的scanFile函數(shù)。這是為什么呢?還是來看看這個MSC的scanFile吧。

2. MyMSC的scanFile分析

(1)JNI層的scanFile

其實,在調用processDirectory時,所傳入的MSC對象的真實類型是MyMediaScannerClient,下面來看它的scanFile函數(shù),代碼如下所示:

[-->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函數(shù)

???????mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,

lastModified, fileSize);

?

???????mEnv->DeleteLocalRef(pathStr);

???????return (!mEnv->ExceptionCheck());

}

太沒有天理了!Native的MyMSCscanFile主要的工作就是調用Java層MyMSC的scanFile函數(shù)。這又是為什么呢?

(2)Java層的scanFile

現(xiàn)在只能來看Java層的這個MyMSC對象了,它的scanFile代碼如下所示:

[-->MediaScanner.java]

public void scanFile(String path, longlastModified, long fileSize) {

???????????......

???????????//調用doScanFile函數(shù)

???????????doScanFile(path, null, lastModified, fileSize, false);

??????? }

//直接來看doScanFile函數(shù)

?publicUri doScanFile(String path, String mimeType, long lastModified,

long fileSize, boolean scanAlways) {

? /*

上面參數(shù)中的scanAlways用于控制是否強制掃描,有時候一些文件在前后兩次掃描過程中沒有

發(fā)生變化,這時候MS可以不處理這些文件。如果scanAlways為true,則這些沒有變化

的文件也要掃描。

? */

?? Uriresult = null;

long t1 = System.currentTimeMillis();

try{

???? /*

????? beginFile的主要工作,就是將保存在mFileCache中的對應文件信息的

mSeenInFileSystem設為true。如果這個文件之前沒有在mFileCache中保存,

則會創(chuàng)建一個新項添加到mFileCache中。另外它還會根據(jù)傳入的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);

?????????????}

//掃描完后,需要把新的信息插入數(shù)據(jù)庫,或者要將原有的信息更新,而endFile就是做這項工作的。

????????????result = endFile(entry, ringtones, notifications,

alarms, music, podcasts);

?????? ?????????}

???????????} ......

???????????return result;

??????? }

下面看這個processFile,這又是一個native的函數(shù)。

上面代碼中的beginFile和endFile函數(shù)比較簡單,讀者可以自行研究。

(3)JNI層的processFile分析

MediaScanner的代碼有點繞,是不是?總感覺我們像追兵一樣,追著MS在赤水來回地繞,現(xiàn)在應該是二渡赤水了。來看這個processFile函數(shù),代碼如下所示:

[-->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);

}