微信 Tinker 负责人张绍文关于 Android 热修复直播分享记录

2,694 阅读12分钟
原文链接: www.diycode.cc

hello,大家好。我是张绍文,目前在微信主要负责Android的性能优化以及终端质量平台相关工作。首先非常抱歉由于本周版本上线,可能准备的不是太充分。若有任何疑问,欢迎在分享结束后提问。

下面开始我们今天的分享,热补丁技术是当前非常热门的Android开发技术,其中比较出名的方案有支付宝的AndFix以及QZone的超级热补丁方案。微信大约在2015年6月开始尝试应用,经过研究与尝试现有的各个方案,我们发现它们都有着自身的一些局限性。我们最终采用不同于它们的技术方案,也就是微信热补丁开源框架Tinker。

我们先来讲讲现有框架的一些局限性:

Andfix是阿里推出的开源框架,它在github的地址是:github.com/alibaba/And…

它的技术原理如下图:它采用native hook的方式,这套方案直接使用dalvik_replaceMethod替换class中方法的实现。

它的缺点主要包括以下几个:

  1. 兼容性不佳;由于它采用native替换的方式,在github Issue中也有大量崩溃的反馈;
  2. 成功率不高;不支持修改inline方法,不支持修改参数超过8个或带有long,double或者float的方法。跟一些使用Andfix的产品讨论过,它们的成功率不超过40%;
  3. 开发不透明;由于它还不支持增加filed,我们需要为了补丁而补丁,无法采用这个技术发布需求。 Andfix的好处是可以立刻生效,但它可以支持的补丁场景非常有限,仅仅可以使用它来修复特定问题。所以我们不考虑采用这个方案。

现在我们讲讲Qzone超级补丁方案

这个方案使用classloader的方式,能实现更加友好的类替换。而且这与我们加载Multidex的做法相似,能基本保证稳定性与兼容性。它主要的面临问题有两个:

1、为了解决unexpected DEX problem异常,而采用插桩的方式给所有类插入不会真正运行的代码,防止类打上preverify标志。

采用插桩导致所有类都非preverify,导致上图中的verify与optimize操作会在加载类时触发。这会有一定的性能损耗,微信分别采用插桩与不插桩两种方式做过两种测试,一是连续加载700个50行左右的类,一是统计微信整个启动完成的耗时。

2、在art平台,若补丁中的类出现Field、Method或Interface变化,可能会导致出现内存地址错乱的问题。为了解决这个问题,我们最后补丁中的类要有以下规则:

a. 修改跟新增的class;
b. 若class有field,method或interface数量变化,它们所有的子类;
c. 若class有field,method或interface数量变化,它们以及它们所有子类的调用类。如果采用ClassN方式,即需要多个dex一起处理。

Qzone的方案最为简单,而且开发透明,补丁的成功率也是非常高的。但由于微信对于运行性能以及补丁大小都比较敏感,我们最终也没有采用这套方案。

那么微信希望的是一套怎么样的热补丁框架呢,我们认为主要的目标有以下几个:

  1. 开发透明;开发者无需关心是否在补丁版本,他可以随意修改,不由框架限制;
  2. 性能无影响;补丁框架不能对应用带来性能损耗;
  3. 完整支持;支持代码,So库以及资源的修复,可以做到发布功能;
  4. 补丁大小较小;补丁大小应该尽量的小,提高升级率。
  5. 稳定,兼容性好;保证微信的数亿用户的使用,尽量减少反射;

现在我们来讲讲微信热补丁框架Tinker的实现

它的名字来至Dota中的地精修补匠,我们希望发版本可以像它一样做到无限刷新。

Tinker的方案来源gradle编译的instant run与buck编译的exopackage。它们的思想都是全量替换新的Dex,即我们完全使用了新的Dex或者资源,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。

但是instant run针对的是编译期,它可以直接将最后生成的所有变化都直接拷到手机端。对于线上方案,这肯定是不可行的。所以当前核心问题是找到合适的,使补丁结果更小的差分算法。

