前不久跑去折騰高德 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 是如何實現動態加載資源的:
將上述內容再簡略,動態加載資源所必需的幾個核心步驟:
這裏須要注意的是,目標 apk(目錄)須要放在 context.getFilesDir()
中,否則會加載失敗(addAssetPath 返回 0)。另外,目標 apk 能夠不簽名,由於 addAssetPath 過程並無進行簽名校驗。
實際狀況中,若是咱們須要獲取相應的資源,就必須先得到資源對應的 id,而外部 apk 的 R.java 並不屬於主 apk,這就致使了獲取資源的困難。
目前存在的解決方案有:
最後兩種方案各有各的優缺點,至於怎麼選擇,還得結合自身的場景。
動態加載資源技術目前的一些應用場景主要有:
動態加載資源技術相關文章有不少,但就我目前所看到的文章只涉及如何獲取 drawable、string 等資源,並無發現關於動態加載資源 apk 中的佈局文件(我姿式不對?_(:зゝ∠)_
)。後續會分享如何動態加載資源 apk 中的佈局文件。