本章主要內容 java
· 介紹多媒體系統中媒體文件掃描的工做原理。 android
本章涉及的源代碼文件名及位置 數據庫
下面是本章分析的源碼文件名及其位置。 數組
· MediaProvider.java 瀏覽器
packages/providers/MediaProvider/MediaProvider.java app
· MediaScannerReceiver.java ide
packages/providers/MediaProvider/MediaScannerReceiver.java 函數
· MediaScannerService.java oop
packages/providers/MediaProvider/MediaScannerService.java post
· 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
多媒體系統,是Android平臺中很是龐大的一個系統。不過因爲篇幅所限,本章只介紹多媒體系統中的重要一員MediaScanner。MediaScanner有什麼用呢?可能有些讀者還不是很清楚。MediaScanner和媒體文件掃描有關,例如,在Music應用程序中見到的歌曲專輯名、歌曲時長等信息,都是經過它掃描對應的歌曲而獲得的。另外,經過MediaStore接口查詢媒體數據庫,從而獲得系統中全部媒體文件的相關信息也和MediaScanner有關,由於數據庫的內容就是由MediaScanner添加的。因此MediaScanner是多媒體系統中很重要的一部分。
伴隨着Android的成長,多媒體系統也發生了很是大的變化。這對開發者來講,一個很是好的消息,就是從Android 2.3開始那個使人極度鬱悶的OpenCore,終於有被幹掉的可能了。今後,也迎來了Stagefright時代。但Android 2.2在很長一段時間內還會存在,因此但願之後能有機會深刻地剖析這個OpenCore。
下面,就來分析媒體文件掃描的工做原理。
多媒體系統的媒體掃描功能,是經過一個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中和媒體文件掃描相關的工做流程。
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的工做。
MSS從Service派生,而且實現了Runnable接口。下面是它的定義:
[-->MediaScannerService.java]
MediaScannerService extends Service implementsRunnable
//MSS實現了Runnable接口,這代表它可能會建立工做線程
根據SDK中對Service生命週期的描述,Service剛建立時會調用onCreate函數,接着就是onStartCommand函數,以後外界每調用一次startService都會觸發onStartCommand函數。接下來去了解一下onCreate函數及onStartCommand函數。
onCreate函數的代碼以下所示:(這是MSS被系統建立時調用的,在它的整個生命週期內僅調用一次。)
[-->MediaScannerService.java]
public void onCreate(){
//得到電源鎖,防止在掃描過程當中休眠
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
//掃描工做是一個漫長的工程,因此這裏單首創建一個工做線程,線程函數就是
//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將會建立一個帶消息處理機制的工做線程,那麼消息是怎麼投遞到這個線程中的呢?
還記得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將把掃描請求信息投遞到工做線程去處理。
掃描請求由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函數。
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登場了。在介紹主角以前,不妨先總結一下本節的內容。
媒體掃描工做流程涉及MSR和MSS的交互,來總結一下相關的流程:
· MSR接收外部發來的掃描請求,並經過startService方式啓動MSS處理。
· MSS的主線程接收MSR所收到的請求,而後投遞給工做線程去處理。
· 工做線程作一些前期處理工做後(例如向系統廣播掃描開始的消息),就建立媒體掃描器MediaScanner來處理掃描目標。
· MS掃描完成後,工做線程再作一些後期處理,而後向系統發送掃描完畢的廣播。
如今分析媒體掃描器MediaScanner的工做原理,它將縱跨Java層、JNI層,以及Native層。先看它在Java層中的內容。
認識一下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開展掃描工做,下面來看這個函數。
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是一個字符串,包含了當前多媒體系統所支持的
媒體文件的後綴名,例如.MP三、.MP4等。mClient爲MyMediaScannerClient類型,
它是從MediaScannerClient類派生的。它的做用咱們後面再作分析。
*/
processDirectory(directories[i], MediaFile.sFileExtensions,
mClient);
}
long scan = System.currentTimeMillis();
postscan(directories);//④掃描後處理
long end = System.currentTimeMillis();
......//統計掃描時間等
}
上面一共列出了四個關鍵點,下面逐一對其分析。
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。
在媒體掃描過程當中,有個使人頭疼的問題,來舉個例子,這個例子會貫穿在對這個問題總體分析的過程當中。例子:假設某次掃描以前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保存了掃描前全部媒體文件的信息,這些信息是從數據庫中查詢得來的,也就是舊有的信息。
接下來,看最後兩個關鍵函數。
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層來進行分析。
如今分析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信息。待會建立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)
{
//建立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「正面交鋒」。
看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函數自己倒不難,但又冒出了幾個咱們以前沒有接觸過的類型,下面先來認識一下它們。
圖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的領地了。
來看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用於控制是否強制掃描,有時候一些文件在先後兩次掃描過程當中沒有
發生變化,這時候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函數比較簡單,讀者能夠自行研究。
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函數了。
這是咱們第一次進入到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))
......
}
文件掃描完了,如今須要把文件中的信息經過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-2所示:
圖10-2 MediaScanner掃描流程圖
經過上圖能夠發現,MS掃描的流程仍是比較清晰的,就是四渡赤水這一招,讓不少初學者摸不着頭腦。不過讀者千萬不要像我當初那樣,以爲這是垃圾代碼的表明。實際上這是碼農有意而爲之,在MediaScanner.java中經過一段比較詳細的註釋,對整個流程作了文字總結,這段總結很是簡單,這裏就不翻譯了。
[-->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.
看完這麼詳細的註釋,想必你也會認爲,碼農真是故意這麼作的。但他們爲何要設計成這樣呢?之後會不會改呢?註釋中也說明了目前設計的流程是這樣,估計之後有可能改。
經過前面的介紹,咱們知道MSS支持以廣播方式發送掃描請求。除了這種方式外,多媒體系統還提供了一個MediaScannerConnection類,經過這個類能夠直接跨進程調用MSS的scanFile,而且MSS掃描完一個文件後會經過回調來通知掃描完畢。MediaScannerConnection類的使用場景包括瀏覽器下載了一個媒體文件,彩信接收到一個媒體文件等,這時均可以用它來執行媒體文件的掃描工做。
下面來看這個類輸出的幾個重要API,因爲它很是簡單,因此這裏就再也不進行流程的分析了。
[-->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鏈接等相關的工做,另外一方面它還支持多個文件的掃描,因此如沒什麼特殊要求,建議讀者仍是使用這個靜態函數。
本節是本書的最後一小節,相信一路走來讀者對Android的認識和理解或許已有提升。下面將提幾個和媒體掃描相關的問題請讀者思考,或者說是提供給讀者自行鑽研。在解答或研究過程當中,讀者若有什麼心得,不妨也記錄並與咱們共享。那些對Android有深入見地的讀者,說不定會收到咱們公司HR MM的電話哦!
下面是我在研究MS過程當中,以爲讀者能夠進行拓展研究的內容:
· 本書尚未介紹android.process.media中的MediaProvider模塊,讀者不妨分別把掃描一個圖片、MP3歌曲、視頻文件的流程走一遍,不過這個流程分析的重點是MediaProvider。
· MP中最複雜的是縮略圖的生成,讀者在完成上一步的基礎上,可集中精力解決縮略圖生成的流程。對於視頻文件縮略圖的生成還會涉及MediaPlayerService。
· 到這一步,相信讀者對MP已有了較全面的認識。做爲深刻學習的跳板,我建議有興趣的讀者能夠對Android平臺上和數據庫有關的模塊,以及ContentProvider進行深刻研究。這裏還會涉及不少問題,例如query返回的Cursor,是怎麼把數據從MediaProvider進程傳遞到客戶端進程的?爲何一個ContentProvider死掉後,它的客戶端也會跟着被kill掉?
本章是全書最後一章,也是最輕鬆的一章。這一章重點介紹了多媒體系統中和媒體文件掃描相關的知識,相信讀者對媒體掃描流程中「四渡赤水」的過程印象會深入一些。
本章拓展部分介紹了API類MediaScannerConnection的使用方法,另外,提出了幾個和媒體掃描相關的問題請讀者與咱們共同思考。