这篇文章分享了笔者近几个月在插件和热补丁技术方面的一些经验积累以及我们开发的动态加载框架Stardust.
Stardust是什么
针对Android平台,集热更新热修复于一体的解决方案,一套机制解决两个问题。
它主要包括三个部分:
- Stardust sdk: 提供对插件、补丁动态加载的能力
- 插件打包工具(gradle脚本): 主要做了两件事,编译后期修改R.java 的pacakgeId字段, 裁剪宿主与插件共同依赖的公共库,这里要感谢Small的作者林光亮同学,我们很大程度上借鉴了Small的思路。
- 补丁生成工具 dexpatcher: 基于dex2jar 比较class的改变,将更改后的class打包到同一个dex中,生成补丁。
简单的背景介绍
-
简介插件化历史
Qihoo360/DroidPlugin
CtripMobile/DynamicAPK
mmin18/AndroidDynamicLoader
singwhatiwanna/dynamic-load-apk
houkx/android-pluginmgr
bunnyblue/ACDD
wequick/Small
主要分为两类:
- 加载独立插件的框架(DroidPlugin, ACDD)
-
加载非独立插件的框架 (Small, DynamicLoadApk)
关于独立插件和非独立插件的解释参见之前写过的一篇文章
主流插件框架对比
方案 | DroidPlugin、ACDD | Dynamic-Load-Apk | Small |
---|---|---|---|
原理 | 沙盒 加载独立插件 | 代理模式 非独立插件 | 组件化方案 非独立插件 |
缺陷 | 兼容性问题 | 需要写代理方法;对于资源的访问受限 | 不支持即时生效,需要重启进程;首次启动性能较差 |
总体来说:
1.沙盒的方案是最极致 也是开发成本最高的,但对兼容性和稳定性提出了很高的要求
2.Small修改资源packageId方案的思路值得借鉴,但不本身不适于产品化,比如首次启动插件的性能问题以及一些兼容性的问题
2.热补丁历史
Java流派:
1)更改classloader加载dex顺序,同时绕过pre-verified:qq空间,nuwa, qfix, robust
2)dex合成:tinkerNative流派: AndFix
| 方案 | Tinker | QZone |AndFix|
| -------- | --------| -- |
| 原理 | 反射classloader+ dexdiff 全量合成 | hack classloader + 插桩 | native hook |
| 缺陷 | 合成逻辑复杂,感觉较重 | 运行时性能受到插桩机制的影响 略差 | 兼容性稳定性较差 |
更详细的比较可以参考Tinker(https://github.com/Tencent/tinker/wiki)
Stardust介绍
Stardust 插件与补丁的文件结构
这张图中我们需要了解以下几点:
文件结构
插件和补丁的文件均已.bundle为后缀,用以区分普通的apk文件,本质上也是一个zip, 插件结构与apk的结构是保持一致的,支持dex、.so以及resource的动态加载;补丁只应该存在dex文件。插件,补丁与宿主的关系
插件中的class与宿主中的class是新增关系,补丁中的class与宿主中的class是替换关系,形式上的不同决定了两者加载机制上的不同,因此对于插件可以支持热加载,插件安装成功后即可运行,对于补丁存在class缓存的问题,只能在安装成功重启进程后才能生效。如何区分插件还是补丁?
在xxx.bundle的AndroidManifest.xml中用一个meta字段用于判断bundle的类型,以便处理不同的加载逻辑。公共库的依赖管理
由于插件和补丁是运行时加载,因此要保证插件,补丁所依赖公共库版本的一致性,设计一套完整可靠的协作开发机制,这里有一个很必要的原则:发布的插件中应该有且仅有自身业务相关的class,resource以及so, 对于其依赖的公共库我们通过白名单的机制在插件的编译脚本中做了裁剪,同时还要考虑插件自身的版本控制,以及插件与宿主版本的依赖控制,不会因为插件或宿主的升级而导致运行崩溃。如何保证多进程加载补丁的一致性?
在Stardust中对于补丁文件我们只允许在主进程加载补丁成功后,其他进程才可加载,否则不能被加载。补丁的生成方式
由于我们是基于dex2jar的方式来比较前后两个apk 生成patch,因此需要主要前后两个apk的混淆规则要保持一致,如果发生变化,需要在混淆配置中通过applymapping进行控制
Stardust框架演进过程
Stardust的开发之路并不是顺利的,最初时并没有想的十分清楚,对于四大组件的支持到什么样的程度?
是否真的有必要支持四大组件的动态更新?
对于热补丁采用哪种机制,Tinker/Qzone/AndFix/QFix?,
这些问题当时都很难回答,也是在开发中不断探索,不断汲取别人的经验,过程中我们借鉴了Small以及QFix设计上的很多思路,对此表示感谢,日后我们也会在Stardust更完善的时候将它开源,与大家共同学习成长。
Stardust的最终形态
- 全平台兼容(Dalvik && Art)
- 支持热更新、热修复,同时支持插件、补丁的动态加载:不支持四大组件的动态加载,插件中用到的四大组件全部需要在宿主中预先注册。
- 性能:调整插件的加载时序,将相对耗时的操作放在后台,这里主要针对optDex操作
- 对于插件支持热加载,无需重启进程;对于补丁,需要重启进程后才能加载
- 支持插件补丁的版本管理
-
鲁棒:
- 轻量级的hook: 减少反射以及对系统接口的hook, 仅反射修改了Classloader loadDex以及AssetManager addAssetPath接口。
- 补丁回退机制:如果检测到补丁机制不兼容会将补丁卸载
- 错误状态监控:我们对于插件加载失败可能出现的原因加了几十个错误状态信息的errCode,同时进行上报统计。
效果
目前整套机制还在产品的灰度测试中,已覆盖2000+用户
- 补丁的加载成功率在98%(补丁运行成功的用户数/启动补丁的用户数)
- 插件的加载成功率在99%(启动插件成功的用户数/启动插件的用户数)
为什么不支持四大组件的动态加载?
最开始的开发中我们已经开发了对四大组件动态加载的原型,在其后的开发中我们也在不断思考动态化加载方案的优势
- 利于不同业务模块的解耦
- 缩减apk大小
- 便于不同业务间的并行开发
而引入hook 四大组件加载的机制对于稳定性、兼容性也提出了更高的要求,以及对于插件的进程管理也需要统一的维护管理,反而不如注册在宿主的AndroidManifest.xml中交由交由系统管理,各自的生命周期,对此仅需要规范的开发流程即可保证。
总结
以上是笔者在插件和补丁的技术研究中的一些收获,目的是为了提供一个新的思路,关于具体的实现技术细节并没有做展开的阐述,感兴趣的同学可以参考提供链接