背景当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?解决方案该方案基于的是androiddx分包方案的,关于dx分包方案,网上有几篇解释了,所以这里就不再赘述。简单的概括一下,就是把多个dx文件塞入到app的classloadr之中,但是androiddx拆包方案中的类是没有重复的,如果classs.dx和classs1.dx中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?让我们来看看类加载的代码:
publicClassfindClass(Stringnam,ListThrowablsupprssd){
for(Elmntlmnt:dxElmnts){//每个Elmnt就是一个dx文件
DxFildx=lmnt.dxFil;
if(dx!=null){
Classclazz=dx.loadClassBinaryNam(nam,dfiningContxt,supprssd);
if(clazz!=null){
rturnclazz;
}
}
}
if(dxElmntsSupprssdExcptions!=null){
supprssd.addAll(Arrays.asList(dxElmntsSupprssdExcptions));
}
rturnnull;
}
一个ClassLoadr可以包含多个dx文件,每个dx文件是一个Elmnt,多个dx文件排列成一个有序的数组dxElmnts,当找类的时候,会按顺序遍历dx文件,然后从当前遍历的dx文件中找类,如果找类则返回,如果找不到从下一个dx文件继续查找。理论上,如果在不同的dx中有相同的类存在,那么会优先选择排在前面的dx文件的类,如下图:
在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dx(patch.dx)中去,然后把这个dx插入到Elmnts的最前面,如下图
好,该方案基于第二个拆分dx的方案,方案实现如果懂拆分dx的原理的话,大家应该很快就会实现该方案,如果没有拆分dx的项目的话,可以参考一下谷歌的multidx方案实现。然后在插入数组的时候,把补丁包插入到最前面去。好,看似问题很简单,轻松的搞定了,让我们来试验一下,修改某个类,然后打包成dx,插入到classloadr,当加载类的时候出现了(本例中是ActivityManagr要被替换):
为什么会出现以上问题呢?从log的意思上来讲,ModulManagr引用了ActivityManagr,但是发现这这两个类所在的dx不在一起,其中:1.ModulManagr在classs.dx中2.ActivityManagr在patch.dx中结果发生了错误。这里有个问题,拆分dx的很多类都不是在同一个dx内的,怎么没有问题?让我们搜索一下抛出错误的代码所在,嘿咻嘿咻,找到了一下代码:
从代码上来看,如果两个相关联的类在不同的dx中就会报错,但是拆分dx没有报错这是为什么,原来这个校验的前提是:
如果引用者(也就是ModulManagr)这个类被打上了CLASS_ISPREVERIFIED标志,那么就会进行dx的校验。那么这个标志是什么时候被打上去的?让我们在继续搜索一下代码,嘿咻嘿咻~~,在DxPrpar.cpp找到了一下代码:
这段代码是dx转化成odx(dxopt)的代码中的一段,我们知道当一个apk在安装的时候,apk中的classs.dx会被虚拟机(dxopt)优化成odx文件,然后才会拿去执行。虚拟机在启动的时候,会有许多的启动参数,其中一项就是vrify选项,当vrify选项被打开的时候,上面doVrify变量为tru,那么就会执行dvmVrifyClass进行类的校验,如果dvmVrifyClass校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志,那么具体的校验过程是什么样子的呢?此代码在DxVrify.cpp中,如下:
1.验证clazz-dirctMthods方法,dirctMthods包含了以下方法:1.static方法2.privat方法3.构造函数2.?clazz-virtualMthods1.虚函数=ovrrid方法?概括一下就是如果以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dx中的话,那么这个类就会被打上CLASS_ISPREVERIFIED标志
所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志。
最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:
if(ClassVrifir.PREVENT_VERIFY){
Systm.out.println(AntilazyLoad.class);
}
其中AntilazyLoad类会被打包成单独的hack.dx,这样当安装apk的时候,classs.dx内的类都会引用一个在不相同dx中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了。只要没被打上这个标志的类都可以进行打补丁操作。然后在应用启动的时候加载进来.AntilazyLoad类所在的dx包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dx包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。所以Application作为应用的入口不能插入这段代码。(因为载入hack.dx的代码是在Application中onCrat中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dx加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)其中:
classClassVrifir{
publicstaticboolanPREVENT_VERIFY=fals;//fals防止代码被执行,提高性能
}
之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。隐患:虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试。但是在大项目中拆分dx的问题已经比较严重,很多类都没有被打上这个标志。如何打包补丁包:1、空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5.还有一份mapping混淆文件。2、在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dx,只需要把修改过的类的class文件打包成patchdx,然后放到sdcard下,那么就会让改变的代码生效。
赞赏