android热修复--适合做sdk的项目

1####一、摘要
移动互联的兴起,越来越多的重量级移动产品活跃在线上。要想提高用户粘性,除了产品本身的业务功能以外,用户体验也是重中之重。相信是程序员都有过被投诉线上崩溃的问题。然而这个时候你除了焦头烂额别无他法,因为你对线上运行的客户端产品没有任何掌控力。lz自己的项目作为一款sdk上线过近亿设备,每日活跃也五六百万。纵然前期代码编写步步为营,难免疏忽,呼呼。。。好在近期整理了一套方案,算是可以作为线上紧急问题的临时防护墙。

1.1方案选择:

目前网上流行的已经有几套方案,基本都基于两大阵营–阿里的dexposed,腾讯Qzone

1.2为何不选用andFix

阿里的andFix方案前期有过调研,其核心思想就是底层hook将有问题的方法的指针指向修复问题后的方法地址。从而达到方法替换的目的。如果手机厂商对系统有过一些更改很能引起一些底层hook的兼容性问题,另外andFix会引入平台so,如此一来势必会增大我们项目sdk的体积,这违背了我们sdk指标,于是项目中弃用该方案。

1.3选用Qzone方案

Qzone的原理简单,逻辑清晰,整篇由上层java语言主导,不存在版本兼容问题。这里是Qzone给出了详细的分析步骤:http://zhuanlan.zhihu.com/p/20308548
当你熟读了上面的文章会发现要做这个热修复也不过就几步的事:
1 阻止系统校验引用与被引用的class是否在同一个dex–防止需要打补丁的类被标记CLASS_ISPREVERIFIED
2 制作补丁
3补丁加载完成热修复
在正式介绍之前,来几张项目结构图:
这里写图片描述
整个项目分成app和library两个模块。libray并不是以依赖或者aar形式提供给app—–后续再介绍//TODO
本次我们需要重点关注patch文件夹

这里写图片描述

二、防止类被标记
2.1javassist操作class文件

简单一句话:防止类被标记就是要让相关类引用外部dex中的代码。
这里需要用到AOP技术。网上相关开源较多,比较好用的有ASM,javassist等。本次采用的javassist。Javassist完全符合java的编程风格。核心操作代码如下(详细代码打开inject.jar查看):

if(!flag&&!cClass.isInterface()&&!isAbstract(cClass)&&!jarClassName.contains("$")){
        System.out.println("cClass="+cClass +" is class");
        CtField cf =CtField.make("private static final boolean INJECT_FLAG = false;", cClass);
        cClass.addField(cf);
        CtMethod[] ms = cClass.getDeclaredMethods();
        if(null!=ms && ms.length>=1&&!isNativeMethod(ms[0])){
        System.out.println("ms="+ms.length+ms[0].getName()+" ,isEmpty= "+ms[0].isEmpty());
        ms[0].insertBefore("{if(INJECT_FLAG)Class ss = com.pa.anydoor.Hack.class;}");
        }
        }else{
            System.out.println("cClass="+cClass.getSimpleName()+" not inject ");
        }
        if(cClass.isFrozen()){
            cClass.defrost();
        }
        cClass.writeFile(dir);

        }

解释一下上面的代码:
1接口类不注入
2 抽象类不注入
3内部类不注入
4 native方法不注入
5我们在相关类的第一个方法的方法体中注入
Class ss = com.pa.anydoor.Hack.class; 这里的Hack.class是位于一个外部jar包(inject.jar)之中,同我们待注入的jar位于不同工程。
例如,我们为ResManager.class注入成功之后我们可以看到以下结果:
注入前:
这里写图片描述
注入后:
这里写图片描述
这样该类就被成功注入了一段外部工程的代码
if(ResManager.INJECT_FLAG){
Class localClass=Hack.class;
}
其中ResManager.INJECT_FLAG为常量false,它能保证Class localClass=Hack.class;一定不会执行,这样既可以节省程序运行开销,也可以避免因为种种原因导致Hack.class找不到类的异常。
我们一定要搞清楚,我们注入的Hack.class 只是为了编译打包apk的时候防止类被标记,我们并不是让它在程序中运行。

2.2具体执行

到这里你应该会迫不及待的想试一把,楼主已经为了准备好了自动化脚本—位于patch/inject/inject.sh

