Android客户端插件化热修复学习总结

Posted by 晓晨DEV on July 19, 2016

作者:晓晨DEV

2016年不能扯几句热修复和插件化都不好意思说自己是做 Android 的,虽然我对这个技术不怎么感兴趣,奈何业务需要也得深入的研究一下,本文记录我对热修复的插件化的学习和研究。

技术背景

插件化解决的问题

  1. 减小主包大小
  2. 不发版上新功能
  3. 独立开发加载 A/B TEST 模块
  4. bug 修复工具

个人态度

学习这项技术是关心技术后的本质,在项目中能不用就不用,因为本身这种做法是 Google 不推荐的,RN才是今后的发展方向。但是RN性能方面的优化也很重要,这方面没深入了解。

在项目中引入新技术

在项目中使用新技术,哪怕是引入一个第三方库也要小心谨慎,确定是否确实需要。但是还是要对不了解的新的东西要积极学习、积极研究,这样在引入实际生产环境后也能快速踩坑出坑。

个人认为开发不能脱离业务,不能脱离实际场景为了技术而技术,也不能害怕引入新技术,风险和效率等优点是并存的,但是要合理的控制风险。

介绍名词

  1. 插件化 – apk 分为宿主和插件部分,插件在需要的时候才加载进来

  2. 热修复 – 更新的类或者插件粒度较小的时候,我们会称之为热修复,一般用于修复bug

  3. 热更新 – 2016 Google 的 Android Studio 推出了Instant Run 功能 同时提出了3个名词

    “热部署” – 方法内的简单修改,无需重启app和Activity。 “暖部署” – app无需重启,但是activity需要重启,比如资源的修改。 “冷部署” – app需要重启,比如继承关系的改变或方法的签名变化等。

所以站在app开发者角度的“热”是指在不发版的情况来实现更新,而Google提出的“热”是指值是否需要重新启动。 同时在开发插件化的时候也有两种情景,一种是插件与宿主apk没有交互,只是在用户使用到的时候进行一次吊起,还有一种是与宿主有很多的交互。

从MulitiDex开始

当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt 。DexOpt 的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODex 的效率会比直接执行 Dex 文件的效率要高很多。

但是在早期的 Android 系统中,DexOpt 有一个问题,DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结 构里面。但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的 Android 系统中,DexOpt 修复了这个问题,但是我们仍然需要对低版本的 Android 系统做兼容。

为了解决方法数超限的问题,需要将该dex文件拆成两个或多个,为此谷歌官方推出了 multidex 兼容包,配合 AndroidStudio 实现了一个 APK 包含多个 dex 的功能。


MulitDex 引起的问题有:

  1. 在应用安装到手机上的时候dex文件的安装是复杂的(complex)有可能会因为第二个dex文件太大导致ANR。
  2. 使用了mulitDex的App有可能在4.0(api level 14)以前的机器上无法启动,因为Dalvik linearAlloc bug(Issue 22586) 。
  3. 使用了mulitDex的App在runtime期间有可能因为Dalvik linearAlloc limit (Issue 78035) Crash。该内存分配限制在 4.0版本被增大,但是5.0以下的机器上的Apps依然会存在这个限制。
  4. 主dex被dalvik虚拟机执行时候,哪些类必须在主dex文件里面这个问题比较复杂。build tools 可以搞定这个问题。但是如果你代码存在反射和native的调用也不保证100%正确。

对于 davilk 和 art 虚拟机 Mulitdex 的不同:

  1. ART模式相比原来的Dalvik,会在安装APK的时候,使用Android系统自带的dex2oat工具把APK里面的.dex文件转化成OAT文件。

这里说一下罗迪的快速加载 Mulitdex 方案:art虚拟机对dex优化需要很长时间,加载插件dex跳过优化实现加速。跳过会影响类加载的性能。

插件化实现方案分析对比

下面对实现插件化需要关注的一些点,和主流插件化框架进行了一些总结分析。

