前不久跑去折腾高德 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 就引入了反编译功能)。

1
2
3
4
5
6
7
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 中的布局文件。

最后特别感谢 Andy ZhangMadisonRong 两位朋友帮忙校对并对文章提出了宝贵的意见,谢谢。


参考文章:

《Android中插件开发篇之—-应用换肤原理解析》

从高德 SDK 学习 Android 动态加载资源的更多相关文章

  1. Android 动态加载 (二) 态加载机制 案例二

    探秘腾讯Android手机游戏平台之不安装游戏APK直接启动法 重要说明 在实践的过程中大家都会发现资源引用的问题,这里重点声明两点: 1. 资源文件是不能直接inflate的,如果简单的话直接在程序 ...

  2. [转载] Android动态加载Dex机制解析

    本文转载自: http://blog.csdn.net/wy353208214/article/details/50859422 1.什么是类加载器? 类加载器(class loader)是 Java ...

  3. Android动态加载技术初探

    一.前言: 现在,已经有实力强大的公司用这个技术开发应用了,比如淘宝,大众点评,百度地图等,之所以采用这个技术,实际上,就是方便更新功能,当然,前提是新旧功能的接口一致,不然会报Not Found等错 ...

  4. Android动态加载jar/dex

    前言 在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优 ...

  5. 【Android】Android动态加载Jar、APK的实现

    本文介绍Android中动态加载Jar.APK的实现.而主要用到的就是DexClassLoader这个类.大家都知道Android和普通的Java虚拟机有差别,它只能加载经过处理的dex文件.而加载这 ...

  6. Android 动态加载 (一) 态加载机制 案例一

    在目前的软硬件环境下,Native App与Web App在用户体验上有着明显的优势,但在实际项目中有些会因为业务的频繁变更而频繁的升级客户端,造成较差的用户体验,而这也恰恰是Web App的优势.本 ...

  7. Android应用开发提高系列(4)——Android动态加载(上)——加载未安装APK中的类

    前言 近期做换肤功能,由于换肤程度较高,受限于平台本身,实现起来较复杂,暂时搁置了该功能,但也积累了一些经验,将分两篇文章来写这部分的内容,欢迎交流! 关键字:Android动态加载 声明 欢迎转载, ...

  8. Android动态加载代码技术

    Android动态加载代码技术 在开发Android App的过程当中,可能希望实现插件式软件架构,将一部分代码以另外一个APK的形式单独发布,而在主程序中加载并执行这个APK中的代码. 实现这个任务 ...

  9. 深入浅出Android动态加载jar包技术

    在实际项目中,由于某些业务频繁变更而导致频繁升级客户端的弊病会造成较差的用户体验,而这也恰是Web App的优势,于是便衍生了一种思路,将核心的易于变更的业务封装在jar包里然后通过网络下载下来,再由 ...

随机推荐

  1. uboot移植之环境变量在NandFlash

    一.概述 u-boot环境变量可以设置在Norflash上,也可以在NandFlash上. 倘若环境变量在NorFlash上,再假设S3C2440从NorFlash启动,是能正确从NorFlash上读 ...

  2. 移动App设计之分层架构+MVC

    http://www.cnblogs.com/Logen/archive/2012/11/08/2760638.html 场景分析:我们知道,一个移动设备的应用大多与网络有关,也就是说,我在移动设备上 ...

  3. LightOj_1265 Island of Survival

    题目链接 题意: 在孤岛生存, 孤岛上有t头老虎,d头鹿, 每天会出现随机出现两只生物(包括你自己), 如果出现了一只老虎,那么你将被吃掉, 如果两只老虎, 则两只老虎会同归于尽,其他情况你都将生存下 ...

  4. Uva_11762 Race to 1

    题目链接 题意: 给一个数n, 每次从小于等于n的素数里选一个P, 如果能被n整除, 那么就n就变成n / P. 问: n 变成1的期望. 思路: 设小于等于n的素数有p 个, 其中是n的约数的有g个 ...

  5. seajs的spm使用

    压缩JS文件 只需要执行这个命令即可 spm build xxx.js 这时候你将得到一个压缩过的__build/xxx.js文件 合并JS文件 如果希望将JS文件中require的其他模块都合并到这 ...

  6. bzoj 3744: Gty的妹子序列 主席树+分块

    3744: Gty的妹子序列 Time Limit: 15 Sec  Memory Limit: 128 MBSubmit: 101  Solved: 34[Submit][Status] Descr ...

  7. [spring-framework]Spring定时器的配置和使用

    开发中我们常常会做一些定时任务,这些任务有开始时间,并会按一定的周期或规则执行.如此我们在Java程序开发中使用定时器来处理定时任务. <!-- MessageRequestTask类中包含了m ...

  8. 【HDOJ】3221 Brute-force Algorithm

    归来吧很好推导.T(n) = a^f(n-1)*b^f(n)%p.主要难点在于求mod和fibo.引用如下公式A^B%C = A^(B%phi(C) + phi(C))%C, 满足B>=phi( ...

  9. [LeetCode#84]Largest Rectangle in Histogram

    Problem: Given n non-negative integers representing the histogram's bar height where the width of ea ...

  10. HDU-2522 A simple problem

    http://acm.hdu.edu.cn/showproblem.php?pid=2522 学习://除数的运算的应用和算法.除法的本质,如果余数出现重复就表示有循环节 A simple probl ...