26人阅读

Android热修复详解

0
Tinker 阿里百川HotFix QZone AndFix Robust Dexposed
类替换 yes yes yes no no no
So替换 yes yes no no no no
资源替换 yes yes yes no no no
全平台支持 yes yes yes yes yes no
即时生效 no yes no yes yes yes
性能损耗 较小 较小 较大 较小 较小 较小
补丁包大小 较小 较小 较大 一般 一般 一般
开发透明 yes yes yes no no no
复杂度 较低 较低 较低 复杂 复杂 复杂
gradle支持 yes yes no no no no
接口文档 丰富 丰富 一般 一般 一般 较少
Rom体积 较大 较小 较小 较小 较小 较小
成功率 较高 较高 较高 一般 最高 一般
补丁管理后台 yes yes no no no no

##Dexposed

作者:手淘团队
修复粒度:方法级别
实现原理:基于 Xposed 实现的无侵入的运行时 AOP 框架
支持系统版本:很有限,如下图。

Runtime Android Version Support
Dalvik 2.2 Not |Test
Dalvik 2.3 Yes
Dalvik 3.0 No
Dalvik 4.0-4.4 Yes
ART 5.0 Testing
ART 5.1 No
ART M No

Dexposed支持函数级别的在线热更新。
Dexposed是基于久负盛名的开源Xposed框架实现的一个Android平台上功能强大的无侵入式运行时AOP框架。
####1、关于之前的Xposed框架

通过进行设备root之后,替换/system/bin/app_process程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。

Android中的所有程序都是通过app_process命令启动,而且app_process命令还可以启动任何Java程序

app_process [java-options] cmd-dir start-class-name [options]

在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例。

Zygote进程在启动的过程中,除了会创建一个Dalvik虚拟机实例之外,还会将Java运行时库加载到进程中来,以及注册一些Android核心类的JNI方法来前面创建的Dalvik虚拟机实例中去。注意,一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库。这也就是可以将XposedBridge这个jar包加载到每一个Android应用程序中的原因。XposedBridge有一个私有的Native(JNI)方法hookMethodNative,这个方法也在app_process中使用。这个函数提供一个方法对象利用Java的Reflection机制来对内置方法覆写。

这就是Xposed框架选择app_proess作为入口的一个原因,因为这个入口有两个好处:
1、一旦修改了,就可以修改了所有的app
2、这里的时机是最早的,可以加载所有的东西

####2、修改非native方法为native方法

Alt text
我们了解到要对一个java函数进行hook需要步骤有
* [1] 把修改method的属性修改成native
* [2] 修改method的registersSize、insSize、nativeFunc、computeJniArgInfo
* [3] RegisterNatives注册目标method的native函数

然后我们将系统获取Mac地址的方法getMacAddress和我们自己的一个函数做一次注册:
{“android/net/wifi/WifiInfo”,”getMacAddress”,”()Ljava/lang/String;”,(void*)test}
那么这里的test函数就是我们自己想要做事的函数,可以返回任意值啦~~

这个知识点很重要,也是我们今天说的核心知识点。

####3、dexposed原理
好了到这里我们就分析完了native层的代码,下面来总结一下:
* 1、首先在JNI_OnLoad函数中做了三件事:
* 获取设备信息dexposedInfo
* 注册JNI方法(com_taobao_android_dexposed_DexposedBridge_hookMethodNative)
* 初始化信息(获取Java层DexposedBridge中方法的Method对象)
* 2、然后在com_taobao_android_dexposed_DexposedBridge_hookMethodNative函数中主要做了两件事:
* 把Java层传递的信息构造成DexposedInfo信息
* 设置hook方法为native,并且指定nativeFunc函数
* 3、最后在执行第二步中的nativeFunc函数dexposedCallHandler函数中主要做了两件事:
* 获取刚刚构造的DexposedInfo信息
* 调用Java层DexposedBridge.java中的handleHookedMethod方法

所以我们在整个过程中可以看到,先通过JNI注册,从Java世界转到Native世界,然后在native世界中主要修改被hook方法的一些信息,然后在通过反射调用handleHookedMethod回到Java世界。

####4、总结
* 1、Dexposed框架是免root实现hook的,但是他也有局限性就是只能hook本应用中的一些方法。
* 2、Dexposed框架hook方法的原理,就是首先修改需要hook的方法为native方法,然后再设置nativeFunc值为我们需要处理的函数然后这个函数在用反射机制调用上层的Java代码即可
* 3、Dexposed框架实现热修复,是基于hook技术,每个需要修复的模块都需要遵循一个协议,然后采用hook技术,去hook掉需要修复的方法,实现替换功能。

