前不久跑去折腾高德 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. myeclipse 添加服务器运行时环境

    像servlet-api.jar.servlet-api.jar服务器能提供的包 解决方法如下: 1,File->New->Other->Server->Server(注意在n ...

  2. 在Docker下部署Nginx

    在Docker下部署Nginx 在Docker下部署Nginx,包括: 部署一个最简单的Nginx,可以通过80端口访问默认的网站 设置记录访问和错误日志的路径 设置静态网站的路径 通过proxy_p ...

  3. 【转】不同VLAN之间相互通信及VTP、STP、EtherChannel概念

    厘清最后一个概念. 转了网上两个相关帖子: http://www.net130.com/CMS/Pub/Tech/tech_zh/2009_03_12_97386_3.htm http://blog. ...

  4. 【UVALive - 3211】Now or later (二分+2-SAT)

    题意: 有n架飞机需要着陆.每架飞机有两种选择,早着陆或者晚着陆,二选其一.现在为了保证飞机的着陆安全,要求两架着陆的飞机的时间间隔的最小值达到最大. 分析: 最小值最大问题我们想到二分答案.对于猜测 ...

  5. Python安装及开发环境配置

    Python的语法简洁,功能强大,有大量的第三方开发包(模块).同时Python不像java一样对内存要求非常高,适合做一些经常性的任务方面的编程.根据codeeval网站数据统计显示,连续三年,Py ...

  6. Qt写的截图软件包含源代码和可执行程序

    http://blog.yundiantech.com/?log=blog&id=14 Qt写的截图软件包含源代码和可执行程序 http://download.csdn.net/downloa ...

  7. QMetaObject感觉跟Delphi的类之类有一拼,好好学一下

    提供了一堆原来C++没有的功能,比如反射什么的...但是可能还是没有Delphi的类之类更强,因为类之类可以“创建类”.可惜我学艺不精,对“类之类”也没有完全学会.先留个爪,有空把两个东西都好好学学, ...

  8. MYSQL常用命令集合

    1.导出整个数据库 mysqldump -u 用户名 -p --default-character-set=latin1 数据库名 > 导出的文件名(数据库默认编码是latin1) mysqld ...

  9. 误导人的接口(interface)

    接口,interface,这个词语有误导之嫌.窃以为,这也是其名称与实际开发不符,造成难于直观理解和使用过程中产生困惑的根源.所谓名不正则言不顺:不怕生错命,最怕改坏名. 在现实生活中,接口通常是指将 ...

  10. Installing vSphere SDK for Perl

    Installing vSphere SDK for Perl 你可以安装vSphere SDK 在Linux 或者Microsoft Windows 系统,或者 部署 VSphere Managem ...