Android 動態加載資源

jopen 9年前發布 | 16K 次閱讀 Android Android開發 移動開發

前不久跑去折騰高德 SDK 中的 HUD 功能,相信用過該功能的用戶都知道 HUD 界面上的導航轉向圖標是動態變化的。從高德官方導航 API 文檔中 AMapNaviGuide 類的描述可知,導航轉向圖標有23種類型。

誒,等等,23 種?那圖標應該是放在 assets 文件夾吧?總不可能是在服務器上下載吧?

看下導航 API 的 jar 包結構。

AMap_ Navi_v1.3.0_20150828.jar
  |- assets
    |- autonavi_Resource1_1_0.png
    |- custtexture*.png (7 張)
  |- com
    |- amap.api.navi
    |- autonavi
  |- META-INF

納尼?assets 上的圖片總共也只有 8 張,而且圖片的內容跟 HUD 毫無關系,莫非真的是從服務器下載資源?
用 Android Studio 打開 jar 包中的 AMapHudView.class 來看下 AMapHudView 的邏輯(AS 1.2 就引入了反編譯功能)。

...
import com.autonavi.tbt.g;
...

public class AMapHudView extends FrameLayout implements OnClickListener, OnTouchListener, e {

    static final int[] hud_imgActions = new int[]{2130837532, 2130837532, 2130837532, 2130837533, 2130837534, 2130837535, 2130837536, 2130837537, 2130837538, 2130837539, 2130837522, 2130837523, 2130837524, 2130837525, 2130837526, 2130837527, 2130837528, 2130837529, 2130837530, 2130837531};
    ...
    private ImageView roadsignimg;// 方向圖標對應的 View
    ...
    private int resId;// 方向圖標的 id,對應 hud_imgActions 的 index,根據高德的文檔,該變量值為 0-23
    ...
    private void updateHudWidgetContent() {
        ...
        if(this.roadsignimg != null && this.resId != 0 && this.resId != 1) {
            Drawable var1 = g.a().getDrawable(hud_imgActions[this.resId]);// g.a() 返回的是 Resource 對象
            this.roadsignimg.setBackgroundDrawable(var1);
            ...
        }
    }
}

先看hud_imgActions,里面的值是不是很熟悉?轉成16進制均為 0x7F02 開頭(0x7F 是應用資源,而 0x02 則是 drawable 資源)。再看updateHudWidgetContent()方法,邏輯比較簡單,通過resId獲取hud_imgActions對應的 drawable id,再通過該 id 獲取到對應的 Drawable 對象并將其設置到 ImageView 中。

看到這,可以肯定高德 SDK 最終是通過本地資源的索引獲取到 Drawable。

然而我們的 apk 中并沒有相應的資源,為什么能夠正常獲取到對應的 Drawable?我們看回上面的第12行代碼:

Drawable var1 = g.a().getDrawable(hud_imgActions[this.resId]);// g.a() 返回的是 Resource 對象

我們將注意力集中到g.a()中,找到com.autonavi.tbt.g#a()

public static Resources a() {
    if (b == null) {
        b = e.getResources();
    }
    return b;
}

其中變量e為上層傳遞進來的 Activity,而我們前面說過,我們的 apk 中并沒有相應的資源,所以將注意力放到變量b在其他地方的賦值上。
public static boolean a(Context context) {
    ...
    a = b(context.getFilesDir() + "/autonavi_Resource1_1_0.jar");
    b = a(context, a);// 變量 a 為 AssetManager
    return true;
}

private static AssetManager b(String str) {
    try {
        Class cls = Class.forName("android.content.res.AssetManager");
        AssetManager assetManager = (AssetManager) cls.getConstructor().newInstance();
        try {
            cls.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, str);
        } catch (Throwable th) {
        }
        return assetManager;
    } catch (Throwable th2) {
        return null;
    }
}

private static Resources a(Context context, AssetManager assetManager) {
    DisplayMetrics displayMetrics = new DisplayMetrics();
    displayMetrics.setToDefaults();
    return new Resources(assetManager, displayMetrics, context.getResources().getConfiguration());
}

