[深入理解Android卷一全文-第十章]深入理解MediaScanner
由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該由于紙質媒介的問題而中斷。所以我將在CSDN博客中全文轉發這兩本書的全部內容。
第10章 深入理解MediaScanner
本章主要內容
·? 介紹多媒體系統中媒體文件掃描的工作原理。
本章涉及的源代碼文件名稱及位置
以下是本章分析的源代碼文件名稱及其位置。
·? 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 ?概述
多媒體系統。是Android平臺中非常龐大的一個系統。只是由于篇幅所限,本章僅僅介紹多媒體系統中的重要一員MediaScanner。MediaScanner有什么用呢?可能有些讀者還不是非常清晰。
MediaScanner和媒體文件掃描有關。比如,在Music應用程序中見到的歌曲專輯名、歌曲時長等信息,都是通過它掃描相應的歌曲而得到的。另外,通過MediaStore接口查詢媒體數據庫,從而得到系統中全部媒體文件的相關信息也和MediaScanner有關,由于數據庫的內容就是由MediaScanner加入的。所以MediaScanner是多媒體系統中非常重要的一部分。
伴隨著Android的成長,多媒體系統也發生了非常大的變化。這對開發人員來說,一個非常好的消息。就是從Android 2.3開始那個令人極度郁悶的OpenCore,終于有被干掉的可能了。
從此。也迎來了Stagefright時代。
但Android 2.2在非常長一段時間內還會存在,所以希望以后能有機會深入地剖析這個OpenCore。
以下。就來分析媒體文件掃描的工作原理。
10.2 ?android.process.media的分析
多媒體系統的媒體掃描功能,是通過一個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派生)模塊負責接收外界發來的掃描請求。也就是MS對外提供的接口。
除了支持通過廣播發送掃描請求外。MediaScannerService也支持利用Binder機制跨進程調用掃描函數。
這部分內容。將在本章的拓展部分中介紹。
本章僅關注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函數中處理廣播
??? 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廣播,則啟動內部存儲區的掃描工作,內部存儲區
???????????//實際上掃描的是/system/media目錄。這里存儲了系統自帶的鈴聲等媒體文件。
????????????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 +"/")) {
??????????????????? /*
外部應用能夠發送MEDIA_SCANNER_SCAN_FILE廣播讓MSR啟動單個文件
的掃描工作。
注意這個文件必須位于SD卡上。
*/
??????????????????? scanFile(context, path);
???????????????}
???????????}
??????? }
??? }
從上面代碼中發現MSR接收的三種請求,也就是說,它對外提供三個接口函數:
·? 接收BOOT_COMPLETED請求。這樣MSR會啟動內部存儲區的掃描工作,注意這個內部存儲區實際上是/system/media這個目錄。
·? 接收MEDIA_MOUNTED請求,而且該請求攜帶的外部存儲掛載點路徑必須是/mnt/sdcard。通過這樣的方式MSR會啟動外部存儲區也就是SD卡的掃描工作,掃描目標是目錄/mnt/sdcard。
·? 接收MEDIA_SCANNER_SCAN_FILE請求,而且該請求必須是SD卡上的一個文件,即文件路徑須以/mnt/sdcard開頭,這樣,MSR會啟動針對這個文件的掃描工作。
讀者是否注意到,MSR和跨Binder調用的接口(在本章拓展內容中將介紹)都不支持對目錄的掃描(除了SD卡的根目錄外)。實現這個功能并不復雜,有興趣的讀者可自行完畢該功能,假設方便,請將自己實現的代碼與大家共享。
大部分的媒體文件都已放在SD卡上了,那么來看收到MEDIA_MOUNTED請求后MSR的工作。
還記得第9章中對Vold的分析嗎?這個MEDIA_MOUNTED廣播就是由MountService發送的,一旦有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的工作。
10.2.2 ?MSS模塊的分析
MSS從Service派生,而且實現了Runnable接口。
以下是它的定義:
[-->MediaScannerService.java]
MediaScannerService extends Service implementsRunnable
//MSS實現了Runnable接口。這表明它可能會創建工作線程
依據SDK中對Service生命周期的描寫敘述。Service剛創建時會調用onCreate函數,接著就是onStartCommand函數。之后外界每調用一次startService都會觸發onStartCommand函數。接下來去了解一下onCreate函數及onStartCommand函數。
1. onCreate的分析
onCreate函數的代碼例如以下所看到的:(這是MSS被系統創建時調用的。在它的整個生命周期內僅調用一次。
)
[-->MediaScannerService.java]
public void onCreate(){
?? //獲得電源鎖,防止在掃描過程中休眠
??PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
??mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
//掃描工作是一個漫長的project。所以這里單獨創建一個工作線程,線程函數就是
//MSS實現的Run函數
??? Threadthr = new Thread(null, this, "MediaScannerService");
???thr.start();
|
onCreate將創建一個工作線程:
?publicvoid run()
??? {
????? ??/*
設置本線程的優先級,這個函數的調用有非常關鍵的數據,由于媒體掃描可能會耗費非常長
????????? 時間,假設不調低優先級的話。CPU將一直被MSS占用,導致用戶感覺系統變得非常慢
????????*/
???????Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
????????????????????????????????Process.THREAD_PRIORITY_LESS_FAVORABLE);
???? ???Looper.prepare();
?
???????mServiceLooper = Looper.myLooper();
??????? /*
創建一個Handler,以后發送給這個Handler的消息都會由工作線程處理。
這一部分內容,已在第5章Handler中分析過了。
*/
???????mServiceHandler = new ServiceHandler();
?
???????Looper.loop();
}
onCreate后,MSS將會創建一個帶消息處理機制的工作線程,那么消息是怎么投遞到這個線程中的呢?
2. onStartCommand的分析
還記得MSR的scan函數嗎?例如以下所看到的:
[-->MediaScannerReceiver.java::scan函數]
context.startService(
???????????????new Intent(context, MediaScannerService.class).putExtras(args));
當中Intent包括了目錄掃描請求的目標/mnt/sdcard。這個Intent發出后,終于由MSS的onStartCommand收到并處理,其代碼例如以下所看到的:
[-->MediaScannerService.java]
@Override
?publicint onStartCommand(Intent intent, int flags, int startId)
?{
???? /*
等待mServiceHandler被創建。
耕耘這段代碼的碼農難道不知道
HandlerThread這個類嗎?不熟悉它的讀者請再閱讀第5章的5.4節。
???? */
???? 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函數處理。其代碼例如以下所看到的:
[-->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);
??????? ???????}
}
以下,單獨用一小節來分析這個scan函數。
4. MSS的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]);
?? //向系統發送一個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);???
???????????}
??????? //創建媒體掃描器,并調用它的scanDirectories函數掃描目標目錄
???????MediaScanner scanner = createMediaScanner();
? ????????scanner.scanDirectories(directories,volumeName);
??????? }
?????????......
//通過特殊Uri讓MediaProvider做一些清理工作
???????getContentResolver().delete(scanUri, null, null);
//向系統發送MEDIA_SCANNER_FINISHED廣播
???????sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
?
???????mWakeLock.release();
}
上面代碼中,比較復雜的是MSS和MP的交互。
除了后文中即將看到的正常數據庫操作外,MSS還常常會使用一些特殊的Uri來做數據庫操作,而MP針對這些Uri會做一些特殊處理,比如打開數據庫文件等。
本章不擬對MediaProvider做過多的討論。這部分知識對那些讀完前9章的讀者來說,應該不是什么難題。
如有可能,請讀者自己整理MediaProvider的工作流程,然后提供給大家一起學習,探討。
看MSS中創建媒體掃描器的函數createMediaScanner:
private MediaScanner createMediaScanner() {
//以下這個MediaScanner是在framework/base/中,稍后再分析
???????MediaScanner scanner = new MediaScanner(this);
//獲取當前系統使用的區域信息。掃描的時候將把媒體文件里的信息轉換成當前系統使用的語言
???????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) {
//為掃描器設置當前系統使用的國家和語言。
??????????????????? scanner.setLocale(language+ "_" + country);
???????????????} else {
???????????????????scanner.setLocale(language);
???????????????}
???????????}???
??????? }
???????return scanner;
}
MSS模塊掃描的工作就到此為止了,以下輪到主角MediaScanner登場了。在介紹主角之前,最好還是先總結一下本節的內容。
10.2.3 ?android.process.media媒體掃描工作的流程總結
媒體掃描工作流程涉及MSR和MSS的交互,來總結一下相關的流程:
·? MSR接收外部發來的掃描請求。并通過startService方式啟動MSS處理。
·? MSS的主線程接收MSR所收到的請求。然后投遞給工作線程去處理。
·? 工作線程做一些前期處理工作后(比如向系統廣播掃描開始的消息),就創建媒體掃描器MediaScanner來處理掃描目標。
·? MS掃描完畢后。工作線程再做一些后期處理,然后向系統發送掃描完畢的廣播。
?
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]
?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函數運行完后。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 = 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函數,所以以下將轉戰到JNI層來進行分析。
?
10.3.2 ?JNI層的分析
如今分析MS的JNI層。在Java層中,有三個函數涉及JNI層,它們是:
·? native_init,這個函數由MediaScanner類的static塊調用。
·? native_setup。這個函數由MediaScanner的構造函數調用。
·? processDirectory。這個函數由MS掃描目錄時調用。
分別來分析它們。
1. native_init函數的分析
以下是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信息。待會創建Native對象的指針會保存
//到JavaMS對象的mNativeContext變量中。
??? ?fields.context = env->GetFieldID(clazz,"mNativeContext", "I");
?? ??......
}
native_init函數沒什么新意,這樣的把Native對象的指針保存到Java對象中的做法,已經屢見不鮮。以下看第二個函數native_setup。
2. native_setup函數的分析
native_setup相應的JNI函數例如以下所看到的:
[-->android_media_MediaScanner.cpp]
android_media_MediaScanner_native_setup(JNIEnv*env, jobject thiz)
{
//創建Native層的MediaScanner對象
MediaScanner*mp = createMediaScanner();
......
//把mp的指針保存到Java MS對象的mNativeContext中去
env->SetIntField(thiz,fields.context, (int)mp);
}
//以下的createMediaScanner這個函數將創建一個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函數將創建一個Native層的MS對象。只是可惜的是,它使用的還是Opencore提供的PVMediaScanner,所以后面還不可避免地會和Opencore“正面交鋒”。
4. processDirectory函數的分析
看processDirectories函數,它相應的JNI函數代碼例如以下所看到的:
[-->android_media_MediaScanner.cpp]
android_media_MediaScanner_processDirectory(JNIEnv*env, jobject thiz,
jstring path, jstring extensions, jobject client)
{
?? /*
注意上面傳入的參數,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函數本身倒不難,但又冒出了幾個我們之前沒有接觸過的類型。以下先來認識一下它們。
5. 究竟有多少種對象?
圖10-1展示了MediaScanner所涉及的相關類和它們之間的關系:
圖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的領地了。
10.3.3 ?PVMediaScanner的分析
1. PVMS的processDirectory分析
來看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吧。
2. MyMSC的scanFile分析
(1)JNI層的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函數。這又是為什么呢?
(2)Java層的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用于控制是否強制掃描。有時候一些文件在前后兩次掃描過程中沒有
發生變化,這時候MS能夠不處理這些文件。
假設scanAlways為true,則這些沒有變化
的文件也要掃描。
? */
?? Uriresult = null;
long t1 = System.currentTimeMillis();
try{
???? /*
????? beginFile的主要工作。就是將保存在mFileCache中的相應文件信息的
mSeenInFileSystem設為true。假設這個文件之前沒有在mFileCache中保存,
則會創建一個新項加入到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函數比較簡單,讀者能夠自行研究。
(3)JNI層的processFile分析
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);
}
看來,如今得去看看PVMS的processFile函數了。
3. PVMS的processFile分析
(1)掃描文件
這是我們第一次進入到PVMS的代碼中進行分析:
[-->PVMediaScanner.cpp]
status_t PVMediaScanner::processFile(const char*path, const char* mimeType,
?MediaScannerClient& client)
{
???status_t result;
???InitializeForThread();
??
??? //調用Native MyMSC對象的函數做一些處理
client.setLocale(locale());
/*
beginFile由基類MSC實現。這個函數將構造兩個字符串數組,一個叫mNames,還有一個叫mValues。
這兩個變量的作用和字符編碼有關,后面會碰到。
?? */
???client.beginFile();
??? ......
??? constchar* extension = strrchr(path, '.');
??? //依據文件后綴名來做不同的掃描處理
??? if(extension && strcasecmp(extension, ".mp3") == 0) {
???????result = parseMP3(path, client);//client又傳進去了,我們看看對MP3文件的處理
????......
}
? /*
endFile會依據client設置的區域信息來對mValues中的字符串做語言轉換。比如一首MP3
?? 中的媒體信息是韓文,而手機設置的語言為中文簡體,endFile會盡量對這些韓文進行轉換。
?? 只是語言轉換向來是個大難題,不能保證全部語言的文字都能相互轉換。轉換后的每個value都
會調用handleStringTag做興許處理。
*/
client.endFile();
......
}
以下再到parseMP3這個函數中去看看。它的代碼例如以下所看到的:
[-->PVMediaScanner.cpp]
static PVMFStatus parseMP3(const char *filename,MediaScannerClient& client)
{
? //對MP3文件進行解析。得到諸如duration、流派、標題的TAG(標簽)信息。
在Windows平臺上
//可通過千千靜聽軟件查看MP3文件的全部TAG信息
?? ......
//MP3文件已經掃描完了。以下將這些TAG信息加入到MyMSC中,一起看看
?? if(!client.addStringTag("duration", buffer))
???????......
}
(2)加入TAG信息
文件掃描完了。如今須要把文件里的信息通過addStringTag函數告訴給MyMSC。以下來看addStringTag的工作。這個函數由MyMSC的基類MSC處理。
[-->MediaScannerClient.cpp]
bool MediaScannerClient::addStringTag(constchar* name, const char* value)
{
??? if(mLocaleEncoding != kEncodingNone) {
???????bool nonAscii = false;
???????const char* chp = value;
???????char ch;
???????while ((ch = *chp++)) {
???????????if (ch & 0x80) {
???????????????nonAscii = true;
???????????????break;
???????????}
??????? }
????? /*
推斷name和value的編碼是不是ASCII,假設不是的話則保存到
mNames和mValues中,等到endFile函數的時候再集中做字符集轉換。
?????*/??
??????? if(nonAscii) {
???????????mNames->push_back(name);
???????????mValues->push_back(value);
???????? ???return true;
??????? }
}
//假設字符編碼是ASCII的話。則調用handleStringTag函數。這個函數由子類MyMSC實現。
??? returnhandleStringTag(name, value);
}
[-->android_media_MediaScanner.cpp::MyMediaScannerClient類]
virtual bool handleStringTag(const char* name,const char* value)
{
......
//調用Java層MyMSC對象的handleStringTag進行處理
??mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr,valueStr);
}
[-->MediaScanner.java]
? publicvoid handleStringTag(String name, String value) {
???????????//保存這些TAG信息到MyMSC相應的成員變量中去。
???????????if (name.equalsIgnoreCase("title") ||name.startsWith("title;")) {
???????????????mTitle = value;
???????????} else if (name.equalsIgnoreCase("artist") ||
?name.startsWith("artist;")) {
???????????????mArtist = value.trim();
???????????} else if (name.equalsIgnoreCase("albumartist") ||
?name.startsWith("albumartist;")) {
???????????????mAlbumArtist = value.trim();
???????????}
......
? }
到這里。一個文件的掃描就算做完了。只是,讀者還記得是什么時候把這些信息保存到數據庫的嗎?
是在Java層MyMSC對象的endFile中。這時它會把文件信息組織起來,然后存入媒體數據庫。
10.3.4 ?MediaScanner的總結
以下總結一下媒體掃描的工作流程,它并不復雜。就是有些繞,如圖10-2所看到的:
圖10-2? MediaScanner掃描流程圖
通過上圖能夠發現。MS掃描的流程還是比較清晰的。就是四渡赤水這一招,讓非常多剛開始學習的人摸不著頭腦。
只是讀者千萬不要像我當初那樣。認為這是垃圾代碼的代表。
實際上這是碼農有意而為之,在MediaScanner.java中通過一段比較具體的凝視,對整個流程做了文字總結,這段總結非常easy。這里就不翻譯了。
[-->MediaScanner.java]
//前面還有一段話,讀者可自行閱讀。
以下是流程的文件總結。
* In summary:
?* JavaMediaScannerService calls
?* JavaMediaScanner scanDirectories, which calls
?* JavaMediaScanner processDirectory (native method), which calls
?* nativeMediaScanner processDirectory, which calls
?* nativeMyMediaScannerClient scanFile, which calls
?* JavaMyMediaScannerClient scanFile, which calls
?* JavaMediaScannerClient doScanFile, which calls
?* JavaMediaScanner processFile (native method), which calls
?* nativeMediaScanner processFile, which calls
?* nativeparseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls
?* nativeMyMediaScanner handleStringTag, which calls
?* JavaMyMediaScanner handleStringTag.
?* OnceMediaScanner processFile returns, an entry is inserted in to the database.
看完這么具體的凝視。想必你也會認為,碼農真是有益這么做的。但他們為什么要設計成這樣呢?以后會不會改呢?凝視中也說明了眼下設計的流程是這樣,預計以后有可能改。
10.4 ?拓展思考
10.4.1 ?MediaScannerConnection介紹
通過前面的介紹,我們知道MSS支持以廣播方式發送掃描請求。除了這樣的方式外。多媒體系統還提供了一個MediaScannerConnection類,通過這個類能夠直接跨進程調用MSS的scanFile。而且MSS掃描完一個文件后會通過回調來通知掃描完畢。MediaScannerConnection類的使用場景包括瀏覽器下載了一個媒體文件,彩信接收到一個媒體文件等。這時都能夠用它來運行媒體文件的掃描工作。
以下來看這個類輸出的幾個重要API。由于它非常easy。所以這里就不再進行流程的分析了。
[-->MediaScannerConnection.java]
public class MediaScannerConnection implementsServiceConnection {
?
?//定義OnScanCompletedListener接口,當媒體文件掃描完后,MSS調用這個接口進行通知。
?publicinterface OnScanCompletedListener {
???????public void onScanCompleted(String path, Uri uri);
??? }
//定義MediaScannerConnectionClient接口,派生自OnScanCompletedListener。
//它添加了MediaScannerConnection connect上MSS的通知。
public interface MediaScannerConnectionClient extends
?OnScanCompletedListener {
???????public void onMediaScannerConnected();//連接MSS的回調通知。
???????public void onScanCompleted(String path, Uri uri);
??? }
? //構造函數。
? publicMediaScannerConnection(Context context,
MediaScannerConnectionClient client);
? //封裝了和MSS連接及斷開連接的操作。
? publicvoid connect();
? publicvoid disconnect()
? //掃描單個文件。
? publicvoid scanFile(String path, String mimeType);
? //我更喜歡以下這個靜態函數,它支持多個文件的掃描,實際上間接提供了目錄的掃描功能。
? publicstatic void scanFile(Context context, String[] paths,
String[] mimeTypes,OnScanCompletedListener callback);
?
? ......
}
從使用者的角度來看。本人更喜歡靜態的scanFile函數,一方面它封裝了和MSS連接等相關的工作,還有一方面它還支持多個文件的掃描,所以如沒什么特殊要求,建議讀者還是使用這個靜態函數。
10.4.2 ?我問你答
本節是本書的最后一小節,相信一路走來讀者對Android的認識和理解也許已有提高。
以下將提幾個和媒體掃描相關的問題請讀者思考,或者說是提供給讀者自行鉆研。在解答或研究過程中,讀者如有什么心得,最好還是也記錄并與我們共享。那些對Android有深刻見地的讀者。說不定會收到我們公司HR MM的電話哦!
以下是我在研究MS過程中,認為讀者能夠進行拓展研究的內容:
·? 本書還沒有介紹android.process.media中的MediaProvider模塊。讀者最好還是分別把掃描一個圖片、MP3歌曲、視頻文件的流程走一遍。只是這個流程分析的重點是MediaProvider。
·? MP中最復雜的是縮略圖的生成,讀者在完畢上一步的基礎上。可集中精力解決縮略圖生成的流程。對于視頻文件縮略圖的生成還會涉及MediaPlayerService。
·? 到這一步,相信讀者對MP已有了較全面的認識。作為深入學習的跳板,我建議有興趣的讀者能夠對Android平臺上和數據庫有關的模塊,以及ContentProvider進行深入研究。這里還會涉及非常多問題,比如query返回的Cursor。是怎么把數據從MediaProvider進程傳遞到客戶端進程的?為什么一個ContentProvider死掉后,它的客戶端也會跟著被kill掉?
10.5? 本章小結
本章是全書最后一章。也是最輕松的一章。
這一章重點介紹了多媒體系統中和媒體文件掃描相關的知識。相信讀者對媒體掃描流程中“四渡赤水”的過程印象會深刻一些。
本章拓展部分介紹了API類MediaScannerConnection的用法。另外,提出了幾個和媒體掃描相關的問題請讀者與我們共同思考。
?
轉載于:https://www.cnblogs.com/yutingliuyl/p/6873008.html
總結
以上是生活随笔為你收集整理的[深入理解Android卷一全文-第十章]深入理解MediaScanner的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: The Future Of the So
- 下一篇: 单据体新增界面,单价与金额字段自动进行合