輕讀一下 Android 應用開發中的 assets 目錄


2019-08-07

關鍵字:APK預置文件、預置配置文件、res,raw與assets的區別


 

在Android的應用開發中,難免會遇到外部文件的預置需求。例如圖像、音視頻、配置文件、字體等等。對於圖像,我們很容易會想到將它們存放在 res/drawable 目錄或者是 res/mipmap 目錄下。但對於其它類型的文件,就得另尋它法了。

 

比較常見的可以保存任意類型文件的地方主要有兩個:

1、res/raw 目錄;

2、assets 目錄。

 

drawable/mipmap、raw、assets 三者都可以用來存儲一些外部資源文件,那它們之間各自有什么優缺點呢?

 

首先,在 drawable 或 mipmap 下只能保存圖像文件或圖像描述文件,但在這兩個目錄下保存的圖像文件在編譯時會建立一張“索引表”。這個索引信息會被統一保存在一個名稱為 R.java 的文件中。在程序的任何地方都可以直接通過 R.drawable.xxx 的形式來使用圖片資源。

 

res/raw 目錄算是一個比較特殊的資源文件目錄。它被設計用於保存一些二進制文件,即在這個目錄下的所有文件都會被記錄到“索引表”中,但是編譯系統不會去動里面的文件。raw 目錄下的文件放進去時是什么樣的,編譯成 APK 以后還是什么樣。這個目錄比較適合保存一些音視頻等二進制文件。

 

res 目錄下的資源文件夾,不管是 raw 還是 drawable 或 mipmap,都不能自由地設計子目錄層級關系。不管你有多少文件,都只能放在同一級目錄中。

 

assets 目錄是一個非常自由的目錄。它就像是Android應用中的“三不管”地帶,不會為里面的文件建立索引、不會限制目錄層級關系、不會處理里面的文件。如果你想更好地管理自己的外部資源文件,建議使用 assets 目錄。

 

本篇文章,我們重點來講解一下 Android 的 assets 目錄。

 

1、assets 簡介

 

assets 在 APK 工程中就是一個普通的目錄而已。不管是 Eclipse 創建的工程還是 Android Studio 創建的工程,在它的工程根目錄下都可以發現(或者自己創建)一個 assets 目錄,如下圖所示

assets 目錄是專門用於保存各種外部文件的。常見的有:圖像、音視頻、配置文件、字體、自帶數據庫等。之所以說它適合用來管理這些文件,是因為應用程序在編譯時不會去處理這個目錄下的文件,但是卻會將它們打包進 APK 中。而其它你隨便創建的目錄在編譯時就會被直接忽略掉。同時,你可以在 assets 目錄內任意創建目錄層級關系,這對於有大量外部文件需要集成的應用來說,就能很方便地分類管理了。

 

在 APK 開發中,有一種管理配置信息的做法比較常見:直接將配置信息文件放入 assets 目錄中管理,程序首次運行時將這里面的配置信息拷貝到外部的可操作的目錄下,后續程序的運行均靠這份保存在外部的配置信息為准,assets 中的信息僅作為原始配置信息的備份。

 

但是,assets 目錄在使用上也還是有一點小缺憾的。

 

assets 目錄內的文件在程序打包發布以后就是只讀的。就是你只能讀取那里面的文件,而無法修改或增加文件。這條特性其實也可以理解,因為應用程序一旦打包發布了,它就應該是只讀的。而 assets 目錄又是直接保存在 APK 內部的,所以它自然也不能修改或增加內容了。實在要想增加內容,通過 Database 或者 SharedPreferences 往 /data/data 目錄下保存就好了嘛。再或者這兩者不能滿足你的要求,你也可以直接將它們保存在 sdcard 下面嘛。反正現在市面上的 APK  在 sdcard 里創建自己的數據文件夾的可不少。

 

2、assets 開發

 

關於讀取 assets 目錄下的文件,Android 提供了一個 android.content.res.AssetManager 類來實現。這個類的簽名體如下圖所示

這里我們需要關注的方法有以下幾個:

1、構造方法

2、open() 方法

3、openFd() 方法

4、openNonAssetFd() 方法

5、openXmlResourceParser() 方法

 

構造方法

這里我們注意到 AssetManager 的構造方法的權限是 default,這意味着我們無法在我們的程序中通過 new 的方式來實例化它(在Android4.4 中它是 public 修飾的)。通常,我們可以通過兩種方式來得到 AssetManager 的實例:1、通過 Context 實例的 getAssets() 方法;2、通過 Resources 實例的 getAssets() 方法

 

open(string) & open(string,int)

這兩個方法的作用是一樣的。都是將 assets 目錄下的某個文件封裝成 InputStream 的形式以供使用。說白了就是讓我們讀文件用的。

 

兩個方法中的 string 參數都指的是“文件名”,其實應該說是文件的相對路徑更合適,它需要的是某個文件在 assets 目錄下的相對路徑。例如:dir1/file1.png , dir2/dir3/dir4/file2.avi。

 

第二個方法中還有一個 int 型參數,它是“訪問模式”,就是將 assets 目錄下的文件以什么模式來打開的意思。它一共有以下 4 種模式可供選擇:

1、ACCESS_UNKNOW

無模式。其代表的值是 0。

2、ACCESS_RANDOM

這個不應該翻譯成隨機訪問模式,無序訪問模式會更適合一點。這種模式下文件的訪問只會打開其中一段內容,然后再根據你的需要向流的前方或后方移動讀取指針。其代表的值是 1。

3、ACCESS_STREAMING

順序讀取模式。文件將會被從頭部打開,然后按順序向后面移動讀取數據。其代表的值是 2。

4、ACCESS_BUFFER 

緩存讀取模式。讀取時會將整個文件直接讀取到內存中,這種模式適合小文件的讀取。其代表的值是 3。

 