这里演示一下 将mylibrary/out/lib/classes.jar 进行注入后放到mylibrary/out (命令行中执行)
这里写图片描述
第一步:cd到inject.sh目录
第二步:执行脚本 ./inject.sh xxx.jar outdir (xxx.jar 表示待注入的jar,演示中为classes.jar ;outdir为jar注入成功后存放路径,演示中为out文件夹)
脚本准确无误的跑结束后,去out/out_jar目录下看看,这里会有惊喜。

PS:如果执行脚本有如下类似错误

这里写图片描述
根据错误提示知道是找不到android.os.Bundle类(也或者其他xx类找不到)。解决办法很简单,把android.jar(也或者xx类所在的jar包)放入到inject/libs文件夹下,然后执行inject文件夹下的addlib.sh
(命令行执行以下两步)
cd inject
./addlib.sh
执行完以上再执行inject.sh 即可

三、补丁制作
3.1补丁制作思路

补丁其实就是修复好bug的某几个类打包后的的dex文件。为减小不必要的开销,我们不必要为整工程的class类制作补丁文件,因此我们的首要任务就是将预期的class文件进行挑选。
我们都知道一个文件的内容发生改变,其对应的MD5签名会随之变化,利用这一特性,我们能很轻松的挑选出有变化的类文件。

3.2补丁制作步骤

这里先给出制作补丁的文件目录结构图:
这里写图片描述

1 将有bug的jar包放到oldjar文件夹中并解压(jar –xvf xxx.jar),得到所有class文件
2 通过 md5 file 命令循环遍历class文件,将class文件及其对应md5值存放到oldjarinfo.txt中
3 将修复好bug的jar包方到newjar文件夹中并解压,得到所有class文件
4 同样计算所有class文件md5值,将class文件及其对应md5值存放到newjarinfo.txt中
5 通过diff命令 diff oldjarinfo.txt newjarinfo.txt >> diff.txt 记录下两个jar包中不同的class文件名
6 从newjar文件夹中挑选出记录在diff.txt中的类
7通过命令 jar –cvfM xxx.jar . 将挑选出的class文件制作成jar包
8 再执行./d2j-jar2dex.sh xx.jar 制作成最终dex文件
整个步骤请参考工程目录下patch.sh(不是pack.sh)

3.3具体执行

命令行执行脚本:./patch.sh 3.3 即可
这里的3.3为补丁版本号
最终生成的补丁长这样:
这里写图片描述
最终生成了一个mf文件和zip文件
mf文件里面记录了补丁的基本信息:
补丁MD5值:MD5=f1825a4bcf53482af92b35cf239f110d
补丁版本号:
PATCH_VERSION=3.3
补丁名字:
PATCH_NAME=patch_3.3_160525162703.jar
zip文件放的就是补丁包文件:patch_3.3_160525162703.jar
在这里设计zip和mf的目的是为了方便补丁文件的校验—-上线后补丁文件肯定是从服务器下载回本地,安全起见怎么也得校验补丁文件的完整性等。

四、补丁加载

1、lib工程里新建一个hotfix的管理类,主要用于补丁文件的加载
Demo中的管理类如下:
这里写图片描述
2 、将hotfix.jar 拷贝到宿主app的libs目录(另外一个classes.jar 就是例子中的mylibrary工程jar包)
这里写图片描述
3、在宿主app的application里的oncreate第一行调用热修复的初始化
这里写图片描述

五、DEMO工程使用

demo下载地址:https://github.com/killer8000/HotFix_SDK2
1、运行demo时请先替换打包环境(位于mylibrary/build.xml)
这里写图片描述
2、mylibrary有更改后,先执行脚本pack.sh(不是patch.sh)—-该脚本是叫mylibrary打包整合到app里
3、运行demo,当你点击“我是BUTTON”按钮时,会崩溃(在mylibrary/ActivityTest.java里面制造了 int i=1/0;)
4、将patch.jar 拷贝到手机根目录(这里的patch.jar就是补丁包)
5、杀掉进程再次进入应用,当你再次点击“我是BUTTON”按钮时就会有惊喜出现

六、混淆

保持hotfix.jar 不要混淆
keep -libraryjars libs/hotfix.jar

七、注意事项

1、执行pack.sh出现以下错误
这里写图片描述
修复办法:
在mylibrary下新建文件夹 gen
这里写图片描述
2、执行pack.sh出现以下错误:
这里写图片描述
修复办法:
在mylibrary下新建文件夹build_new
这里写图片描述

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值