高德SDK內資源的分析

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

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

看下導航 API 的 jar 包結構。android

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 就引入了反編譯功能)。api

 

1服務器

2架構

3ide

4佈局

5ui

6this

7spa

8

9

10

11

12

13

14

15

16

17

18

19

20

21

 

...

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行代碼:

 

1

 

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

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

 

1

2

3

4

5

6

 

public static Resources a() {

if (b == null) {

b = e.getResources();

}

return b;

}

其中變量 e 爲上層傳遞進來的 Activity,而咱們前面說過,咱們的 apk 中並無相應的資源,因此將注意力放到變量 b 在其餘地方的賦值上。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

 

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,這個文件又是怎麼來的?

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

 

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 中的佈局文件。

相關文章
相關標籤/搜索