总结Dexposed框架其实就一句话:
找到比较好的时机(JNI_OnLoad),修改hook方法,然后挂钩上层Java代码,实现偷龙转凤的功能

####5、注意点
* 修复模块中不要引用宿主模块中已经有的jar;
* 每个修复类必须遵从IPatch协议接口,定义需要修复的类和函数;
* 写补丁有点困难,需要反射写混淆后的代码;

##Andfix

作者:阿里技术团队
修复粒度:方法级别
实现原理:native hook 方式
支持系统版本:支持Dalvik和ART两种运行时环境,同时它支持Android2.3 – 6.0版本,支持arm和x86架构CPU的设备

####原理浅析
.apatch实际是一个压缩文件,解压后如下:
Alt text
meta-inf文件夹为:
Alt text

打开patch.mf文件可以发现两个apk的差异信息:

Manifest-Version: 1.0
Patch-Name: app-release-fix
To-File: app-release-online.apk
Created-Time: 30 Mar 2017 16:16:27 GMT
Created-By: 2.0.0 (ApkPatch)
Patch-Classes: com.showjoy.shop.activity.me.AboutActivity_CF
From-File: app-release-fix.apk

这个Patch-CLasses标志了哪些类有修改,这里会显示完全的类名同时加上一个_CF后缀。AndFix首先会读取这个文件里面的东西,保存在Patch类的一个对象里,备用。

通过dlsym系统调用获得了dalvik的_Z20dvmDecodeIndirectRefP6ThreadP8_jobject和_Z13dvmThreadSelfv函数指针,这里还针对apilevel是否大于10进行判断。

这两个函数在后面的替换Method中是直接用到的,换句话而已,AndFix实际上最终是调用了dalvik的上述两个方法来获取源方法和目标方法的句柄,从而进行“方法粒度”的无感知替换,当虚拟机误以为方法还是之前的“方法”。

AndFix对于libandfix.so提供了两个实现,一个是Dalvik的一个是ART的。

HotFix 的使用中不被允许的情况

  • 暂时不支持新增方法、新增类
  • 不支持新增 Field
  • 不支持针对同一个方法的多次 patch,如果客户端已经有一个 patch 包在运行,则下一个 patch 不会立即生效。
  • 三星 note3、S4、S5 的 5.0 设备以及 X8 6设备不支持(点击查看具体支持的机型)
  • 参数包括:long、double、float 的方法不能被 patch
  • 被反射调用的方法不能被 patch
  • 使用 Annotation 的类不能 patch
  • 参数超过 8 的方法不能被 patch
  • 泛型参数的方法如果 patch 存在兼容性问题

Qzone方案

与之类似的有RocooFix和Nua,修复范围Class级的替换(仅包括java文件, 不支持替换资源文件, so文件等), 而且需要在获取补丁后再次启动方可生效, 并将影响app启动速度(如果项目已经分包就忽略这条), 插桩具有一定的侵入性. 整个方案包括运行时工具和编译时工具, 运行时工具大致就是一套multidex框架, 编译时工具则主要是生成差异包和插桩.

为了完善的热修复方案需要考虑到一些点:
安全策略;
重启管理;
补丁更新;
应用版本升级.
整体运行架构如图:
Alt text

当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行

  • Dalvik: 在 dexopt 过程,若 class verify 通过会写入 pre-verify 标志,在经过 optimize 之后再写入odex文件。这里的 optimize 主要包括 inline 以及 quick 指令优化等。
    若采用插桩导致所有类都非 preverify,这导致 verify 与 optimize 操作会在加载类时触发。这会有一定的性能损耗,微信分别采用插桩与不插桩两种方式做过两种测试,一是连续加载700个50行左右的类,一是统计微信整个启动完成的耗时。
    Alt text

  • Art: Art 采用了新的方式,插桩对代码的执行效率并没有什么影响。但是若补丁中的类出现修改类变量或者方法,可能会导致出现内存地址错乱的问题。为了解决这个问题我们需要将修改了变量、方法以及接口的类的父类以及调用这个类的所有类都加入到补丁包中。这可能会带来补丁包大小的急剧增加。

  • Alt text

这里是因为在 dex2oat 时 fast* 已经将类能确定的各个地址写死。如果运行时补丁包的地址出现改变,原始类去调用时就会出现地址错乱。

