Android媒體掃描詳細解析之一(MediaScanner & MediaProvider)
用過Android手機的同學都知道,每次開機的時候系統會先掃描sdcard,sdcard重新插拔(掛載)也會掃描一次sdcard。
為什麼要掃描sdcard,其實是為了給系統的其他應用提供便利,比如,Gallary、Music、VideoPlayer等應用,進入Gallary後會顯示sdcard中的所有圖片,
如果進入Gallary後再去掃描,可想而知,你會厭惡這個應用,因為我們會覺得它反應太慢了。還有Music你看到播放列表的時候實際能看到這首歌曲的時長、演唱者、專輯
等資訊,這個也不是你進入應用後一下子可以讀出來的。
所以Android使用sdcard掛載後掃描的機制,先將這些媒體相關的資訊掃描出來儲存在資料庫中,當開啟應用的時候直接去資料庫讀取(或者所通過MediaProvider去從資料庫讀取)並show給使用者,這樣使用者體驗會好很多,下面我們分析這種掃描機制是如何實現的。
在原始碼目錄的\packages\providers\MediaProvider下面是MediaProvider的原始碼,它就是完成掃描並將資料保存於資料庫中的程式。
先看下它的AndroidManifest.xml檔案
application android:process="android.process.media",也就是應用程式名稱為android.process.media,我們用adb 連線到android 裝置,並且進入shell後輸入ps可以看到的確有應用程式app_4 2796 2075 165192 19420 ffffffff 6fd0eb58 S android.process.media 在執行。另外此程式中有三個部分,分別是provider - MediaProvider 、receiver - MediaScannerReceiver、service - MediaScannerService,它並沒有activity,說明它是一直運行於後臺的程式。並且從receiver中的
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
可以看出它是開機自啟動的。下面從這個廣播開始看程式碼。
public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Uri uri = intent.getData(); String externalStoragePath = Environment.getExternalStorageDirectory().getPath(); if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { // scan internal storage scan(context, MediaProvider.INTERNAL_VOLUME); } else { 。 。 。 } } }
收到開機廣播後,首先執行scan函式STEP 1,
private void scan(Context context, String volume) {
Bundle args = new Bundle();
args.putString("volume", volume);
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
}
scan函式主要傳進來一個volume卷名,MediaProvider.INTERNAL_VOLUME實際就是內建儲存卡"internal",在此我們首先理解為開機後首先掃描內建儲存卡。
然後啟動services MediaScannerService,這也是此服務第一次被啟動。
service的啟動流程就不說了,onCreate肯定是首先被呼叫的
public void onCreate()
{
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block.
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
}
此處前面是申請了一把wake lock ,主要是防止CPU休眠的,然後啟動了一個執行緒實際就是 MediaScannerService自身的執行緒,它繼承自Runnable,下面主要看Run函式
public void run()
{
// reduce priority below other background threads to avoid interfering
// with other services at boot time.
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
Process.THREAD_PRIORITY_LESS_FAVORABLE);
Looper.prepare();
mServiceLooper = Looper.myLooper();
mServiceHandler = new ServiceHandler();
Looper.loop();
}
可以看出此執行緒的目的是為了處理hander訊息ServiceHandler
執行完onCreate後就會執行onStartCommand
public int onStartCommand(Intent intent, int flags, int startId)
{
while (mServiceHandler == null) {
synchronized (this) {
try {
wait(100);
} catch (InterruptedException e) {
}
}
}
if (intent == null) {
Log.e(TAG, "Intent is null in onStartCommand: ",
new NullPointerException());
return Service.START_NOT_STICKY;
}
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent.getExtras();
mServiceHandler.sendMessage(msg);
// Try again later if we are killed before we can finish scanning.
return Service.START_REDELIVER_INTENT;
}
在這裡我們通過STEP 1中傳入進來的volume字串就作為了msg.obj通過handler來處理了
private final class ServiceHandler extends Handler
{
@Override
public void handleMessage(Message msg)
{
Bundle arguments = (Bundle) msg.obj;
String filePath = arguments.getString("filepath");
String folder = arguments.getString("folder");
try {
if (filePath != null) {
IBinder binder = arguments.getIBinder("listener");
IMediaScannerListener listener =
(binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
Uri uri = scanFile(filePath, arguments.getString("mimetype"));
if (listener != null) {
listener.scanCompleted(filePath, uri);
}
} else if(folder != null) {
String volume = arguments.getString("volume");
String[] directories = null;
directories = new String[] {
new File(folder).getPath(),
};
if (directories != null) {
if (Config.LOGD) Log.d(TAG, "start scanning volume " + volume + " ; path = " + folder);
scan(directories, volume);
if (Config.LOGD) Log.d(TAG, "done scanning volume " + volume + " ; path = " + folder);
}
}else {
String volume = arguments.getString("volume");
String[] directories = null;
if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
// scan internal media storage
directories = new String[] {
Environment.getRootDirectory() + "/media",
};
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
// scan external storage
directories = new String[] {
Environment.getExternalStorageDirectory().getPath(),
};
}
if (directories != null) {
if (Config.LOGD) Log.d(TAG, "start scanning volume " + volume);
scan(directories, volume);
if (Config.LOGD) Log.d(TAG, "done scanning volume " + volume);
}
}
} catch (Exception e) {
Log.e(TAG, "Exception in handleMessage", e);
}
stopSelf(msg.arg1);
}
};
}
很顯然我們會執行到標記為紅色的else中,我們是先掃描內建sdcard,很顯然directories的值為/system/media ,然後呼叫 scan(directories, volume);函式,應該是內建sdcard中所有的媒體檔案都幾種儲存在/system/media下面所以只需要掃描這一個路徑就行了。STEP2
private void scan(String[] directories, String volumeName) {
// don't sleep while scanning
mWakeLock.acquire();
ContentValues values = new ContentValues();
values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
Uri uri = Uri.parse("file://" + directories[0]);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
try {
if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
openDatabase(volumeName);
}
MediaScanner scanner = createMediaScanner();
scanner.scanDirectories(directories, volumeName);
} catch (Exception e) {
Log.e(TAG, "exception in MediaScanner.scan()", e);
}
getContentResolver().delete(scanUri, null, null);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
mWakeLock.release();
}
開始掃描和結束掃描時都會發送一個全域性的廣播,第三方應用程式也可以通過註冊這兩個廣播來避開在media 掃描的時候往改掃描資料夾裡面寫入或刪除檔案,這個我在專案中就遇到過這種bug。在這一步驟中建立了MediaScanner並呼叫它的scanDirectories方法 STEP3
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(directories[i], MediaFile.sFileExtensions, mClient);
}
long scan = System.currentTimeMillis();
postscan(directories);
long end = System.currentTimeMillis();
if (Config.LOGD) {
Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n");
Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n");
Log.d(TAG, "postscan time: " + (end - scan) + "ms\n");
Log.d(TAG, " total time: " + (end - start) + "ms\n");
}
} catch (SQLException e) {
// this might happen if the SD card is removed while the media scanner is running
Log.e(TAG, "SQLException in MediaScanner.scan()", e);
} catch (UnsupportedOperationException e) {
// this might happen if the SD card is removed while the media scanner is running
Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e);
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in MediaScanner.scan()", e);
}
}
其中initialize prescan processDirectory postscan這四個函式都比較重要。STEP 4
private void initialize(String volumeName) {
mMediaProvider = mContext.getContentResolver().acquireProvider("media");
mAudioUri = Audio.Media.getContentUri(volumeName);
mVideoUri = Video.Media.getContentUri(volumeName);
mImagesUri = Images.Media.getContentUri(volumeName);
mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
if (!volumeName.equals("internal")) {
// we only support playlists on external media
mProcessPlaylists = true;
mProcessGenres = true;
mGenreCache = new HashMap<String, Uri>();
mGenresUri = Genres.getContentUri(volumeName);
mPlaylistsUri = Playlists.getContentUri(volumeName);
// assuming external storage is FAT (case insensitive), except on the simulator.
if ( Process.supportsProcesses()) {
mCaseInsensitivePaths = true;
}
}
}
做一些初始化的動作,得到MediaProvider和一些URI實際也就是操作資料庫的一些表名。Audio.Media.getContentUri可以在MediaStore.java中找到,此類儲存了所有的媒體格式URI等資訊,此處獲得的mAudioUri的值為“content://media/internal//audio/media"
STEP5
private void prescan(String filePath) throws RemoteException {
。 。 。
}
此函式比較長,在此省略程式碼,有興趣的可以看原始碼,這裡所做的操作是對於之前有掃描過的,就將資料庫中現有的媒體資訊放到幾個資料結構中臨時儲存起來。
然後最重要的STEP 6 processDirectory是一個native函式,先注意幾個傳入引數directories[i]為STEP2中傳入的路徑/system/media ,MediaFile.sFileExtensions 這個你可以跟到MediaFile中看看這個是如何賦值的,實際就是所有支援的媒體格式字尾以‘,’的方式串在一起的字串”MP3,M4A,3GA,WAV。。。“最重要的mClient是MyMediaScannerClient的一個例項,此物件將是native層回撥函式的介面,所有掃描完後的媒體都會通過此物件來儲存到資料庫中。
下面進入Native層對應檔案是android_media_MediaScanner.cpp STEP 6
static void
android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject client)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
if (path == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
if (extensions == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
if (extensionsStr == NULL) { // Out of memory
env->ReleaseStringUTFChars(path, pathStr);
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
MyMediaScannerClient myClient(env, client);
mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
env->ReleaseStringUTFChars(path, pathStr);
env->ReleaseStringUTFChars(extensions, extensionsStr);
}
這裡比較重要的一個點時MyMediaScannerClient myClient(env, client);定義了一個客戶端,並將java層的client傳入進去,很顯然,是想通過MyMediaScannerClient 再來回調client。
STEP 7
status_t MediaScanner::processDirectory(
const char *path, const char *extensions,
MediaScannerClient &client,
ExceptionCheck exceptionCheck, void *exceptionEnv) {
int pathLength = strlen(path);
if (pathLength >= PATH_MAX) {
return UNKNOWN_ERROR;
}
char* pathBuffer = (char *)malloc(PATH_MAX + 1);
if (!pathBuffer) {
return UNKNOWN_ERROR;
}
int pathRemaining = PATH_MAX - pathLength;
strcpy(pathBuffer, path);
if (pathLength > 0 && pathBuffer[pathLength - 1] != '/') {
pathBuffer[pathLength] = '/';
pathBuffer[pathLength + 1] = 0;
--pathRemaining;
}
client.setLocale(locale());
status_t result =
doProcessDirectory(
pathBuffer, pathRemaining, extensions, client,
exceptionCheck, exceptionEnv);
free(pathBuffer);
return result;
}
此函式沒幹什麼事,具體工作是在doProcessDirectory中做的 STEP 8
status_t MediaScanner::doProcessDirectory(
char *path, int pathRemaining, const char *extensions,
MediaScannerClient &client, ExceptionCheck exceptionCheck,
void *exceptionEnv) {
. . .
}
此函式太長,在此不粘出來了,這裡首先要解釋下這些引數,path - 要掃描資料夾路徑以'/'結尾,pathRemaining為路徑長度與路徑最大長度之間的差值,也就是防止掃描時路徑超出範圍,extensions 前面已經解釋過是字尾,client是是STEP6中例項化的MyMediaScannerClient物件,後面兩個引數是一些異常處理不用關心。
大家仔細看這個函式的程式碼就可以知道,它完成的是遍歷資料夾並找到有相應extensions 裡面字尾的檔案fileMatchesExtension(path, extensions),如果檔案大小大於0就呼叫client.scanFile(path, statbuf.st_mtime, statbuf.st_size);來進行檔案讀取掃描 注意這裡才會讀檔案的實際內容。
STEP 9
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}
看看,終於用到了mClient,java層傳進來的client ,這就是回撥到了java 類MyMediaScannerClient裡面的STEP 10
public void scanFile(String path, long lastModified, long fileSize) {
// This is the callback funtion from native codes.
// Log.v(TAG, "scanFile: "+path);
doScanFile(path, null, lastModified, fileSize, false);
}
主要看doScanFile STEP 11
public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) {
Uri result = null;
// long t1 = System.currentTimeMillis();
try {
FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize);
// rescan for metadata if file was modified since last scan
if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
String lowpath = path.toLowerCase();
boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
(!ringtones && !notifications && !alarms && !podcasts);
if (!MediaFile.isImageFileType(mFileType)) {
processFile(path, mimeType, this);
}
result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
}
} catch (RemoteException e) {
Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
}
// long t2 = System.currentTimeMillis();
// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1));
return result;
}
此函式裡面又有三個比較重要的函式beginFile processFile endFile先看beginFile STEP 12
public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) {
. . .
}
構建一個FileCacheEntry物件,儲存檔案的一些基本資訊,並且放入mFileCache HashMap中。
根據此檔案是否修改來覺得是否processFile ,又進入到native中
在此插入一段程式碼
static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
MediaScanner *mp = NULL;
char value[PROPERTY_VALUE_MAX];
if (property_get("media.framework.option", value, NULL) && (!strcmp(value, "1"))){
#ifndef NO_OPENCORE
mp = new PVMediaScanner();
#else
mp = new StagefrightMediaScanner;
#endif
}else{
mp = new StagefrightMediaScanner;
}
if (mp == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
env->SetIntField(thiz, fields.context, (int)mp);
}
android2.2以上mediascanner使用StagefrightMediaScanner
STEP 13
static void
android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
if (path == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
const char *mimeTypeStr = (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
if (mimeType && mimeTypeStr == NULL) { // Out of memory
env->ReleaseStringUTFChars(path, pathStr);
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
MyMediaScannerClient myClient(env, client);
mp->processFile(pathStr, mimeTypeStr, myClient);
env->ReleaseStringUTFChars(path, pathStr);
if (mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
不再累述,直接進入 STEP 14
status_t StagefrightMediaScanner::processFile(
const char *path, const char *mimeType,
MediaScannerClient &client) {
. . .
}
由於StagefrightMediaScanner又進入到了stagefright 框架,比較複雜,鑑於篇幅限制,在下一篇blog中繼續分析STEP 14