編輯:高級開發
一 MediaScannerService
多媒體掃描是從MediaScannerService開始的。這是一個單獨的package。位於
packagesprovidersMediaProvider:含以下Java文件
l MediaProvider.Java
l MediaScannerReceiver.Java
l MediaScannerService.Java
l MediaThumbRequest.Java
分析這個目錄的android.mk文件,發現它運行的進程名字就是android.process.media。
application android:process=android.process.media
1.1 MediaScannerReceiver
這個類從BroadcastReceiver中派生,用來接收任務的。
MediaScannerReceiver extends BroadcastReceiver
在它重載的onRecIEve函數內有以下幾種走向:
if (action.equals(Intent.ACTION_BOOT_COMPLETED)) {
// 收到”啟動完畢“廣播後,掃描內部存儲
scan(context, MediaProvider.INTERNAL_VOLUME);
} else {
……….
if (action.equals(Intent.ACTION_MEDIA_MOUNTED) &&
externalStoragePath.equals(path)) {
/收到MOUNT信息後,掃描外部存儲
scan(context, MediaProvider.EXTERNAL_VOLUME);
}
else if (action.equals(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) &&
path != null && path.startsWith(externalStoragePath + "/")) {
//收到請求要求掃描某個文件,注意不會掃描內部存儲上的文件
scanFile(context, path);
…………………………..
}
……下面是它調用的scan函數:
scan(Context context, String volume)
Bundle args = new Bundle();
args.putString("volume", volume);
//直接啟動MediaScannerService了,
context.startService(
new Intent(context, MediaScannerService.class).putExtras(args));
總結:
MediaScannerReceiver是用來接收任務的,它收到廣播後,會啟動MediaService進行掃描工作。
下面看看MediaScannerService.
1.2 MediaScannerService
接上頁
MSS標准的從Service中派生下來,
MediaScannerService extends Service implements Runnable
//注意:是一個Runnable…,可能有線程之類的東西存在
下面從Service的生命周期的角度來看看它的工作。
1. onCreate
public void onCreate()
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
//獲得電源鎖,防止在掃描過程中休眠
//單獨搞一個線程去跑掃描工作,防止ANR
Thread thr = new Thread(null, this, "MediaScannerService");
thr.start();
2. onStartCommand
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
//注意這個handler,是在另外一個線程中創建的,往這個handler裡sendMessage
//都會在那個線程裡邊處理
//不明白的可以去查看handler和Looper機制
//這裡就是同步機制,等待mServiceHandler在另外那個線程創建完畢
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();
//把MediaScannerReceiver發出的消息傳遞到另外那個線程去處理。
mServiceHandler.sendMessage(msg);
………….
基本上MSR(MediaScannerReceiver)發出的請求都會傳到onStartCommand中處理。如果有多個存儲的話,也只能一個一個掃描了。
下面看看那個線程的主函數
3. 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和handler的實現
Looper.prepare();//把這個looper對象設置到線程本地存儲
mServiceLooper = Looper.myLooper();
mServiceHandler = new ServiceHandler();//創建handler,默認會把這個looper
//的消息隊列賦值給handler的消息隊列,這樣往handler中發送消息就是往這個線程的looper發
Looper.loop();//消息循環,內部會處理消息隊列中的消息
//也就是handleMessage函數
}
上面handler中加入了一個掃描請求(假設是外部存儲的),所以要分析handleMessage函數。
4. handleMessage
private final class ServiceHandler extends Handler
{
@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)) {
//是掃描內部存儲的請求?
// scan internal media storage
directorIEs = new String[] {
Environment.getRootDirectory() + "/media",
};
}
else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
//是掃描外部存儲的請求?獲取外部存儲的路徑
directorIEs = new String[] {
Environment.getExternalStorageDirectory().getPath(),
};
}
if (directorIEs != null) {
//真正的掃描開始了,上面只不過是把存儲路徑取出來罷了.
scan(directorIEs, volume);
…..
//掃描完了,就把service停止了
stopSelf(msg.arg1);
}
};
5. scan函數
接上頁
private void scan(String[] directorIEs, String volumeName) {
mWakeLock.acquire();
//下面這三句話很深奧…
//從 getContentResolver獲得一個ContentResover,然後直接插入
//根據AIDL,這個ContentResover的另一端是MediaProvider。只要去看看它的
//insert函數就可以了
//反正這裡知道獲得了一個掃描URI即可。
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();
//交給掃描器去掃描文件夾 scanDirectorIEs
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();
}
說說上面那個深奧的地方,在MediaProvider中重載了insert函數,insert函數會調用insertInternal函數。
如下:
private Uri insertInternal(Uri uri, ContentValues initialValues) {
long rowId;
int match = URI_MATCHER.match(uri);
// handle MEDIA_SCANNER before calling getDatabaseForUri()
//剛才那個insert只會走下面這個分支,其實就是獲得一個地址….
//太繞了!!!!!
if (match == MEDIA_SCANNER) {
mMediaScannerVolume =
接上頁
initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);return MediaStore.getMediaScannerUri();
}
……..
再看看它創建了什麼樣的Scanner,這就是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的工作了。
6. 總結
MSS的工作流程如下:
l 1 單獨啟動一個帶消息循環的工作線程。
l 2 主線程接收系統發來的任務,然後發送給工作線程去處理。
l 3 工作線程接收任務,創建一個MediaScanner去掃描。
l 4 MSS順帶廣播一下掃描工作啟動了,掃描工作完畢了。
二 MediaScanner
MediaScanner位置在
frameworksasemedia下,包括jni和Java文件。
先看看Java實現。
這個類巨復雜,而且和MediaProvider交互頻繁。在分析的時候要時刻回到MediaProvider去看看。
1. 初始化
public class MediaScanner
{
static {
//libmedia_jni.so的加載是在MediaScanner類中完成的
//這麼重要的so為何放在如此不起眼的地方加載???
System.loadLibrary("media_jni");
native_init();
}
public MediaScanner(Context c) {
native_setup();//調用jni層的初始化,暫時不用看了,無非就是一些
接上頁
//初始化工作,待會在再進去看看
……..
}
剛才MSS中是調用scanDirectorIEs函數,我們看看這個。
2. scanDirectorIEs
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++) {
//掃描文件夾,這裡有一個很重要的參數 mClIEnt
// processDirectory是一個native函數
processDirectory(directories[i], MediaFile.sFileExtensions, mClIEnt);
}
long scan = System.currentTimeMillis();
postscan(directorIEs);//掃描後處理
long end = System.currentTimeMillis();
…..打印時間,異常處理…沒了…
下面簡單講講initialize ,preScan和postScan都干嘛了。
private void initialize(String volumeName) {
//打開MediaProvider,獲得它的一個實例
mMediaProvider = mContext.getContentResolver().acquireProvider("media");
//得到一些uri
mAudioUri = Audio.Media.getContentUri(volumeName);
mVideoUri = Video.Media.getContentUri(volumeName);
mImagesUri = Images.Media.getContentUri(volumeName);
mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
//外部存儲的話,可以支持播放列表之類的東西,搞了一些個緩存池之類的
//如mGenreCache等
if (!volumeName.equals("internal")) {
// we only support playlists on external media
mProcessPlaylists = true;
mGenreCache = new HashMap();
…
preScan,這個函數很復雜:
大概就是創建一個FileCache,用來緩存掃描文件的一些信息,例如last_modifIEd等。這個FileCache是從MediaProvider中已有信息構建出來的,也就是歷史信息。後面根據掃描得到的新信息來對應更新歷史信息。
接上頁
postScan,這個函數做一些清除工作,例如以前有video生成了一些縮略圖,現在video文件被干掉了,則對應的縮略圖也要被干掉。
另外還有一個mClient,這個是從MediaScannerClIEnt派生下來的一個東西,裡邊保存了一個文件的一些信息。後續再分析。
剛才說到,具體掃描工作是在processDirectory函數中完成的。這個是一個native函數。
在frameworksasemediajniandroid_media_MediaScanner.cpp中。
三 MediaScanner JNI層分析
MediaScanner JNI層內容比較多,單獨搞一節分析吧。
先看看android_media_MediaScanner這個文件。
1. native_init函數,jni對應的函數如下
static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaScanner");
//得都Java類中mNativeContext這個成員id
fields.context = env->GetFIEldID(clazz, "mNativeContext", "I");
//不熟悉JNI的自己去學習下吧
}
3. native_setup函數,jni對應函數如下:
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
//創建MediaScanner對象
MediaScanner *mp = createMediaScanner();
//太變態了,自己不保存這個對象指針.
//卻把它設置到Java對象的mNativeContext去保存
env->SetIntField(thiz, fIElds.context, (int)mp);
}
//創建MediaScanner函數
static MediaScanner *createMediaScanner() {
#if BUILD_WITH_FULL_STAGEFRIGHT
..
//使用google自己的
return new StagefrightMediaScanner;
#endif
#ifndef NO_OPENCORE
//使用opencore提供的
….
return new PVMediaScanner();
#endif
4. processDirectorIEs函數,jni對應如下:
android_media_MediaScanner_processDirectory(JNIEnv *env, jobject thiz, jstring path, JString extensions, jobject clIEnt)
{
接上頁
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fIElds.context);
//每次都要回調到Java中去取這個Scanner!!
………
const char *pathStr = env->GetStringUTFChars(path, NULL);
const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
…….
//又在C++這裡搞一個clIEnt,然後把Java的client放到C++ClIEnt中去保存
//而且還是棧上的臨時變量..
MyMediaScannerClient myClient(env, clIEnt);
//scanner掃描文件夾,用得是C++的clIEnt
mp->processDirectory(pathStr, extensionsStr, myClIEnt, ExceptionCheck, env);
env->ReleaseStringUTFChars(path, pathStr);
env->ReleaseStringUTFChars(extensions, extensionsStr);
}
到這裡似乎就沒有了,那麼掃描後的數據庫是怎麼更新的呢?為什麼要傳入一個clIEnt進去呢?看來必須得trace到scanner中去才知道了。
四 PVMediaScanner
這個在externalopencoreandroidmediascanner.cpp中。
1. processDirectory
status_t MediaScanner::processDirectory(const char *path, const char* extensions,
MediaScannerClient& clIEnt, ExceptionCheck exceptionCheck, void* exceptionEnv)
{
InitializeForThread();
int error = 0;
status_t result = PVMFSuccess;
….
//調用clIEnt的設置區域函數
clIEnt.setLocale(mLocale);
//掃描文件夾,咋還沒開始??
result = doProcessDirectory(pathBuffer, pathRemaining, extensions, clIEnt, exceptionCheck, exceptionEnv);
..
2. doProcessDirectory
status_t MediaScanner::doProcessDirectory(char *path, int pathRemaining, const char* extensions,
MediaScannerClient& clIEnt, ExceptionCheck exceptionCheck, void* exceptionEnv)
{
…終於看到點希望了
//打開這個文件夾,枚舉其中的內容。
//題外話,這個時候FileManager肯定刪不掉這個文件夾!!
接上頁
DIR* dir = opendir(path);
while ((entry = readdir(dir))) {
const char* name = entry->d_name;
//不處理.和..文件夾
if (isDirectory) {
……..
//不處理.開頭的文件夾。如果是文件夾,遞歸調用doProcessDirectory
//深度優先啊!
int err = doProcessDirectory(path, pathRemaining - nameLength - 1, extensions, clIEnt, exceptionCheck, exceptionEnv);
if (err) {
LOGE("Error processing '%s' - skipping", path);
continue;
}
} else if (fileMatchesExtension(path, extensions)) {
//是一個可以處理的文件,交給clIEnt處理
//徹底瘋掉了….這是干嘛呢???
clIEnt.scanFile(path, statbuf.st_mtime, statbuf.st_size);
這裡要解釋下,剛才createMediaScanner中,明明創建的是PVMediaScanner,為何這裡看得是MediaScanner代碼呢?
l 因為PVMediaScanner從MediaScanner中派生下來的,而且沒有重載processDirectory函數
l Eclaire沒有PVMediaScanner這樣的東西,估計是froyo又改了點啥吧。
FT,processDirctory無非是列舉一個目錄內容,然後又反回去調用client的scanFile處理了。為何搞這麼復雜?只有回去看看C++的clIEnt干什麼了。
3. MediaScannerClIEnt---JNI層
JNI中的這個類是這樣的:
class MyMediaScannerClient : public MediaScannerClIEnt
//這是它的scanFile實現
virtual bool scanFile(const char* path, long long lastModifIEd, long long fileSize)
{
//再次崩潰了,C++的clIEnt調用了剛才傳進去的Java ClIEnt的
//scanFile函數…不過這次還傳進去了該文件的路徑,最後修改時間和文件大小。
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModifIEd, fileSize);
…..想死,,,
}
沒辦法了,只能再去看看MediaScanner.Java傳進去的那個clIEnt了。
4. MediaScannerClIEnt----Java層
這個類在MediaScanner.Java中實現。
private class MyMediaScannerClient implements MediaScannerClIEnt:
接上頁
public void scanFile(String path, long lastModifIEd, long fileSize) {
//調用DOScanFile..很煩..
DOScanFile(path, null, lastModifIEd, fileSize, false);
…
//下面是DOScanFile
public Uri DOScanFile(String path, String mimeType, long lastModifIEd, long fileSize, boolean scanAlways) {
//預處理,看看之前創建的文件緩存中有沒有這個文件
FileCacheEntry entry = beginFile(path, mimeType, lastModifIEd, fileSize);
// rescan for metadata if file was modifIEd since last scan
if (entry != null && (entry.mLastModifIEdChanged || scanAlways)) {
//如果事先有這個文件的信息,則需要修改一些信息,如長度,最後修改時間等
…..
//真正的掃描文件
processFile(path, mimeType, this);
//掃描完了,做最後處理
endFile(entry, ringtones, notifications, alarms, music, podcasts);
processFile又是jni層的。
對應android_media_MediaScanner_processFile函數
android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, JString mimeType, jobject clIEnt)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fIElds.context);
//無語了,又搞一個 MyMediaScannerClIEnt
MyMediaScannerClient myClient(env, clIEnt);
mp->processFile(pathStr, mimeTypeStr, myClIEnt);
…}
第一次到PVMediaScanner中來了
status_t PVMediaScanner::processFile(const char *path, const char* mimeType, MediaScannerClient& clIEnt)
{
status_t result;
InitializeForThread();
//調用clIEnt的beginFile,估計是做一些啥預處理
clIEnt.setLocale(locale());
clIEnt.beginFile();
//LOGD("processFile %s mimeType: %s", path, mimeType);
const char* extension = strrchr(path, '.');
if (extension && strcasecmp(extension, ".mp3") == 0) {
接上頁
result = parseMP3(path, client);//clIEnt又傳進去了
…根據後綴名去掃描….
}
clIEnt.endFile();
到parseMP3去看看。這個在
static PVMFStatus parseMP3(const char *filename, MediaScannerClient& clIEnt)
{//這個函數太專業了,和編解碼有關,我們重點關注clIEnt在這裡干什麼了
…
//原來,解析器從文件中解析出一個信息,就調用clIEnt的addStringTag
if (!clIEnt.addStringTag("duration", buffer))
….
addStringTag在JNI的clIEnt中處理。
這個MediaScannerClIEnt是在opencore中的那個MediaScanner.cpp中實現的,而android_media_MediaScanner.cpp中的是MyMediaScannerClient,從MediaScannerClIEnt派生下來的
bool MediaScannerClIEnt::addStringTag(const char* 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;
}
}
//如果不是ASCII編碼的話,內部先保存一下這些個tag信息
//待會掃描完後再集中做一次字符串編碼轉換
if (nonAscii) {
// save the strings for later so they can be used for native encoding detection
mNames->push_back(name);
mValues->push_back(value);
return true;
}
// else fall through
}
//調用子類的handleStringTag
return handleStringTag(name, value);
}
class MyMediaScannerClient : public MediaScannerClIEnt{
//調用到子類的handleStringTag了
virtual bool handleStringTag(const char* name, const char* value)
{
//又傳遞到Java層的handleStringTag來處理
//麻木了..
mEnv->CallVoidMethod(mClIEnt, mHandleStringTagMethodID, nameStr,
接上頁
valueStr);}
Java層
MediaScannerService中的MyMediaScannerClIEnt類
public void handleStringTag(String name, String value) {
//下層掃描的文件tag信息,全部處理後賦值給Java層這個MyScannerClIEnt了
例如MP3的title,專輯名等等。
….
int num = parseSubstring(value, 0, 0);
mTrack = (num * 1000) + (mTrack % 1000);
} else if (name.equalsIgnoreCase("duration")) {
mDuration = parseSubstring(value, 0, 0);
} else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) {
mWriter = value.trim();
到這裡了,還沒寫到數據庫呢?啥時候更新數據庫?看來是在clIEnt.endFile()中了。
但是這個endClIEnt並沒有調用到Java層去。那在哪裡結束呢?
還記得JAVA中的DOScanFile函數嗎,對了,這個endFile就是在那裡直接由Java調用的。
private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications,
boolean alarms, boolean music, boolean podcasts)
throws RemoteException {
// update database
Uri tableUri;
boolean isAudio = MediaFile.isAudioFileType(mFileType);
boolean isVideo = MediaFile.isVideoFileType(mFileType);
boolean isImage = MediaFile.isImageFileType(mFileType);
….
//來了一個新文件,直接插入數據庫
result = mMediaProvider.insert(tableUri, values);
//或者更新數據庫
mMediaProvider.update(result, values, null, null);
這回真算是完了。
5.流程總結
l MediaScanner(MS)調用scanDirectorIEs中的processDirectory,進入到JNI層
l JNI調用PVMediaScanner的processDirectory
l PVMediaScanner的processDirectory為目錄下的文件調用MyMediaScannerClIEnt的scanFile
l MyMediaScannerClIEnt
Google於2007年底正式發布了android SDK, 作為 android系統的重要特性,Dalvik虛擬機也第一次進入了人們的視野。它對內存的高效使用,和在低
android作為谷歌最重要的資本之一,盡管在智能終端上的起步較蘋果晚不少,然而面對強有力的競爭對手,從不停追趕到部分超越,已為其未來戰略目標做好了充分鋪墊。迄今,全球
最近,Google面向大學生推出android開發挑戰賽,android開發成為時下開發者的熱點開發項目。像《在NetBeans上搭建android SDK環境》這樣的
android應用廣泛,應用方式靈活,可以在模擬器中進行相應修改實現許多特定的功能需求。我們在這裡就先來了解一下android創建sdcard的具體方法,從中感受一下這