第一部分:什么是热修复

我们经常上线一个app后,才发现有个bug还没改掉,需要紧急修复。如果按照通常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样是不是太麻烦了,我相信有过这个经历的人肯定不在少数。热修复就是解决这个问题的。简单点说就是给你的应用打补丁,也就是说你只要把要修改的地方打成补丁放在服务端,通过事先设定好的接口,把补丁下载到客服端,从而修复bug。

第二部分:热修复的原理

热修复技术主要主要是集中在三个地方,代码热修复、资源热修复、So库的热修复,我们平时有bug或是说希望替换的地方也就是这三个方面了。下面从这三个方面来说下热修复原理。

一、代码热修复技术。
代码修复有两大主要方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案。

这两类方案各有优劣:

•底层替换方案限制颇多,但时效性最好,加载轻快,立即见效。

•类加载方案时效性差,需要重新冷启动才能见效,但修复范围广,限制少。

1、底层替换方案

底层替换方案是在已经加载了的类中直接替换掉原有方法,是在原来类的基础上进行修改的。因而无法实现对与原有类进行方法和字段的增减,因为这样将破坏原有类的结构。

一旦补丁类中出现了方法的增加和减少,就会导致这个类以及整个Dex的方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问方法时就无法正常地索引到正确的方法了。如果字段发生了增加和减少,和方法变化的情况一样,所有字段的索引都会发生变化。并且更严重的问题是,如果在程序运行中间某个类突然增加了一个字段,那么对于原先已经产生的这个类的实例,它们还是原来的结构,这是无法改变的。而新方法使用到这些老的实例对象时,访问新增字段就会产生不可预期的结果。
这是这类方案的固有限制,而底层替换方案最为人诟病的地方,在于底层替换的
不稳定性。
传统的底层替换方式,不论是Dexposed、Andfix或者其他安全界的Hook方案,都是直接依赖修改虚拟机方法实体的具体字段。例如,改Dalvik方法的jni函数指针、改类或方法的访问权限等等。这样就带来一个很严重的问题,由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix ArtMethod的结构是根据公开的Android源码中的结构写死的。如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,通用性的替换机制就会出问题。这便是不稳定的根源。

2、类加载方案
类加载方案的原理是在app重新启动后让Classloader去加载新的类。因为在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。如果不重启,原来的类还在虚拟机中,就无法加载新类。因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve为新类。从而达到热修复的目的。

再来看看腾讯系三大类加载方案的实现原理。QQ空间方案会侵入打包流程,并且为了 hack添加一些无用的信息,实现起来很不优雅。而QRx的方案,需要获取底层虚拟机的函数,不够稳定可靠,并且有个比较大的问题是无法新增public函数。

微信的Tinke「方案是完整的全量dex加载,并且可谓是将补丁合成做到了极
致,然而我们发现,精密的武器并非适用于所有战场。Tinke的合成方案,是从dex的方法和指令维度进行全量合成,整个过程都是自己研发的。虽然可以很大地节省空间,但由于对dex内容的比较粒度过细,实现较为复杂,性能消耗比较严重。实际上,dex的大小占整个apk的比例是比较低的,一个app里面的dex文件大小并不是主要部分,而占空间大的主要还是资源文件。因此,Tinker的性价比不高。

二、资源热修复技术

资源的热修复其实是利用下发的补丁包,直接更新资源。大部分现在更新资源的如修复都参考了Instant Run的实现,那么我们简单说下其实现。
基本上来看,分了俩步。
1、构造一个新的AssetManager,并通过反射调用addAssetPath,然后把这个完整的新资源包加入到AssetManager中,这样就得到了一个含新资源的AssetManager。
2、找到之前所有引用到AssetManager的地方,通过反射把引用处替换成AssetManager。

现在资源热修复的技术,大多参考了Instant Run 的实现,有以下几种方案:
1、运行时合成完整的资源包。(比如微信Tinker)
2、下发完整包。(Amigo)
3、修改aapt。
4、构造了一个package id为0x66的资源包,这个包里只包含改
变了的资源项,然后直接在原有AssetManager中addAssetPath这个包。

三、so库热修复技术。

这个一般改动不是很大,热修复一般用不上。

第三部分:热修复现有解决方案

一、AndFix

阿里的热修复解决方案AndFix:https://github.com/alibaba/AndFix,这个框架使用非常简单,下面的使用方法,其实是从文档中翻译过来的,也可以直接看文档。

可以在gradle.build里直接依赖

dependencies { 
 compile ‘com.alipay.euler:andfix:0.5.0@aar’ 
 }

1、Initialize PatchManager

patchManager = new PatchManager(context);
patchManager.init(appversion);//current version

2、Load patch

patchManager.loadPatch();
这个一般在Application.onCreate()中初始化

3、Add patch

patchManager.addPatch(path);//path of the patch file that was downloaded

下面是工程中的例子,就是按上面的步骤来的。

public class MainApplication extends Application {
    private static final String TAG = "euler";

    private static final String APATCH_PATH = "/out.apatch";
    /**
     * patch manager
     */
    private PatchManager mPatchManager;

    @Override
    public void onCreate() {
        super.onCreate();
        // initialize
        mPatchManager = new PatchManager(this);
        mPatchManager.init("1.0");
        Log.d(TAG, "inited.");

        // load patch
        mPatchManager.loadPatch();
        Log.d(TAG, "apatch loaded.");

        // add patch at runtime
        try {
            // .apatch file path
            String patchFileString = Environment.getExternalStorageDirectory()
                    .getAbsolutePath() + APATCH_PATH;
            mPatchManager.addPatch(patchFileString);
            Log.d(TAG, "apatch:" + patchFileString + " added.");
        } catch (IOException e) {
            Log.e(TAG, "", e);
        }

    }
}

现实中应用out.patch 一般来自网络,下到本地,再去修复。那么这个修复补丁是怎么来的呢?
首先生成修复后的 fixed.apk,要和要修复的bug.apk(就是已经发出去的有错的)用同一个签名key,这个是肯定的,要不被人随便替换了。然后解压开源文件tools目录里的apkpatch-1.0.3工具。

把之前生成的bug.apk和fixed.apk,还有打包所使用的keystore文件放到apkpatch-1.0.3目录下
打开命令行,进入到apkpatch-1.0.3目录下,输入如下指令(Mac的操作)

apkpatch.sh -f fixed.apk -t bug.apk -o patchfile -k keystore -p 111111 -a 111111 -e 111111

每个参数含义如下

-f 新版本的apk
-t 旧版本的apk
-o 输出apatch文件的文件夹,可以随意命名
-k 打包的keystore文件名
-p keystore的密码
-a keystore 用户别名
-e keystore 用户别名的密码

如果出现add modified …….就表示成功了,去apkpatch-1.0.3目录看下,找到以.aptch 为结尾的文件,你可以随便从新命名。
接下来就可以尝试热修复了,先安装bug.apk,然后push .aptch 文件到相应的加载目录(实际中放在后台去),然后重启apk,就可以了。