在 open(string) 中,它使用的文件讀取模式是 ACCESS_STREAMING 模式。

 

openFd(string)

將 assets 目錄中的文件以 FileDescriptor 的形式打開,返回一個 AssetFileDescriptor 實例。

 

openNonAssetFd(string) & openNonAssetFd(int,string)

這個其實和上面的 openFd() 是一樣的。只不過它是跳出了 assets 目錄的范圍限定,它是站在工程根目錄的視角來打開文件的 FileDescriptor 的。換句話說,它允許打開 APK 中任意位置的文件的 AssetFileDescriptor 實例。

 

openXmlResourceParser(int,string)

打開 assets 目錄下的 xml 形式的文件,直接返回 XmlResourceParser 實例。其實就是官方替我們做了從 InputStream 到 XML 解析器之間的轉換,有助於增加一些開發效率而已。

 

 

那接下來,我們通過實例來演示一下 assets 目錄下的文件的讀取方法。首先第一個是直接讀取最普通的文件的方法

try {
    InputStream is = this.getAssets().open("moutain.png");
    Log.d("type1", "File available:" + is.available());

    InputStream is2 = this.getResources().getAssets().open("river.png");
    Log.d("type1", "File available2:" + is2.available());
} catch (IOException e) {
    e.printStackTrace();
}

執行的結果如下所示:

 D/type1: File available:3
 D/type1: File available2:11416

當然,最后一定不要忘記將用完的 InputStream 資源關掉!!!

 

第二個是讀取自定義目錄層級的文件的方法

try {
    InputStream is = this.getAssets().open("sences/nature/forest.png");
    Log.d("type2", "File available:" + is.available());

} catch (IOException e) {
    e.printStackTrace();
}

執行的結果如下所示:

 D/type2: File available:32114

同樣,不要忘記調用 InputStream 的 close() 方法哦。

 

第三個是以文件描述符形式讀取的方法

try {
    AssetFileDescriptor afd = this.getAssets().openFd("river.png");
    Log.d("type3", "File available:" + afd.getLength());

    InputStream is = afd.createInputStream();
    Bitmap bm = BitmapFactory.decodeStream(is);

    is.close();
    afd.close();

    iv01.setImageBitmap(bm);
} catch (IOException e) {
    e.printStackTrace();
}

這里將一張圖片以 AssetFileDescriptor 的形式讀取出來,並轉換成 Bitmap 顯示在 ImageView 上,這段代碼的執行結果如下圖所示

 

 

3、assets 深度剖析

 

這一小節我們來探究一下 AssetManager 的“前世今生”。

 

首先來看看 AssetManager 實例是怎么來的。通過前面的介紹,我們已經知道了可以直接通過 Activity 實例來調用 getAssets() 方法以取得 AssetManager 實例,或者是通過 Activity 實例里的 getResources() 得到 Resources 的實例以后再調用 getAssets() 來得到 AssetManager 實例。那我們應該都知道,所謂的 Activity 里提供的 getAssets() 方法或者是 getResources() 方法,都是被定義在 android.app.Context 類中的方法,如下圖所示

但是它們在 Context 類中都是抽象方法。那這兩個方法的具體實現在哪呢?在 android.app.ContextImpl 類上。如下圖所示

通過上圖,我們很意外地發現,原來通過 Context 的 getAssets() 和通過 Resources 的 getAssets() 走的路線竟然是完全一樣的!!!

 

好吧,來不及在這感慨了,我們得接着看看 mResources 又是怎么來的。

這個 mResource 是通過 packageInfo 得到的。而 packageInfo 又是在 ContextImpl 構造的時候傳進來的。

我們這里且不管是誰 new 了 ContextImpl 的對象,我們只需要知道這個 packageInfo 是 LoadedApk 類的實例就好了。我們下面直接去看看 LoadedApk 類里的 getResources() 方法。

好嘛,又跳到 ActivityThread 類里去了,跟過去

牛皮,又跳到 ResourcesManager 里去了,沒辦法,只能再跟。不過在跟蹤之前這里必須說一下,這個方法中的第一個參數 resDir 指的是這個 APK 在文件系統下的路徑,例如,你是通過 install 方式安裝的,它就會在 /data/app 下,你是系統應用它就會在 /system/app 或 /system/priv-app 下。總之這個參數的作用就是把當前正在運行的 APK 的地址傳進去,因為后面要去解析它里面的資源的。

 

ResourcesManager 里的 getTopLevelResources 方法比較長,這里只貼需要我們關注的代碼。我們在這個方法里終於發現有人實例化了 AssetManager 類。

同時下面一點的地方還發現了這樣一段代碼

將剛創建出來的 AssetManager 實例傳給 Resources 類實例化去了。傳它的作用想也不用想就知道是將這個 assets 對象保存起來,以便后面調用 Resources 類的 getAssets() 方法時好返回了。而事實上,Resources 類中的 getAssets() 方法所干的事也確實就是簡單地返回這個 assets 對象的引用而已。下圖是 Resources 類中的代碼實現

 

前面我們簡單地了解了 AssetManager 的由來過程。簡單總結一下就是:

1、每個 APK 在啟動時都會實例化屬於自己的 Context 對象;

2、APK 里的資源文件統一具化為 Resources 實例,並由 ResourceManager 管理;

3、assets 目錄統一由一個 AssetManager 實例管理;

4、AssetManager 實例在 APK 啟動時創建;

 

 

上面是 AssetManager 在應用層的流程。當然其實 AssetManager 還有一個在 Java 以下的流程,這個流程挺復雜的,我這邊也不是很有時間,而且感覺了解的太過深入的價值不是很高,就不再繼續分析了。不過不排除以后我有興趣時會繼續跟蹤下去。

 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM