本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/57ad7a70eaed47bb2699e68egit
Dev Club 是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是通过审核的移动开发工程师。每周都会举行嘉宾分享,话题讨论等活动。github
本期,咱们邀请了腾讯WXG Android开发工程师——张绍文,为你们分享**《微信热补丁Tinker的实践演进之路》**。算法
分享内容简介: Tinker 是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So库以及资源,让应用可以在不须要从新安装的状况下实现更新。这里大体介绍 Tinker 的实现原理,当时遇到的各类坑以及对它各个方面性能的优化工做。安全
内容大致框架:性能优化
下面是本期分享内容整理微信
hello,你们好。我是张绍文,目前在微信主要负责 Android 的性能优化以及终端质量平台相关工做。app
下面开始咱们今天的分享。框架
热补丁技术是当前很是热门的 Android 开发技术,其中比较出名的方案有支付宝的 AndFix以及** QZone 的超级热补丁**方案。ide
微信大约在2015年6月开始尝试应用,通过研究与尝试现有的各个方案,咱们发现它们都有着自身的一些局限性。咱们最终采用不一样于它们的技术方案,自研微信热补丁开源框架 Tinker。性能
下面咱们先来说讲先有框架的一些局限性。
Andfix 是阿里推出的开源框架,它在 github 的地址是:
它的技术原理以下图:它采用 native hook 的方式,这套方案直接使用 dalvik_replaceMethod
替换 class 中方法的实现。
它的缺点主要包括如下几个:
**兼容性不佳;**因为它采用 native 替换的方式,在 github Issue 中也有大量崩溃的反馈;
**成功率不高;**不支持修改 inline 方法,不支持修改方法参数超过8个或参数中带有 long, double 或者 float。跟一些使用 Andfix 的产品讨论过,它们的成功率不超过40%;
**缘由:**只替换了 DexCache 中的 ArtMethod 结构体,对于 Art 中一些 compiledCode 是直接经过 bx 过去
Andfix 的好处是能够马上生效,但它能够支持的补丁场景很是有限,仅仅可使用它来修复特定问题。
因此咱们不考虑采用这个方案。
如今咱们讲讲 Qzone 超级补丁方案,在腾讯内部已开源。
这个方案使用 classloader 的方式,能实现更加友好的类替换。并且这与咱们加载 Multidex 的作法类似,能基本保证稳定性与兼容性。
它主要的面临问题有两个:
为了解决 unexpected DEX problem
异常,而采用插桩的方式给全部类插入不会真正运行的代码,防止类打上 preverify 标志。 采用插桩致使全部类都非 preverify,致使上图中的 verify 与 optimize 操做会在加载类时触发。这会有必定的性能损耗,微信分别采用插桩与不插桩两种方式作过两种测试,一是连续加载700个50行左右的类,一是统计微信整个启动完成的耗时。
在 art 平台,若补丁中的类出现 Field、Method 或 Interface 变化,可能会致使出现内存地址错乱的问题。为了解决这个问题,咱们最后补丁中的类要有如下规则: **a. **修改跟新增的 class; **b. **若 class 有 field,method 或 interface 数量变化,它们全部的子类; **c. **若 class 有 field,method 或 interface 数量变化,它们以及它们全部子类的调用类。若是采用 ClassN 方式,即须要多个 dex 一块儿处理。
Qzone 的方案最为简单,并且开发透明,补丁的成功率也是很是高的。
但因为微信对于运行性能以及补丁大小都比较敏感,咱们最终也没有采用这套方案。
那么微信但愿的是一套怎么样的热补丁框架呢,咱们认为主要的目标有如下几个:
如今咱们来说讲微信热补丁框架 Tinker 的实现,目前在腾讯内部已开源。
它的名字来至 Dota 中的地精修补匠,咱们但愿发版本能够像它同样作到无限刷新。
Tinker 的方案来源 gradle 编译的 instant run 与 buck 编译的 exopackage。它们的思想都是全量替换新的 Dex。即咱们彻底使用了新的 Dex,那样既不出现 Art 地址错乱的问题,在 Dalvik 也无须插桩。
可是 instant run 针对的是编译期,它能够直接将最后生成的全部变化都直接拷到手机端。对于线上方案,这确定是不可行的。因此当前核心问题是找到合适的,使补丁结果更小的差分算法。
微信首先 demo 中采用的是 bsdiff,它无关文件格式,但对于dex效果不是特别好,并且很是不稳定。当前微信对于 so,依然使用 bsdiff 算法。
而后咱们想到 dexmerge 算法,把修改跟新增的类经过 dexmerge 方式与原来的 dex 合并,从而获得最终的完整 Dex。
通过实践,dexmerge 的核心问题有两个:
最后咱们决定基于 dex 的格式,自研出一种 Dexdiff 算法,它须要达到如下目标;
这里面主要的原理是深度利用原来 dex 中的信息,对于 dex 的每个 section 作处理。这块在今天再也不深刻,感兴趣的同窗能够交流。
内存方面 dexdiff 峰值内存是 dex 的两倍左右,达到预期的结果。
对于微信热补丁的更多信息,能够阅读我以前发的一篇文章。
而后咱们来看看 Tinker 的框架设计,它主要包括如下几部分:
在微信中,咱们为 Tinker 框架加入了100多个实时上报,监控着在每一个过程可能出现的问题:
接下来咱们来看看在开发 Tinker 过程当中,遇到的一些问题:
对于 Art 平台,dex2oat 时间较长。特别是厂商 OTA 以后,全部动态加载的代码都须要从新执行 dex2oat。这是由于 boot image 已经改变,可是系统在升级时只会给 ClassN.dex 从新 OTA。
对于补丁 dex 会出现主进程同步执行 dex2oat,这个时间很是久,颇有可能会出现 ANR,对于小米等一些产品的开发板更是如此。这也是咱们如今努力在实现分平台合成的缘由,即在 Art平台,只合成规则下须要的 class。只要不是全量替换,从新 dex2oat 的时间是能够接受的。
这块花了必定的时间从新梳理了 Android N art 的代码,详细的分析能够查看以前我发的一篇文章。
开始的时候,咱们加载补丁 dex 采用的是 makedexElement 的方式。可是发现大约有几十万台机器,补丁加载成功了,可是使用的仍是旧版本的代码。某些机器相似三星 s6 502系统,尽管反射 pathList 成功,查找顺序依然以 base.apk 优先。
这里采起的解决方法是相似 instant run,采用反射 parent classloader 的方式。这里不得不提,instant run 的 increaseClassLoader 实现很是精妙。
市面上有各类各样的微信插件,它们在微信启动前会提早加载微信中的类,这会致使两个问题:
**a. **在 Dalvik 平台,直接出现 Class ref in pre-verified class resolved to unexpected implementation
的 crash;
**b. **在 Art 平台,因为出现部分类使用了旧的代码,这可能致使补丁无效,或者地址错乱的问题。
它们根本的缘由都是Xposed反射调用,提早导入了咱们的某些类。
事实上,因为补丁使用不当或者其余问题,咱们的确须要有一个安全模式。即在应用启动不起来或屡次 crash 时,进入补丁清理或者升级的流程。
也许有人以为 Tinker 过于臃肿,过于复杂。这是由于热补丁并非仅仅加载一个 dex 或 so 文件,事实上它要关心的细节有不少。进程的一致性,控制可修改类的范围,版本的管理,扩展性等等。
Tinker 的将来规划是真正的开源出去,大约下周会提交分享平台合成以及资源相关的全部代码。而后等公司的开源审计结束后将在 github 开源,欢迎你们接入 Tinker 内测,给咱们更多的意见。
因为时间有限,今天的分享就到这里。对于 So,资源的合成方式,dexdiff 的技术细节,若你们感兴趣能够与咱们交流。
**Q1:**请教下 patch 进程和主进程是怎么通讯的?
是经过 intent service 通讯的,主进程一个接受补丁结果的 intent service,patch 进程是一个接受补丁请求的 intent service
Q2:“分平台合成”没听太明白,能再仔细说下么?
分平台合成就是在 Dalvik 平台,咱们合成全量的 dex,这能够避免咱们插桩的要求。
在 Art 平台,咱们只合成上述三个条件下的类:
a. 修改跟新增的 class; b. 若 class 有 field,method 或 interface 数量变化,它们全部的子类; c. 若 class 有 field,method 或 interface 数量变化,它们以及它们全部子类的调用类。若是采用 ClassN 方式,即须要多个 dex 一块儿处理。
这里的难点是同一份 diff 代码,能够作到不一样的合成方式。
**Q3:**对于内部空间不足引发的 patch 失败如今有什么好的解决办法?
对于咱们的方案,空间占用有可能比较大,咱们解决的方法有两个:
- 在 patch 以前提早检查用户的剩余空间,若是用户剩余空间过少,即不尝试。
- 若本次失败,咱们会有回调,而后咱们会按期重试三次。
你也能够在这里采用提示用户清理空间。Tinker 框架是能够高度定制化的。
**Q4:**对于替换 classloader 失败后再用 MultiDex install 这种方案有什么考虑?
有的,对于替换失败的话,的确会回退到相似 Multidex install 方式的
**Q5:**目前微信对热补丁技术的应用场景通常集中在哪些方面呢?除了修复紧急的 BUG,还有哪些真实场景下用过这个技术吗?微信是如何评估是否须要经过打热补丁的方式来处理一些问题的呢?
正如我以前的一篇文章来讲,在 Android 热补丁技术的应用比 iOS 更加容易。咱们能够彻底作到无感知的开发,推给用户等。这里面的应用场景有不少,用户调试,版本升级,发布需求,Abtest 等等。
**Q6:**想问下大神,对于替换 app 中使用的第三方 jar 包,有具体实践吗?
抱歉,这部分尚未实践。原理上是没问题的,若是第三方的 jar 包是集成到源码,那么编译新包的时候已经能够带上改变。若是第三方的 jar 包是动态加载的,也是没有问题的。咱们经过 parent classloader 的方式,查找顺序也会在大家以前。
**Q7:**patchCoreSDK 怎么绕过 换 classloader 后跨 dex 加载类 accesserror 的问题?有对 patchcoreSDK 作强制访问隔离吗?
是的,Tinker 框架分为两部分,核心加载代码,成为 loader 类,这里大概有十几个类,他们是不容许修改的。其余大部分 Tinker 的类也是能够经过补丁修改的,这里 Tinker 框架已经作了处理,即在新合成的 Dex,咱们已经删除了 loader 相关的类,从而完全避免了这个问题。
**Q8:**patch 成功后怎么及时重启其余进程?
为了保证各个进程的惟一性,咱们有一个版本管理文件用于记录当前补丁的版本。它分为 old 与 new 两个字段。同时作了约定,只有 patch 进程能够修改 new 字段,只有主进程能够修改 old 字段,其余全部进程启动时都只会加载 old 字段的补丁版本。而后主要主进程能够发起版本升级,即把 new 字段赋值给 old 字段,这个时候主进程要杀掉其余全部的进程,以保证统一性。 而及时重启其余进程的问题,主要是在我刚才讲的 result service。在结果回调中,咱们若是发现补丁已经成功了,咱们能够设置主进程在后台或者锁屏时自杀,以达到最快的应用。
**Q9:**彻底使用新的资源包是怎么理解?旧的资源包会被替换删除吗?
旧的资源包是安装的 apk,咱们是不会删掉的。咱们只是反射系统的一些接口,把它替换成新的资源包
**Q10:**超级补丁方案,有没有想过不采用插桩的方式,而是去 hook 检验的方法,就能缓解性能的问题?
事实上,有些人实现 hook preverify 标志来避免插桩。可是看过底层代码,就知道是不可行的。咱们要知道系统检查那个标志位的真正缘由,即便 hook 了 preverify 标志,在真正运行过程当中,因为 quck 指令以及 vtable 的优化,依然运行时会出问题。这个问题告诉咱们,作事情须要知其然也要知其因此然。
**Q11:**合成新的资源和 so 是怎么加载的?
so 能够经过反射 classloader 的 lib path,可是咱们并不建议这么多,一来是兼容性问题,二来在某些机器上,多 abi 的判断并不许确。咱们更但愿经过封装代码来支持。对于资源,咱们处理是跟 dex 差很少,启动时即反射调用。
**Q12:**是否有动态下发第三方的 jar 包,如何调用第三方 jar 包的方法。反射?
Tinker 框架只会合成输入 pattern 下的 dex,并且在启动的时候把他们加载。若是调用的问题,使用者本身决定的。
**Q13:**差量下发更新,合成的时候是否会有性能问题?是否支持(图片)资源的差量下发?
合成的话,咱们对于内存、GC 以及耗时都有大量的优化。即便是微信这样体量的 app,从外部监控来看,大部分用户都能在60秒之内完成。
**Q14:**须要在补丁合成加载以后才进入程序(交由用户操做)吗?
合成与加载是分开两个过程,咱们的原则是除非合成已经完全完成,否则其余进程是不会去加载的。即补丁不会去影相其余进程的加载性能
**Q15:**代码彻底开源吗?
对的,全部代码都会开源,从编译到各个模块。
**Q16:**xposed 框架的那些插件,是经过反射调用替换值?那通常有啥方式保证安全性?保证 app 数据的安全性?
它们只要是反射调用微信的某些类,达到某些功能的篡改。事实上,若是在 root 下,单纯的保护是比较难的。
**Q17:**为何要在补丁成功的时候加结果回调是为了启动程序么,可是和您刚才说的为了实时上报?
回调结果是为了给使用者一个回调,在这个回调里面它能够作各类各样的工做。例如我弹出升级完成的 dialog。我设置锁屏或者程序进入后台后自杀,这能够加快补丁的应用
**Q18:**既然能加载 so 和资源,Tinker 能用于插件化吗?
Tinker 当前没有作四大组件的代理,可是 Tinker 将来绝对是具有这个能力的
**Q19:**merge 失败后的补救机制是怎样的?能够回退么?
merge 失败,咱们会收到回调,这个时候咱们不会加载的。在默认实现里面,咱们会删除这些临时文件。
**Q20:**这套框架目前是多少我的在维护呢?
Tinker当前有3我的在开发维护
**Q21:**请问资源是编译到 arsc 中仍是反射加载二进制流?
你的问题我不太明白,资源咱们采用的是全量替换,即彻底使用新的资源包
**Q22:**在加入 Tinker 以后,对各平台的加固适配如何?微信是否有加固?
微信没有使用加固,可是加固应该是不影响的,只须要把接口改一下就能够了。