1. 程式人生 > >Android 深入瞭解相簿內部 二

Android 深入瞭解相簿內部 二

      通過上篇部落格我們知道了是系統對外暴露出來的ContentProvider來獲取資料庫中的圖片資訊的,使我們知道了如何去實現一個簡單的相簿了,而不是僅僅去跳轉到系統中去做處理了,這麼方便的操作極大的滿足了我們平常的開發的一些特殊的需求。但是我們在實現完成功能之後我們更多的是要去了解其內部的原理以及是如何執行操作的這樣子才能更好的有助於我們水平的提高,同時閱讀別人優秀的程式碼也是對自己的一種提高。

    我們主要是使用系統uri(MediaStore.Images.Media.EXTERNAL_CONTENT_URI)來獲取系統圖片資訊


public static final
String AUTHORITY = "media"; private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/"; /** * The content:// style URI for the "primary" external storage * volume. */ public static final Uri EXTERNAL_CONTENT_URI = getContentUri("external"); /** * Get the content:// style URI for the image media table on the * given volume. * * @param
volumeName the name of the volume to get the URI for * @return the URI to the image media table on the given volume */
public static Uri getContentUri(String volumeName) { return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName + "/images/media"); }

      根據原始碼以及程式碼註釋我們 EXTERNAL_CONTENT_URI獲取的是第一張SD卡的檔案裡面的 image 資訊,其 AUTHORITY 為 media,因為我們是通過呼叫ContentProvider去獲取資料庫的資訊,那麼系統程式碼的 AndroidManifest.xml必然會註冊一個這樣子的provider的,於是乎我們就可以通過使用Source Insight去搜尋整個Android的程式碼,同時我們還可以過濾只搜尋.xml的檔案中是否存在 android:authorities=”media” 的就行了。最後發現整個該ContentProvider路徑為: /packages/providers/MediaProvider/

。MediaProvider則是我們要查詢的Provider。

程式碼分析

Uri路徑匹配

   &nbsp通過上面的分析我們知道獲取圖片資訊是通過呼叫系統的ContentProvider的來獲取系統資料庫中儲存的圖片的資訊了。平時我們在寫ContentProvider的時候一般都會寫靜態程式碼塊用於儲存不同Uri的路徑和資訊的,以後我們就會用該類去匹配各種不同的Uri了。上篇文章的時候我們已經介紹了獲取圖片的uri地址為:content://media/external/images/media,接著我們看看MediaProvider類中靜態程式碼Uri資訊匹配。

static
    {
        //匹配的是所有的圖片的所有資訊,也就是我們本次的目標
        URI_MATCHER.addURI("media", "*/images/media", IMAGES_MEDIA);
        //根據Image的id來獲取對應的圖片資訊
        URI_MATCHER.addURI("media", "*/images/media/#", IMAGES_MEDIA_ID);

        .....
        //獲取音訊檔案的所有資訊
        URI_MATCHER.addURI("media", "*/audio/media", AUDIO_MEDIA);
        //根據所給的音訊檔案的編號獲取音訊的資訊
        URI_MATCHER.addURI("media", "*/audio/media/#", AUDIO_MEDIA_ID);
        //獲取音訊檔案的藝術家資訊
        URI_MATCHER.addURI("media", "*/audio/artists", AUDIO_ARTISTS);
        URI_MATCHER.addURI("media", "*/audio/artists/#", AUDIO_ARTISTS_ID);
        //獲取視訊檔案的所有資訊
        URI_MATCHER.addURI("media", "*/video/media", VIDEO_MEDIA);
        //根據視訊檔案的編號獲取其資訊
        URI_MATCHER.addURI("media", "*/video/media/#", VIDEO_MEDIA_ID);
        ....

        // Used by MTP implementation
        URI_MATCHER.addURI("media", "*/file", FILES);
        URI_MATCHER.addURI("media", "*/file/#", FILES_ID);

        ......
    }

    從上面的程式碼中我們可以很快的發現圖片的路徑匹配的code是IMAGES_MEDIA,所以我們在通過Provider查詢圖片資訊的時候就知道是其匹配的Code則是 IMAGES_MEDIA。通過上面的靜態程式碼塊的分析我們很快的就知道了我們要分析的uri的code,下面我們就分析query函式:

query 函式的分析
    通過靜態程式碼塊我們根據Uri分析出了對應的code之後,下面我們就可以通過查詢方法去查詢對應的資料庫了,因為在查詢的時候是需要匹配Uri的。

public Cursor query(Uri uri, String[] projectionIn, String selection,
                                                    String[] selectionArgs, String sort) {

    uri = safeUncanonicalize(uri);

    int table = URI_MATCHER.match(uri);
    List<String> prependArgs = new ArrayList<String>();
    /**
     * 這裡是根據對應的Uri來獲取對應的DatabaseHelper,有了該類之後我們就可以獲取DataBase了
     * 所以getDatabaseForUri()就是一個非常重要的方法。因為會根據不同的uri獲取不同DataBaseHelper。
     */
    DatabaseHelper helper = getDatabaseForUri(uri);
    if (helper == null) {
        return null;
    }
    helper.mNumQueries++;
    SQLiteDatabase db = helper.getReadableDatabase();
    if (db == null) return null;
    SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

    ........

    switch (table) {
        //這個就是我們之前匹配成功的uri對應的code。
        case IMAGES_MEDIA:
            qb.setTables("images");
            if (uri.getQueryParameter("distinct") != null)
                qb.setDistinct(true);

            // set the project map so that data dir is prepended to _data.
            //qb.setProjectionMap(mImagesProjectionMap, true);
            break;
            .......
    }
    .......
    //這裡我們通過SQLiteQueryBuilder以及引數db去查詢具體的資訊
    Cursor c = qb.query(db, projectionIn, selection,
                combine(prependArgs, selectionArgs), groupBy, null, sort, limit);

    if (c != null) {
        String nonotify = uri.getQueryParameter("nonotify");
        if (nonotify == null || !nonotify.equals("1")) {
            c.setNotificationUri(getContext().getContentResolver(), uri);
        }
    }

    return c;
}

/**
 * Looks up the database based on the given URI.
 * @param uri The requested URI
 * @returns the database for the given URI
 */
private DatabaseHelper getDatabaseForUri(Uri uri) {
    synchronized (mDatabases) {
        if (uri.getPathSegments().size() >= 1) {
            return mDatabases.get(uri.getPathSegments().get(0));
        }
    }
    return null;
}

    我們之前圖片的Uri為content://media/external/images/media通過呼叫getPathSegments()返回的則是一個List,比如:”external”,”images”,”media”,具體不清楚的則需要詳細的看看Uri的規則與協議的。mDatabases是一個HashMap

@Override
public boolean onCreate() {
    mDatabases = new HashMap<String, DatabaseHelper>();
    attachVolume(INTERNAL_VOLUME);

    .....

    StorageManager storageManager =
            (StorageManager)context.getSystemService(Context.STORAGE_SERVICE);
    mExternalStoragePaths = storageManager.getVolumePaths();

    // open external database if external storage is mounted
    String state = Environment.getExternalStorageState();
    //首先這裡會判斷SD卡是否已經掛載或者是是否可讀
    if (Environment.MEDIA_MOUNTED.equals(state) ||
            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        attachVolume(EXTERNAL_VOLUME);
    }
    ...
 }

    在ContentProvider的onCreate()方法中我們可以很清楚的知道了attachVolume()就是建立或者開啟資料庫的核心方法,從這裡我們可以看出這裡將會建立兩個資料庫:internal.db和external,通過我們之前的獲取getPathSegments(uri)可以很明確的知道了圖片的資訊都是存放在資料庫external.db,該資料庫主要是用來存放外接SD卡的內容的,internal.db主要是用於存放手機內部儲存卡的資訊

/**
 * Attach the database for a volume (internal or external).
 * Does nothing if the volume is already attached, otherwise
 * checks the volume ID and sets up the corresponding database.
 *
 * @param volume to attach, either {@link #INTERNAL_VOLUME} or {@link #EXTERNAL_VOLUME}.
 * @return the content URI of the attached volume.
 */
private Uri attachVolume(String volume) {

    ......

    synchronized (mDatabases) {//這裡表示mDatabases已經儲存了DataBaseHelper則返回
        if (mDatabases.get(volume) != null) {  // Already attached
            return Uri.parse("content://media/" + volume);
        }

        Context context = getContext();
        DatabaseHelper helper;
        //建立internal 資料庫
        if (INTERNAL_VOLUME.equals(volume)) {
            helper = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true,
                    false, mObjectRemovedCallback);
        } else if (EXTERNAL_VOLUME.equals(volume)) {
            //首先這裡判斷SD是否可以被移除
            if (Environment.isExternalStorageRemovable()) {
                //獲取SD卡的VolumeId編號
                String path = mExternalStoragePaths[0];
                int volumeID = FileUtils.getFatVolumeId(path);
                if (LOCAL_LOGV) Log.v(TAG, path + " volume ID: " + volumeID);

                if (volumeID == -1) {
                    String state = Environment.getExternalStorageState();
                    if (Environment.MEDIA_MOUNTED.equals(state) ||
                            Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
                        Log.e(TAG, "Can't obtain external volume ID even though it's mounted.");
                    } else {
                        Log.i(TAG, "External volume is not (yet) mounted, cannot attach.");
                    }
                    throw new IllegalArgumentException("Can't obtain external volume ID for " +
                            volume + " volume.");
                }

                // generate database name based on volume ID
                String dbName = "external-" + Integer.toHexString(volumeID) + ".db";
                helper = new DatabaseHelper(context, dbName, false,
                        false, mObjectRemovedCallback);
                mVolumeId = volumeID;
            } else {
                // external database name should be EXTERNAL_DATABASE_NAME
                // however earlier releases used the external-XXXXXXXX.db naming
                // for devices without removable storage, and in that case we need to convert
                // to this new convention
                File dbFile = context.getDatabasePath(EXTERNAL_DATABASE_NAME);
                //這裡是判斷安裝包下是否存在了該資料庫檔案
                if (!dbFile.exists()) {
                    // find the most recent external database and rename it to
                    // EXTERNAL_DATABASE_NAME, and delete any other older
                    // external database files
                    File recentDbFile = null;
                    for (String database : context.databaseList()) {
                        if (database.startsWith("external-") && database.endsWith(".db")) {
                            File file = context.getDatabasePath(database);
                            if (recentDbFile == null) {
                                recentDbFile = file;
                            } else if (file.lastModified() > recentDbFile.lastModified()) {
                                context.deleteDatabase(recentDbFile.getName());
                                recentDbFile = file;
                            } else {
                                context.deleteDatabase(file.getName());
                            }
                        }
                    }
                    if (recentDbFile != null) {
                        if (recentDbFile.renameTo(dbFile)) {
                            Log.d(TAG, "renamed database " + recentDbFile.getName() +
                                    " to " + EXTERNAL_DATABASE_NAME);
                        } else {
                            Log.e(TAG, "Failed to rename database " + recentDbFile.getName() +
                                    " to " + EXTERNAL_DATABASE_NAME);
                            // This shouldn't happen, but if it does, continue using
                            // the file under its old name
                            dbFile = recentDbFile;
                        }
                    }
                    // else DatabaseHelper will create one named EXTERNAL_DATABASE_NAME
                }
                helper = new DatabaseHelper(context, dbFile.getName(), false,
                        false, mObjectRemovedCallback);
            }
        } else {
            throw new IllegalArgumentException("There is no volume named " + volume);
        }

        //將建立的資料庫儲存到HashMap<String, DataBasehelper>中
        mDatabases.put(volume, helper);

        .......
    }

    return Uri.parse("content://media/" + volume);
}

    上面就是建立internal.db和external.db資料庫的一些邏輯,具體的邏輯也不是很難的,這裡我就不一一的進行的分析了,我們最關鍵的就是需要查詢圖片的資訊是儲存在哪裡。以及後面我們所說的那些什麼視訊檔案,音訊檔案以及壓縮包檔案等等資訊是如何查詢的。通過對上面的認識我們可以很清楚的知道了圖片資訊都是儲存在包名為com.android.providers.media的安裝包的安裝目錄下的。該應用程式的名字叫Media Storage,只是它沒有主介面的,只有ContentProvider以及Service以及Receiver而已的。主要的作用就是去管理SD卡中的檔案,並將這些檔案的資訊進行分配檢索等等。

    上面我們知道了系統中有一個專門的程式在管理著手機中的檔案資訊,下面我們就adb shell進入我們的手機裡面看看具體的內容(由於進入的目錄是/data/data目錄下,所以手機是需要root許可權才可以看到的)。

