Android 热更新调研汇总
16 年热更新技术大火,林林总总出现很多开源框架和方式,最近正好有时间进行调研比较。
Android 运行模式
先说下 Android 有三种运行模式:JNI、Dalvik、ARTdalvik 明显是最慢的,完全的 JNI 模式是最快的但是开发难度高,ART 介于两者之间,并且不影响现有开发模式
1)JNI:在开发过程中使用编译器在 C/C++等语言直接编译成机器码,运行的时候能够充分利用系统性能,这是最快的。iOS 的 Object C 和 Android 的 NDK 都是这种模式。
2)Dalvik:Android L 系统之前所有 Android 版本的运行方式,采用的是字节码,在运行的时候解释执行变成机器能够识别的机器码。这个过程是比较缓慢的。
3)ART:Android 4.4 开始推出的新的运行环境,在 APP 安装的时候使用 dex2oat 工具直接把 DEX 文件转换为机器码文件,运行的时候以机器码方式运行,可以充分利用系统性能;此外,改进的内存回收机制使得 ART 运行模式下的内存回收速度只有 Dalvik 运行时模式下的 50%,也能够提升系统运行速度。
ART 缺点:1)APP 安装过程会变慢;
2)APP 占用的存储空间会变多,系统更容易出现系统空间不足问题。
贴一下 classloader 机制
Java 中 ClassLoader 的基本概念
类加载器的树状结构:在 JVM 中,所有类加载器实例按树状结构组织,根结点为引导类加载器。除根结点外的所有类加载器都有一个非空的父类加载器,从而构成树状结构;
双亲委托(代理)模型:当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程;
代理模式是为了保证 Java 核心库的类型安全。通过代理模式,对于 Java 核心库的类的加载工作由 bootClassLoader 来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
类的判等:即使类完全相同(名称相同、字节码相同),不同类加载器实例加载的类对象也是不相等的;这条规则是 Java 类加载机制中非常核心的规则,它保证了类加载机制实现“类隔离”、“保护 JDK 中的基础类”等目标。
类的垃圾回收:只有当类加载器可被作为垃圾回收的前提下,其加载的类才有可能被回收
Android 的 classLoader 机制
Android 的 Dalvik/ART 虚拟机如同标准 JAVA 的 JVM 虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此可以利用这一点,在程序运行时手动加载 Class,从而达到代码中动态加载可执行文件的目的。
在 Android 系统启动的时候会创建一个 Boot 类型的 ClassLoader 实例,用于加载一些系统 Framework 层级需要的类。由于 Android 应用里也需要用到一些系统的类,所以 APP 启动的时候也会把这个 Boot 类型的 ClassLoader 传进来。
此外,APP 也有自己的类,这些类保存在 APK 的 dex 文件里面,所以 APP 启动的时候,也会创建一个自己的 ClassLoader 实例,用于加载自己 dex 文件中的类。
一个是 BootClassLoader(系统启动的时候创建的),另一个是 PathClassLoader(应用启动时创建的,用于加载当前已安装 app 里面的类)
Android 经常使用的是 PathClassLoader 和 DexClassLoader
PathClassLoader 官方注释:一个简单的 ClassLoader 的实现,工作在本地文件系统中的文件和目录的列表上,但不尝试从网络加载类。 Android 使用这个类为它的系统类加载器和应用类加载器。
可以看出,Android 是使用这个类作为其系统类和应用类的加载器。并且对于这个类呢,只能去加载已经安装到 Android 系统中的 apk 文件。
DexClassLoader 官方注释:一个 ClassLoader 的实现,从.jar 和.apk 文件内部加载 classes.dex。这可以用于执行非安装程序作为已安装应用程序的一部分的代码。
也就是说可以加载比如 sd 目录下的 dex 文件,获取到不是已安装 app 里面的类。
Android 中使用 PathClassLoader 类作为 Android 的默认的类加载器,PathClassLoade 本身继承自 BaseDexClassLoader,BaseDexClassLoader 重写了 findClass 方法,该方法是 ClassLoader 的核心。
classloader 特性
使用 ClassLoader 的一个特点就是,当 ClassLoader 在成功加载某个类之后,会把得到类的实例缓存起来。下次再请求加载该类的时候,ClassLoader 会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,如果程序不重新启动,加载过一次的类就无法重新加载。如果使用 ClassLoader 来动态升级 APP 或者动态修复 BUG,都需要重新启动 APP 才能生效。
阿里的 dexposed 和 AndFix 采用了 jni hook 方案 Android 程序比起一般 Java 程序在使用动态加载时麻烦在哪里 使用 ClassLoader 动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的 Java 程序,在 Android 程序中使用动态加载主要有两个麻烦的问题: * Android 中许多组件类(如 Activity、Service 等)是需要在 Manifest 文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作; * Res 资源是 Android 开发中经常用到的,而 Android 是把这些资源用对应的 R.id 注册好,运行时通过这些 ID 从 Resource 实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到 R.id 的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源 ID 根本和现有的 Resource 实例中保存的资源 ID 对不上; 说到底,一个 Android 程序和标准的 Java 程序最大的区别就在于他们的上下文环境(Context)不同。
Android 中 context 可以给程序提供组件需要用到的功能,也可以提供一些主题、Res 等资源,而现在的各种 Android 动态加载框架中,核心要解决的东西也正是如何给外部的新类提供上下文环境的问题。
当前框架大致分为两种类型
1.基于 Native hook 方式
AndFix(阿里)
无需重启
andfix 只能替换方法而不能增减新的字段,也不能下发类
支持 2.3~6.0,但是兼容性以及稳定性较差,关键是不需要重启。
启动时间几乎无增加,不增加运行期额外的磁盘消耗。
缺点:
不支持 YunOS
无法添加新类和新的字段
需要使用加固前的 apk 制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露。
使用加固平台可能会使热补丁功能失效(演示样例时已经验证过)。
不能更换资源文件、不能添加类等。
兼容性问题
部分手机奔溃,部分手机 ANR
不能改变量的值,不过方法的添加修改,删除,都可以
需要注意多进程
ART 下模式无法对同一个方法进行多次更新
Dexposed(阿里)
优点:缺点:不支持 art(5.0+) 比较致命,如果线上 release 版本进行了混淆,那写 patch 也是一件很痛苦的事情,反射+内部类
2.基于 classloader(Multidex)
这类具体就是搞 Android 的 ClassLoader 体系,android 中加载类一般使用的是 PathClassLoader 和 DexClassLoader。
手空(超级补丁技术)
未开源(DEX 分包方案)
超级补丁方案通过修改系统加载程序的顺序实现热补丁,也就是老文章中提到的 ClassLoader 方法。这种对系统加载流程的修改,在老版本(Dalvik 虚拟机)的 Android 手机上会造成程序运行性能下降,而为了避免在新版本(ART 虚拟机)Android 系统上的稳定性问题,又不得不在补丁包中加入大量的冗余信息,导致补丁包过大。
超级补丁技术基于 DEX 分包方案,使用了多 DEX 加载的原理,大致的过程就是:把 BUG 方法修复以后,放到一个单独的 DEX 里,插入到 dexElements 数组的最前面,让虚拟机去加载修复完后的方法。
优势:
没有合成整包(和微信 Tinker 比起来),产物比较小,比较灵活
可以实现类替换,兼容性高。整篇由上层 java 语言主导,不存在版本兼容问题,成功率最高 (某些三星手机不起作用)
不足:
不支持即时生效,必须通过重启才能生效。
为了实现修复这个过程,必须在应用中加入两个 DEX!dalvikhack.dex 中只有一个类,对性能影响不大,但是对于 patch.dex 来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加 2s 以上是不能够接受的事。
在 ART 模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到 patch.dex 中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。
Nuwa(点评,类似手空原理)
优点:
比较简单
缺点:
都基于 ClassLoader,兼容性以及稳定性好,支持 4.0~6.0,对于启动速度有影响,补丁重启后才能生效 ART 环境支持的不好。i50 nuwa 只支持 4.0 以上,并不是作者所描述的 2.3~6.0,作者使用的 BaseDexClassLoader 这个 4.0 才有的 API,导致 4.0 以下的手机用 Nuwa 就必定崩溃,特此提醒
RocooFix(美团,Nuwa 改动, Instant Run 方案,native hook)
RocooFix 支持两种模式:1、静态修复某种情况下需要重启应用。 (推荐使用)2、动态修复,无需重启应用即可生效。
HotFix(百度)
没啥特点。
DroidFix
没啥特点。
Tinker(微信,整体替换 DEX 的方案)
优势:
合成整包,不用在构造函数插入代码,防止 verify,verify 和 opt 在编译期间就已经完成,不会在运行期间进行。
性能提高。兼容性和稳定性比较高。
开发者透明,不需要对包进行额外处理。
不足:
与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。
需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。
合并时占用额外磁盘空间,对于多 DEX 的应用来说,如果修改了多个 DEX 文件,就需要下发多个 patch.dex 与对应的 classes.dex 进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。
派生的增量更新
热修复的坑和解
我们知道,多 DEX 方案用来解决应用方法数 65k 的问题,现在 Google 也官方支持了 MultiDex 的实现方案。但是,这实在是应用因方法数超出而作出的不得已的下策,但是超级补丁技术和 Tinker 作为一种热修复的方案,平生给应用增加了多个 DEX,而多 DEX 技术最大的问题在于性能上的坑,因此基于这种方案的补丁技术影响应用的性能是无疑的。
启动加载时间过长
我们可以看到,超级补丁技术和 Tinker 都选择在 Application 的 attachBaseContext()进行补丁 DEX 的加载,即使这是加载 dex 的最佳时机,但是依然会带来很大的性能问题,首当其冲的就是启动时间太长。
对于补丁 DEX 来说,应用启动时虚拟机会进行 dexopt 操作,将 patch.dex 文件转换成 odex 文件,这个过程非常耗时。而这个过程,又要求需要在主线程中,以同步的方式执行,否则无法成功进行修复。就 DEX 的加载时间,大概做了以下的时间测试。
易造成应用的 ANR 和 Crash
正是尤其多 DEX 加载导致了启动时间过长,很容易就会引发应用的 ANR。我们知道当应用在主线程等待超过 5s 以后,就会直接导致长时间无响应而退出。超级补丁技术为保证 ART 不出现地址错乱问题,需要将所有关联的类全部加入到补丁中,而微信 Tinker 采取一种差量包合并加载的方式,都会使要加载的 DEX 体积变得很大。这也很大程度上容易导致 ANR 情况的出现。
除了应用 ANR 以外,多 DEX 模式也同样很容易导致 Crash 情况的出现。我们知道,超级补丁技术为了保证 ART 设备下不出现地址错乱问题,需要把修改类的所有相关类全部加入到补丁中,这里会出现一个问题,为了保证补丁包的体积最小,能否保证引入全部的关联类而不引入无关的类呢?一旦没有引入关联的类,就会出现以下的异常:
NoClassDefFoundError
Could not find class
Could not find method
总结
QQ 空间超级补丁技术和微信 Tinker 支持新增类和资源的替换,在一些功能化的更新上更为强大,但对应用的性能和稳定会有的一定的影响;阿里百川 HotFix 虽然暂时不支持新增类和资源的替换,对新功能的发布也有所限制,但是作为一项定位为线上紧急 BUG 的热修复的服务来说,能够真正做到 BUG 即时修复用户无感知,同时保证对应用性能不产生不必要的损耗,在热修复方面不失为一个好的选择。
阿里百川 HotFix:启动时间几乎无增加,不增加运行期额外的磁盘消耗。
QQ 空间超级补丁技术:如果应用有 700 个类,启动耗时增加超过 2.5s,达到 5.5s 以上。
微信 Tinker:假设应用有 5 个 DEX 文件,分别修改了这 5 个 DEX,产生 5 个 patch.dex 文件,就要进行 5 次的 patch 合并动作,假设每个补丁 1M,那么就要多占用 7.5M 的磁盘空间。
所以 so...
基于公司要制作自己的 sdk 供第三方使用,选用微信的 Tinker 和阿里的 AndFix,因为 Tinker 比较成熟稳定,AndFix 可以即使生效,试着集成这两种具体方案,后面贴出两种集成方案代码。
评论