基于DX的安卓动态更新报告

DX简介

安卓程序的主要代码是java 代码,不过由于安卓系统不直接使用sun的jvm,所以从javac编译过来的class文件并不能直接被安卓系统加载运行。

要想运行java代码,需要 除了和以前一样调用javac将java代码编译为class文件以外,还需要调用dx这个工具,将class文件转成dex文件。然后安卓系统才会去加 载dex文件并执行程序。因此说,dx是将class文件(及class文件集合)转成dex的工具。

dx属于安卓开源项目的一部分,它的源码位于“external/dexmaker”目录下,纯java。由于是纯java工具,所以可以打包成依赖库放到我们的 项目里面,实现安卓平台上,将一组class文件或一个jar文件转换成dex的功能。下面我将通过代码演示将一组class文件编译成dex,并加载运 行。

使用DX

  • 导入class文件

程序的第一步是将class文件以字节数组的方式读入内存。为了演示方便,这里我将运行时编译的class集合选择为MobTools,为了简化示例程序,我事先将它解压到项目的assets中。下面的代码演示的是以递归的方式读取所有class文件到内存中:

基于DX的安卓动态更新报告

  • 初始化dex配置信息

载入class文件后,就可以将它们写入dex,但是dex会有一些配置信息需要初始化,下面的代码演示了这个过程:

基于DX的安卓动态更新报告

我并没有仔细查阅过这两个类的用途,希望后续使用此功能的同事可以追加注释。

  • 添加class文件

接下来是将class类名和数据添加到dex实例中:

基于DX的安卓动态更新报告

DexFile是dex数据在内存中的抽象,可以理解为一个列表。列表中存放着一个个ClassDefItem,携带着各个class的完整路径和描述数据。

我在添加的过程中利用ClassDefItem对类描述数据的解析功能,获取了每一个类的父类和接口列表,并缓存了起来。这个会在后续加载dex的时候用到。

  • 保存dex文件

DexFile是dex文件在内存的解析结果,不能直接拿来使用。安卓系统加载dex都是基于文件的,所以现在还需要将DexFile转成dex数据:

基于DX的安卓动态更新报告

toDex的第一个参数是日志输出工具,如果设置为null,表示不输出日志。

完成转换以后得到一个byte数组,只要将byte数组写入文件,并保存为dex后缀就可以被多数的安卓系统加载。但是对于一些定制的系统,比方说小米,他 们只识别压缩包里的dex,比方说jar包里的dex。这种方式普通系统也是支持的,为了更好的兼容性,我将dex文件保存到了应用私有目录下的一个 jar文件中:

基于DX的安卓动态更新报告

数据会保存到“/data/data/包名/files”下的MobTools.jar中。打开个jar包,里面就是我们熟悉的“classes.dex”文件。

  • 加载dex文件

有了文件,现在就可以调用系统的api来加载它:

基于DX的安卓动态更新报告

基于DX的安卓动态更新报告

dalvik.system.DexFile是android.jar里面提供的api,虽然名字也是DexFile,但是功能和dx里面DexFile不同。使用时通过其静态方法loadDex来获取实例。传入的参数jarPath表示jar包的路径,dexPath表示准备加压的dex文件路径。

由于MobTools中有很多各类,而且不同的类有继承关系,所以这里我开辟了一个方法loadClass做递归加载。其原理是:循环获取 classesNames列表中的第一个元素尝试进行加载,加载前判断这个类是否有父类,如果有,将这个父类从classesNames列表中取出,然后先对这个父类进行加载。如果被加载的类没有父类,则再检查他是否有实现什么借口,如果有,循环加载这些接口的类。父类和接口列表都完成加载以后,再加载最 初尝试加载的类。这样子就不会再加载的过程中报出“子类找不到父类”的问题。

使用

完成了上述的操作后,原先assets中的class文件就被全部加载如内容,可以被使用了。

使用的方式有两种,一直是基于反射,先获取某个类的Class 对象,然后通过Class对象获取方法或字段,最后invoke或者get/set。第二种方式是实现通过依赖mobtools的方式编写一份代码,并将 这份代码编译为jar包,被我们的项目依赖;等到mobtools加载完毕后,代用这个jar包中的api,使用其中的功能。

这两种办法各有优劣。第一种办法编写起来很麻烦,容易出错,但是随时可以改变逻辑。第二种办法由于编码的时候依赖了mobtools,所以能以常规的编码方式进行开发,后来使用时调用它的api时也比较直观。但是它在使用时已经是静态的jar包,要改动比较麻烦。

动态更新

我研究dx的目的在于动态更新,否则在安卓系统上编译和加载class文件并没有什么意义。一般操作方式是这样子:首先,对于一些可能日后要改变的功能,编 译代码时不将它们打包到apk的dex中,而是打包到zip文件,当做资源的方式随包发布;使用时会对zip文件进行解压,然后运行时合并为dex,并 path到内存中使用。然后,等项目的逻辑改变了,服务端可以向客户端推送改变逻辑后的class文件集合,去覆盖原来的class文件;等到下一次客户 端重新启动时,发现class文件版本已经改变了,于是再度动态合并dex,并path。

如此,apk的升级可以变得频繁,而且每次都是增量更新。如果用户删除了缓存的数据,应用只会恢复到初始状态,然后再从服务端获取最新版本的“class补丁”替换合并即可。

甚至,如果服务端的性能更强的情况下,可以对不同的客户端下发不同的class文件,比方说为不同的客户端定制RSA的公钥,或者其它的配置信息等等。

与groovy结合

如果嫌以class为单位来更新代码,颗粒度还不够的话,还可以结合groovy的解析器。

groovy 是一种脚本语言,它基于java虚拟机,并且完全兼容java语法。groovy能将自己的脚本编译为java的class,所以刚好可以被我们用来减少 动态更新的颗粒度。比方说一开始我们发布的时候不是将class文件压缩为一个zip,而是直接压缩java源码。运行后调用groovy来解析这些 java源码,将它们编译为class文件,再调用dx对这些class文件进行合并。升级的时候,服务端指定具体的源码文件的具体段落进行替换,可以替 换一整个包,也可以只是替换一行代码。替换以后再调用groovy重新编译即可。

文/Mob开发者平台 技术副总监 余勋杰

相关推荐