image_1bomne7fr8f8fge11qoi72gio9.png-14.3kB

    通過實踐我們證實了我們之前的猜想是正確的,下面我們就把external.db匯出到電腦上並且用SQLiteExpertPers開啟看看裡面的東西。其實我們可以從程式碼中去檢視具體的表的資訊,但是也可以根據資料庫中的表來猜測其具體的功能。

image_1bomo3g8ffep1teac1t1aul86bm.png-78kB

    由於我手機上的檔案比較多所以資料庫的大小最後都有90MB了,開啟檔案之後我們可以看到有很多的table,具體的資訊我這裡就不做介紹了,其實這些表的結構以及資訊都可以在MediaProvider的內部類DatabaseHelper中看到的。

image_1bomoh3a3ohp1u55lip1d556ol13.png-58.2kB
image_1bomohe71nhu11qo161aidm16gg1g.png-52.7kB

    在上篇部落格的時候我們介紹了查詢圖片資訊的時候用到了一些查詢條件一些內容,通過表中我們就可以很快的知道了這些東西背後的實際情況,_data就是圖片儲存的路徑,_size就是圖片的大小(不過個人不怎麼喜歡用這個大小的,還是喜歡通過實際的檔案去獲取的),_mime_type就是檔案的型別,有些可能是image/png的,_display_name就是圖片的檔名字改名字是截圖_data路徑的後面的那個名字,date_added則是圖片新增的時間,date_modified則是圖片的修改時間等等;下面我就不一一的例舉了,因為我們知道了資料庫的路徑以及它內部的一些實現等等,具體的分析和深入研究要看具體的實際需求了。

image_1bomou721tqe4vh16311991nfn1t.png-152.7kB

    在資料庫的files表中我們可以看到手機SD卡上所有檔案的資訊,在我手機上就有大概210w條記錄,注意:雖然資料庫裡有這麼多的記錄,但是手機可能沒有這麼多的檔案有些檔案可能在刪除的時候並沒有馬上的通知系統(也就是app)更新資料庫,還有一些則是比較深的目錄系統則不會去掃描的,系統只會去掃描一些特定的目錄,或者是我們主動告訴系統我們的檔案存放的路徑,如果我們在刪除檔案或者是更新檔案的時候去告訴系統的話,則系統監測到了則會馬上更新資料庫檔案的

    本文主要是講解Media模組的MediaProvider,通過一個簡單的獲取檔案圖片的資訊來找出其背後的原理,其實我們獲取的圖片資訊只是Media模組的一個非常小的部分的,它還有音訊、視訊、壓縮包、日誌等等其他的操作以及檔案搜尋、刪除、更新等等。其實本文只是簡單的介紹了一下Media的MediaProvider也就是檔案儲存的結構的介紹,詳細的一些細節則需要個人根據具體的業務深入的去分析了。Media Storage的所有操作都是在packages/providers/MediaProvider/目錄下的,我們平時可以如果想對這個方面有更多瞭解的話就可以直接檢視該目錄下的原始碼的,同時你也要更加的關注android.provider.MediaStore,因為大部分有關檔案的uri都是在這個類裡面,獲取手機檔案中的資訊的話,我們只能通過系統暴露給我們的ContentProvider介面去查詢的。

總結

    本次文章我們總結了檔案資訊的存放路徑以及一些簡單的程式碼,當我們閱讀了這些程式碼之後發現其實也沒有我們想象中的這麼難了,我們之後就可以更加深入的瞭解了這個裡面的原理以及操作,同時在檢視原始碼的時候我們也可以學到別人寫程式碼的優點和長處了。其實我們在看程式碼的時候更多的時候是看人家的思想以及思考人家為什麼這麼設計,如果是我來做的話我會怎麼去設計這個系統呢?會去怎麼做這件事情呢?所以設計的思想是非常關鍵,通過深入看了程式碼之後我們發現其實我們也可以寫出來的。下篇文章我們將會分析檔案新增和刪除的一些通知以及系統是如何掃描檔案的。