实现插件化需要解决的技术点

  1. 资源如何加载(资源冲突问题如何解决)?

  2. 代码如何加载访问访问?

  3. 插件的管理后台包括的内容?

  4. 插件的增量更新问题(非必须) ?

  5. 加载插件中的so库 (非必须)?

针对如上问题的解决方案

  • 针对问题1

    • 方案一: 将插件apk资源解压,通过操作文件的方式来使用,这个只是理论上可行,实际使用的时候还是有很多的问题。(主要是混淆后就懵逼了)

    • 方案二: 重写 Context 的getResource() getAsset() 之类的方法。资源冲突需要扩展 aapt 实现。

    • 方案三: 打包后执行一个脚本修改资源ID。

    原理:

    资源id是在编译时生成的,其生成的规则是0xPPTTNNNN,PP段,是用来标记apk的,默认情况下系统资源PP是01,应用程序的PP是07。TT段,是用来标记资源类型的,比如图标、布局等,相同的类型TT值相同,但是同一个TT值不代表同一种资源,例如这次编译的时候可能使用03作为layout的TT,那下次编译的时候可能会使用06作为TT的值,具体使用那个值,实际上和当前APP使用的资源类型的个数是相关联的。NNNN则是某种资源类型的资源id,默认从1开始,依次累加。

    那么我们要解决资源id问题,就可从TT的值开始入手,只要将每次编译时的TT值固定,即可是资源id达到分组的效果,从而避免重复。例如将宿主程序的layout资源的TT固定为33,将插件程序资源的layout的TT值固定为03(也可不对插件程序的资源id做任何处理,使其使用编译出来的原生的值), 即可解决资源id重复的问题了。

    固定资源id的TT值的办法也非常简单,提供一份public.xml,在public.xml中指定什么资源类型以什么TT值开头即可

    还有一个方法是通过定制过的aapt在编译时指定插件的PP段的值来实现分组,重写过的aapt指定PP段来实现id分组。

  • 针对问题2

    • 方案一: 简单加载模式。

    • 方案二: Activity代理模式。

    • 方案三: 动态加载模式。

    原理说明:主要就是 classloader 加载dex,代理模式就是本身宿主中有Activity,通过欺骗系统来创建Activity,欺骗系统的部分hook的有深有浅(对比DroidPlugin和Small),让这个Activity有生命周期,而动态加载模式就是运行时动态创建并编译一个Activity类,需要使用动态创建类的工具实现动态字节码操作。

  • 针对问题3:

    • 提供插件信息查询和下载,包括回滚到某一版本。

    • 管理使用插件的apk,可以向不同版本apk 提供不同插件。

    • MD5校检插件的合法性。

插件化现有框架分析对比

框架名称 出现时间 作者 实现简介 已知存在问题  
AndroidDynamicLoader 2012年7月 mmin18 不使用Activity采用Fragment实现    
DynamicAPK   携程 扩展aapt解决资源问题    
android-pluginmg   houkx 动态创建Activity来实现插件化    
DynamicLoadApk 2014年底 百度工程师 任玉刚 通过代理Activity来实现插件化    
DroidPlugin 2015年8月 奇虎360 深度hook实现 不支持通知栏定制  
Small 2015年底 林光亮 比较DroidPlugin轻量一点,脚本来解决资源问题 不支持Service插件化  
ACCD 2015年4月 bunnyblue OpenAtlas 之后改为ACCD 携程开源框架参考了这个    
nuwa 2015年9月 贾吉新 通过前置相同Dex来实现热修复    
AndFix 2015年11月 阿里 使用JNI实现的热修复,针对Davilk和Art调用的方法不同    
Dexposed   阿里   不支持Art虚拟机  

注意:关于存在的问题 作者只看了部分框架的,列举的也是可能影响作者自身业务的点,仅供参考

后记

差不多找了一周的资料加上看各种文章源码写完这个文章,很多地方的了解不是那么深入,很多东西也是拾人牙慧,希望大家批评指正。

码字不易,各位大哥大姐转载麻烦带上署名。


分享