微信首先demo中采用的是bsdiff,它无关文件格式,但对于Dex效果不是特别好,而且非常不稳定。当前微信对于so,依然使用bsdiff算法。资源也部分使用了bsdiff算法。

然后我们想到dexmerge算法,把修改跟新增的类通过dexmerge方式与原来的dex合并,从而得到最终的完整Dex。经过实践,dexmerge的核心问题有两个:

  1. 无法删除class;导致在Dalvik平台会出现加载类重复的情况,这要求我们只能采用miniloader加载方案来避免;
  2. 合成时内存占用过大;dexmerge库使用场景在PC,它没有太多的考虑内存问题。它的峰值内存可以达到输入dex的大小的4倍-6倍。一个12M的dex,峰值内存可能达到70多M。

最后我们决定基于dex的格式,自研出一种Dexdiff算法,它需要达到以下目标;

  1. diff结果小;
  2. 合成过程占用内存小;
  3. 支持删除、新增、修改dex中的class。

这里面主要的原理是深度利用原来dex中的信息,对于dex的每一个section做处理。这块在今天不再深入,感兴趣的同学可以交流或者阅读源码。

内存方面dexdiff峰值内存是dex的两倍左右,达到预期的结果。

对于微信热补丁的更多信息,可以阅读我之前发的一篇文章。微信Android热补丁实践演进之路

然后我们来看看Tinker的框架设计,它主要包括以下几部分:

  1. 补丁合成;这些都在单独的patch进程工作,这里包括dex,so还有资源,主要完成补丁包的合成以及升级;
  2. 补丁的加载;如果通过反射系统加载我们合成好的dex,so与资源;
  3. 监控回调;在合成与加载过程中,出现问题及时回调;
  4. 版本管理;Tinker支持补丁升级,甚至是多个补丁不停的切换。这里我们需要保证所有进程版本的一致性;
  5. 安全校验;无论在补丁合成还是加载,我们都需要有必要的安全校验。

在微信中,我们为Tinker框架加入了100多个实时上报,监控着在每个过程可能出现的问题:

接着我们来看看在开发Tinker过程中,遇到的一些问题:

1、厂商OTA;对于Art平台,dex2oat时间较长。特别是厂商OTA之后,所有动态加载的代码都需要重新执行dex2oat。这是因为boot image已经改变,但是系统在升级时只会给ClassN.dex重新oat。

对于补丁dex会出现主进程同步执行dex2oat,这个时间非常久,很有可能会出现ANR,对于小米等一些产品的开发版更是如此。

这也是我们现在努力在实现分平台合成的原因,即在Art平台,只合成规则下需要的class。只要不是全量替换,重新dex2oat的时间是可以接受的。

2、Android N混合编译导致补丁机制失效;这块花了一定的时间重新梳理了Android N art的代码,详细的分析可以查看之前我发的一篇文章。Android N混合编译与对热补丁影响解析

3、Dex反射成功但是不生效;开始的时候,我们加载补丁dex采用的是makedexElement的方式。但是发现大约有几十万台机器,补丁加载成功了,但是使用的还是旧版本的代码。某些机器类似三星s6 502系统,尽管反射pathList成功,查找顺序依然以base.apk优先。

这里采取的解决方法是类似instant run,采用反射parent classloader的方式。这里不得不提,instant run的increaseClassLoader实现非常精妙。

4、Xposed等微信插件; 市面上有各种各样的微信插件,它们在微信启动前会提前加载微信中的类,这会导致两个问题:

a. 在Dalvik平台,直接出现'Class ref in pre-verified class resolved to unexpected implementation'的crash;
b. 在Art平台,由于出现部分类使用了旧的代码,这可能导致补丁无效,或者地址错乱的问题。

它们根本的原因都是Xposed反射调用,提前导入了我们的某些类。

事实上,由于可能存在补丁使用不当或者其他问题,我们的确需要有一个安全模式。即在应用启动不起来或多次crash时,进入补丁清理或者升级的流程。