总的来说,Qzone 方案好处在于开发透明,简单,这一套方案目前的应用成功率也是最高的,但在补丁包大小与性能损耗上有一定的局限性。特别是无论我们是否真正应用补丁,都会因为插桩导致对程序运行时的性能产生影响。微信对于性能要求较高,所以我们也没有采用这套方案。

Robust

Robust是美团的热修复方案。
Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明。如State.java的getIndex函数:

public long getIndex() {
     return 100;
}

被处理成如下的实现:

public static ChangeQuickRedirect changeQuickRedirect;
    public long getIndex() {
        if(changeQuickRedirect != null) {
            //PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
            if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
                return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
            }
        }
        return 100L;
    }

可以看到Robust为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。
如果需将getIndex函数的返回值改为return 106,那么对应生成的patch,主要包含两个class:PatchesInfoImpl.java和StatePatch.java。
PatchesInfoImpl.java:

public class PatchesInfoImpl implements PatchesInfo {
    public List<PatchedClassInfo> getPatchedClassesInfo() {
        List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
        PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
        patchedClassesInfos.add(patchedClass);
        return patchedClassesInfos;
    }
}

StatePatch.java:

public class StatePatch implements ChangeQuickRedirect {
    @Override
    public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
            return 106;
        }
        return null;
    }

    @Override
    public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
            return true;
        }
        return false;
    }
}

客户端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex后,用DexClassLoader加载patch.dex,反射拿到PatchesInfoImpl.java这个class。拿到后,创建这个class的一个对象。然后通过这个对象的getPatchedClassesInfo函数,知道需要patch的class为com.meituan.sample.d(com.meituan.sample.State混淆后的名字),再反射得到当前运行环境中的com.meituan.sample.d class,将其中的changeQuickRedirect字段赋值为用patch.dex中的StatePatch.java这个class new出来的对象。这就是打patch的主要过程。通过原理分析,其实Robust只是在正常的使用DexClassLoader,所以可以说这套框架是没有兼容性问题的。
Alt text

在编译阶段有插件侵入了产品代码,对运行效率、方法数、包体积还是产生了一些副作用。

so和资源的替换目前暂时未做实现
##Tinker

Alt text

简单来说,在编译时通过新旧两个 Dex 生成差异 path.dex。在运行时,将差异 patch.dex 重新跟原始安装包的旧 Dex 还原为新的 Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch 中。为了补丁包尽量的小,微信自研了 DexDiff 算法,它深度利用 Dex 的格式来减少差异的大小。它的粒度是 Dex 格式的每一项,可以充分利用原本 Dex 的信息,而 BsDiff 的粒度是文件,AndFix/QZone 的粒度为 class。

这套方案并非没有缺点,它带来的问题有两个:

  • 占用 Rom 体积;这边大约是你修改 Dex 数量的1.5倍(dexopt 与 dex 压缩成jar)的大小。
  • 一个额外的合成过程;虽然我们单独放在一个进程上处理,但是合成时间的长短与内存消耗也会影响最终的成功率。

Alt text

#####总的来说:

AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。
#####加载补丁资源
Tinker的资源更新采用的InstantRun的资源补丁方式,全量替换资源。由于App加载资源是依赖Context.getResources()方法返回的Resources对象,Resources 内部包装了 AssetManager,最终由 AssetManager 从 apk 文件中加载资源。我们要做的就是新建一个AssetManager(),hook掉其中的addAssetPath()方法,将我们的资源补丁目录传递进去,然后循环替换Resources对象中的AssetManager对象,达到资源替换的目的。看下代码实现。
#####Tinker的已知问题

由于原理与系统限制,Tinker有以下已知问题:

Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
在Android N上,补丁对应用启动时间有轻微的影响;
不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新;
对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

##为什么使用Bugly热更新?

  • 无需关注Tinker是如何合成补丁的
  • 无需自己搭建补丁管理后台
  • 无需考虑后台下发补丁策略的任何事情
  • 无需考虑补丁下载合成的时机,处理后台下发的策略
  • 我们提供了更加方便集成Tinker的方式
  • 我们通过HTTPS及签名校验等机制保障补丁下发的安全性
  • 丰富的下发维度控制,有效控制补丁影响范围
  • 我们提供了应用升级一站式解决方案

参考文章:
微信 Android 热补丁实践演进之路
安卓App热补丁动态修复技术介绍
Android热更新方案Robust
Android热补丁之Tinker原理解析
为什么选择阿里百川HotFix?

0