天天看点

动态补丁升级

最新github上开源了很多热补丁动态修复框架,大致有:

<a target="_blank" href="https://github.com/dodola/hotfix">https://github.com/dodola/hotfix</a>

<a target="_blank" href="https://github.com/jasonross/nuwa">https://github.com/jasonross/nuwa</a>

<a target="_blank" href="https://github.com/bunnyblue/droidfix">https://github.com/bunnyblue/droidfix</a>

之前项目也使用过动态升级,用的技术也就是dex分包,分包替换策略。

有兴趣的直接看这篇原理文章,加上上面框架的源码基本就可以看懂了。当然了,本篇博文也会做个上述框架源码的解析,以及在整个实现过程中用到的技术的解析。

对于热修复的原理,如果你看了上面的两篇文章,相信你已经大概明白了。重点需要知道的就是,android的classloader体系,android中加载类一般使用的是<code>pathclassloader</code>和<code>dexclassloader</code>,首先看下这两个类的区别:

对于<code>pathclassloader</code>,从文档上的注释来看:

provides a simple {@link classloader} implementation that operates  on a list of files and directories in the local file system, but  does not attempt to load classes from the network. android uses  this class for its system class loader and for its application  class loader(s).

可以看出,android是使用这个类作为其系统类和应用类的加载器。并且对于这个类呢,只能去加载已经安装到android系统中的apk文件。

对于<code>dexclassloader</code>,依然看下注释:

a class loader that loads classes from {@code .jar} and  {@code .apk} files containing a {@code classes.dex} entry.  this can be used to execute code not installed as part of an application.

可以看出,该类呢,可以用来从.jar和.apk类型的文件内部加载classes.dex文件。可以用来执行非安装的程序代码。

ok,如果大家对于插件化有所了解,肯定对这个类不陌生,插件化一般就是提供一个apk(插件)文件,然后在程序中load该apk,那么如何加载apk中的类呢?其实就是通过这个dexclassloader,具体的代码我们后面有描述。

ok,到这里,大家只需要明白,android使用pathclassloader作为其类加载器,dexclassloader可以从.jar和.apk类型的文件内部加载classes.dex文件就好了。

上面我们已经说了,android使用pathclassloader作为其类加载器,那么热修复的原理具体是?

ok,对于加载类,无非是给个classname,然后去findclass,我们看下源码就明白了。 

<code>pathclassloader</code>和<code>dexclassloader</code>都继承自<code>basedexclassloader</code>。在basedexclassloader中有如下源码:

可以看出呢,basedexclassloader中有个pathlist对象,pathlist中包含一个dexfile的集合dexelements,而对于类加载呢,就是遍历这个集合,通过dexfile去寻找。

ok,通俗点说:

一个classloader可以包含多个dex文件,每个dex文件是一个element,多个dex文件排列成一个有序的数组dexelements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。(来自:安卓app热补丁动态修复技术介绍)

那么这样的话,我们可以在这个dexelements中去做一些事情,比如,在这个数组的第一个元素放置我们的patch.jar,里面包含修复过的类,这样的话,当遍历findclass的时候,我们修复的类就会被查找到,从而替代有bug的类。

ok,对于<code>class_ispreverified</code>,还是带大家理一下:

根据上面的文章,在虚拟机启动的时候,当verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上<code>class_ispreverified</code>标志。

那么,我们要做的就是,阻止该类打上<code>class_ispreverified</code>的标志。

注意下,是阻止引用者的类,也就是说,假设你的app里面有个类叫做<code>loadbugclass</code>,再其内部引用了<code>bugclass</code>。发布过程中发现<code>bugclass</code>有编写错误,那么想要发布一个新的<code>bugclass</code>类,那么你就要阻止<code>loadbugclass</code>这个类打上<code>class_ispreverified</code>的标志。

也就是说,你在生成apk之前,就需要阻止相关类打上<code>class_ispreverified</code>的标志了。对于如何阻止,上面的文章说的很清楚,让<code>loadbugclass</code>在构造方法中,去引用别的dex文件,比如:hack.dex中的某个类即可。

ok,总结下:

其实就是两件事:1、动态改变basedexclassloader对象间接引用的dexelements;2、在app打包的时候,阻止相关类去打上<code>class_ispreverified</code>标志。

如果你没有看明白,没事,多看几遍,下面也会通过代码来说明。

那么,这里拿具体的类来说:

大致的流程是:在dx工具执行之前,将<code>loadbugclass.class</code>文件呢,进行修改,再其构造中添加<code>system.out.println(dodola.hackdex.antilazyload.class)</code>,然后继续打包的流程。注意:<code>antilazyload.class</code>这个类是独立在hack.dex中。

ok,这里大家可能会有2个疑问:

如何去修改一个类的class文件

如何在dx之前去进行疑问1的操作

这里我们使用javassist来操作,很简单:

ok,首先我们新建几个类:

注意下,这里的package,我们要做的是,上述类正常编译以后产生class文件。比如:loadbugclass.class,我们在loadbugclass.class的构造中去添加一行:

下面看下操作类:

ok,点击run即可了,注意项目中导入javassist-*.jar的包。

首先拿到classpool对象,然后添加classpath,如果你有多个classpath可以多次调用。然后从classpath中找到loadbugclass,拿到其构造方法,在其最后插入一行代码。ok,代码很好懂。