也许有人觉得Tinker过于臃肿,过于复杂。这是因为热补丁并不是仅仅加载一个dex或者so文件,事实上它要关心的细节有很多。进程的一致性,控制可修改类的范围, 版本的管理,扩展性等等。

Tinker的未来规划是真正的开源出去,大约下周会提交分平台合成以及资源相关的所有代码。然后等公司的开源审计结束后将在github开源
对于So,资源的合成方式,dexdiff的技术细节,若大家感兴趣可以'read the ** source code'或者与我们交流。

由于时间有限,今天的分享就到这里,由于准备仓促,再次给大家致歉。

Q0:大神请教下patch进程和主进程是怎么通信的?
Q0:是通过intent service通信的,主进程一个接受补丁结果的intent service,patch进程是一个接受不请求的intent service
Q1:分平台合成 没听太明白,能再仔细说下么。
Q1:分平台合成就是在Dalvik平台,我们合成全量的dex,这可以避免我们插桩的要求。在Art平台,我们只合成上述三个条件下的类。这里的难点是同一份diff代码,可以做到不同的合成方式。

Q2:对于内部空间不足引起的patch失败现在有什么好的解决办法?
Q2: 对于我们的方案,空间占用的确比较大。我们解决的方法有两个,1. 在patch之前提前检查用户的剩余空间,如果用户剩余空间过少,即不尝试。 2. 若本次失败,我们会有回调,然后我们会定期重试三次。你也可以采用提示用户的方式
B0: 代码完全开源吗?
B0:对的,所有代码都会开源,从编译到各个模块。

B1: 微信安卓版现在有几个activity? 是一个activity 加 多个fragment吗?
B1:非常抱歉,这个问题跟本次分享无关。事实上,我更建议你反编译微信的代码来研究。

b2:xposed框架的那些插件,是通过反射调用替换值?那一般有啥方式保证安全性?保证app数据的安全性
B2: 它们只要是反射调用微信的某些类,达到某些功能的篡改。事实上,如果在root下,单纯的保护是比较难的。

B3:为什么要在补丁成功的时候加结果回调是为了启动程序么,但是和您刚才说的为了实时上报
B3:回调结果是为了给使用者一个回调,在这个回调里面它可以做各种各样的工作。例如我弹出升级完成的dialog。我设置锁屏或者程序进入后台后自杀,这可以加快补丁的应用

B4:既然能加载so和资源,Tinker能用于插件化吗?
B4:Tinker当前没有做四大组件代码,但是Tinker未来绝对是具备这个能力的

B7:这套框架目前是多少个人在维护呢
B7: Tinker当前有3个人在开发维护

b8:请问资源是编译到arsc中还是反射加载二进制流?
B8: 你的问题我不太明白,资源我们采用的是全量替换,即完全使用新的资源包

Q6:patchCoreSDK怎么绕过 换classloader后跨 dex加载类 accesserror的问题?有对patchcoreSDK做强制访问隔离吗?
Q6: 是的,Tinker框架分为两部分,核心加载代码,成为loader类,这里大概有十几个类,他们是不允许修改的。其他大部分Tinker的类也是可以通过补丁修改的,这里Tinker框架已经做了处理,即在新合成的Dex,我们已经删除了loader相关的类,从而彻底避免了这个问题

Q7:patch成功后怎么及时重启其他进程?
Q7: 为了保证各个进程的唯一性,我们有一个版本管理文件用于记录当前补丁的版本。它分为old与new两个字段。同时做了约定,只有patch进程可以修改new字段,只有主进程可以修改old字段,其他所有进程启动时都只会加载old字段的补丁版本。然后主要主进程可以发起版本升级,即把new字段赋值给old字段,这个时候主进程要杀掉其他所有的进程,以保证统一性

以上来内均来自 DEV CLUB 微信直播群,整理发布于diycode分享给大家。大家可以去关注微信技术团队的公众号WeMobileDev