Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 高級開發 >> Android MediaScanner 詳盡分析

Android MediaScanner 詳盡分析

編輯:高級開發

MediaScanner分析

  一 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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved