[深入理解Android卷一全文-第十章]深入理解MediaScanner

阿拉神農發表於2015-08-02

由於《深入理解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);

//掃描工作是一個漫長的工程,所以這裡單獨建立一個工作執行緒,執行緒函式就是

//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中通過一段比較詳細的註釋,對整個流程做了文字總結,這段總結非常簡單,這裡就不翻譯了。

[-->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,由於它非常簡單,所以這裡就不再進行流程的分析了。

[-->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的使用方法,另外,提出了幾個和媒體掃描相關的問題請讀者與我們共同思考。

 

相關文章