ok,我们反编译看下我们生成的class文件:

动态补丁升级

ok,关于javassist,如果有兴趣的话,大家可以参考几篇文章学习下:

<a target="_blank" href="http://www.ibm.com/developerworks/cn/java/j-dyn0916/">http://www.ibm.com/developerworks/cn/java/j-dyn0916/</a>

<a target="_blank" href="http://zhxing.iteye.com/blog/1703305">http://zhxing.iteye.com/blog/1703305</a>

将其源码导入之后,打开app/build.gradle

你会发现,在执行dx之前,会先执行processwithjavassist这个任务。这个任务的作用呢,就和我们上面的代码一致了。而且源码也给出了,大家自己看下。

ok,到这呢,你就可以点击run了。ok,有兴趣的话,你可以反编译去看看<code>dodola.hotfix.loadbugclass</code>这个类的构造方法中是否已经添加了改行代码。

ok,到此我们已经能够正常的安装apk并且运行了。但是目前还未涉及到打补丁的相关代码。

ok,这里就比较简单了,动态改变一个对象的某个引用我们反射就可以完成了。

不过这里需要注意的是,还记得我们之前说的,寻找class是遍历dexelements;然后我们的<code>antilazyload.class</code>实际上并不包含在apk的classes.dex中,并且根据上面描述的需要,我们需要将<code>antilazyload.class</code>这个类打成独立的hack_dex.jar,注意不是普通的jar,必须经过dx工具进行转化。

具体做法:

如果,你没有办法把那一个class文件搞成jar,去百度一下…

ok,现在有了hack_dex.jar,这个是干嘛的呢?

应该还记得,我们的app中部门类引用了<code>antilazyload.class</code>,那么我们必须在应用启动的时候,降这个hack_dex.jar插入到dexelements,否则肯定会出事故的。

那么,application的oncreate方法里面就很适合做这件事情,我们把hack_dex.jar放到assets目录。

下面看hotfix的源码:

ok,在app的私有目录创建一个文件,然后调用utils.preparedex将assets中的hackdex_dex.jar写入该文件。 

接下来hotfix.patch就是去反射去修改dexelements了。我们深入看下源码:

ok,其实就是文件的一个读写,将assets目录的文件,写到app的私有目录中的文件。

下面主要看patch方法

这里很据系统中classloader的类型做了下判断,原理都是反射,我们看其中一个分支<code>hasdexclassloader()</code>;

首先查找类<code>dalvik.system.basedexclassloader</code>,如果找到则进入if体。

在injectaboveequalapilevel14中,根据context拿到pathclassloader,然后通过getpathlist(pathclassloader),拿到pathclassloader中的pathlist对象,在调用getdexelements通过pathlist取到dexelements对象。

ok,那么我们的hack_dex.jar如何转化为dexelements对象呢?

通过源码可以看出,首先初始化了一个dexclassloader对象,前面我们说过dexclassloader的父类也是basedexclassloader,那么我们可以通过和pathclassloader同样的方式取得dexelements。

ok,到这里,我们取得了,系统中pathclassloader对象的间接引用dexelements,以及我们的hack_dex.jar中的dexelements,接下来就是合并这两个数组了。

可以看到上面的代码使用的是combinearray方法。

合并完成后,将新的数组通过反射的方式设置给pathlist.

接下来看一下反射的细节:

其实都是取成员变量的过程,应该很容易懂~~

ok,这里的两个数组合并,只需要注意一件事,将hack_dex.jar里面的dexelements放到新数组前面即可。

到此,我们就完成了在应用启动的时候,动态的将hack_dex.jar中包含的dexfile注入到classloader的dexelements中。这样就不会查找不到antilazyload这个类了。

ok,那么到此呢,还是没有看到我们如何打补丁,哈,其实呢,已经说过了,打补丁的过程和我们注入hack_dex.jar是一致的。

你现在运行hotfix的app项目,点击menu里面的测试:

会弹出:<code>调用测试方法:bug class</code>

接下来就看如何完成热修复。

ok,那么我们假设bugclass这个类有错误,需要修复:

可以看到字符串变化了:bug class -&gt; fixed class .

然后,编译,将这个类的class-&gt;jar-&gt;dex。步骤和上面是一致的。

拿到path_dex.jar文件。

正常情况下,这个玩意应该是下载得到的,当然我们介绍原理,你可以直接将其放置到sdcard上。

然后在application的oncreate中进行读取,我们这里为了方便也放置到assets目录,然后在application的oncreate中添加代码:

其实就是添加了后面的3行,这里需要说明一下,第一行依旧是复制到私有目录,如果你是sdcard上,那么操作基本是一致的,这里就别问:如果在sdcard或者网络上怎么处理~

ok,那么再次运行我们的app。

动态补丁升级

ok,最后说一下,说项目中有一个打补丁的按钮,在menu下,那么你也可以不在application里面添加我们最后的3行。

你运行app后,先点击<code>打补丁</code>,然后点击<code>测试</code>也可以发现成功修复了。

如果先点击<code>测试</code>,再点击<code>打补丁</code>,再<code>测试</code>是不会变化的,因为类一旦加载以后,不会重新再去重新加载了。

原文链接:http://blog.csdn.net/lmj623565791/article/details/49883661

继续阅读