可以看到,高德 SDK 中先通過反射實例化 AssetManager,并且調用 `addAssetPath(context.getFilesDir() + “/autonavi_Resource1_1_0.jar”),接著實例化 Resources 對象。所以事實上是通過這個新的 Resource 來獲取到對應資源的 Drawable 對象。
但是我們的 apk 對應的 files 目錄中并不存在 autonavi_Resource1_1_0.jar,這個文件又是怎么來的?
private static String k = "autonavi_Resource1_1_0.png";
...
private static boolean b(Context var0) {
    String filePath = var0.getFilesDir().getAbsolutePath() + "/autonavi_Resource1_1_0.jar";
    ...
    InputStream var1 = var0.getResources().getAssets().open(k);
    File var3 = new File(filePath);
    long var21 = var3.length();
    int var6 = var1.available();
    if(!var3.exists() || var21 != (long)var6) {
        ...
        File var22 = new File(filePath);
        FileOutputStream var2 = new FileOutputStream(var22);
        byte[] var8 = new byte[1024];

        int var9;
        while((var9 = var1.read(var8)) > 0) {
            var2.write(var8, 0, var9);
        }
    }
    ...
}

還是 com.autonavi.tbt.g 這個類,可以看到,高德是將 jar 包內 assets 目錄中的 autonavi_Resource1_1_0.png 復制到當前 apk 對應的 files 目錄中,并將新的文件命名為 autonavi_Resource1_1_0.jar。

再回到加載資源的問題上,為什么加載 autonavi_Resource1_1_0.jar 能索引資源?
因為該文件其實是 apk(高德將后綴名改成了 jar)。AssetManager 加載該 apk 后,Resource 就能通過該 AssetManager 獲取到里面的相應資源。

AssetManager 的相關知識請參考老羅的《Android應用程序資源管理器(Asset Manager)的創建過程分析》

至此,我們就可以清楚知道高德 SDK 是如何實現動態加載資源的:

  1. 將資源 apk 放置在 jar 包的 assets 目錄中;
  2. 在 View 組件初始化的過程中將 assets 中的資源 apk 復制到 files 目錄中;
  3. 接著實例化 AssetManager,調用 addAssetPath 方法加載 files 目錄中的資源 apk;
  4. 然后將 AssetManager 作為參數實例化 Resouce,最后通過 Resource 對象獲取資源apk 中相應的資源。

總結

將上述內容再簡略,動態加載資源所必需的幾個核心步驟:

  1. 實例化 AssetManager 對象,并通過反射調用 addAssetPath(String) 方法加載目標 apk(或與 apk 文件架構一致的目錄)
  2. 通過第一步得到的 AssetManager 實例化 Resource 對象
  3. 利用第二步得到的 Resource 對象來動態加載資源

這里需要注意的是,目標 apk(目錄)需要放在context.getFilesDir()中,不然會加載失敗(addAssetPath 返回 0)。另外,目標 apk 可以不簽名,因為 addAssetPath 過程并沒有進行簽名校驗。

獲取資源 id

實際情況中,如果我們需要獲取相應的資源,就必須先獲得資源對應的 id,而外部 apk 的 R.java 并不屬于主 apk,這就導致了獲取資源的困難。
目前存在的解決方案有:

  1. 通過反射對應的 R 類獲取對應的 id(極力不推薦,需要知道 field 的 name,若資源 apk 需要混淆,field name 就更不知道是什么了,再者反射的效率并不理想)
  2. 通過接口獲取對應的 id(優點在于靈活性高,主 apk 不需要關心資源。缺點在于若需要的資源較多,處理也較多。更多出現在獲取固定資源的場景中,譬如應用換膚)
  3. 直接將資源 apk 的 R.java 放在主 apk 中,通過 R 獲取 id(簡單粗暴,但若資源 apk 中存在對應的 R.java,會發生沖突。混淆過則不存在這個問題。該方案缺乏靈活性,需要開發人員知道需要的資源名,對應的屬性等。)

最后兩種方案各有各的優缺點,至于怎么選擇,還得結合自身的場景。

應用場景

動態加載資源技術目前的一些應用場景主要有:

  1. 替換應用皮膚(如:QQ 空間)
  2. 減小主 apk 的大小,非重要資源放在服務端
  3. 類似于文中高德 SDK 的做法,使得 jar 包可以加載資源(這種應用可能現在比較少,以前這種做法也只是因為還沒 aar)

后續

動態加載資源技術相關文章有很多,但就我目前所看到的文章只涉及如何獲取 drawable、string 等資源,并沒有發現關于動態加載資源 apk 中的布局文件(我姿勢不對?_(:зゝ∠)_)。后續會分享如何動態加載資源 apk 中的布局文件。

最后特別感謝 Andy ZhangMadisonRong 兩位朋友幫忙校對并對文章提出了寶貴的意見,謝謝。

來自:http://chaosleong.github.io/blog/2015/10/25/conggaode-SDK-xuexi-Android-dongtaijiazaiziyuan/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!