01
奇异果TV作为在电视设备上用户活跃度最高的应用之一,为广大用户提供了丰富的内容播放服务。随着奇异果TV多年的发展,功能逐步增加,业务更加复杂,每次发版都需要经过功能测试、适配测试、线上灰度测试,但线上问题仍不能完全避免,需要及时对线上问题进行修复。
同时,由于电视端特有的商业模式和合作生态,App更新覆盖速度较慢,且更新操作较为复杂,对于以老人和儿童居多的TV用户来说,需要更快速地使用无感知的方式修复线上问题。
在之前的文章里,我们介绍了奇异果TV特有的插件机制,可通过插件对自身主要业务进行更新,也是奇异果TV最主要的升级途径。同样,当遇到严重线上问题时,也可通过插件更新修复错误,但有一定局限性:
1)奇异果的业务插件中几乎包含了整个应用的功能,包体较大。
2)合作的TV厂商和应用商店对质量要求较高,插件上线需要经过严格测试,且每个厂商的流程不同,导致线上问题修复进展较慢。
3)插件更新的检查时机较少,且下次启动才会生效,面对紧急线上问题无法第一时间修复。
4)插件是一种对系统有侵入性hook的方案,需要大量的适配工作。
02
03
3.1 原理
3.2 改进过程
Robust在为一个xxx类创建补丁时,会生成一个xxxPatch的补丁类并把要修复的方法从xxx类中搬运到xxxPatch中。由于直接把方法搬到xxxPatch肯定不适用,被修复方法的实现中会调用到的原类中的私有变量或方法,导致无法在xxxPatch类中直接使用,所以需通过javassist解析方法的字节码,把对应方法、变量的直接引用修改为反射的方式调用。如下:
生成补丁后,补丁类中反编译代码如下:
然而在调用一些系统方法时,如上面System.loadLibrary("sodemo")正常调用是没有问题的,但使用反射调用就会因为so找不到而报错。
从报错日志来看,是从/vendor/lib和/system/lib这两个文件夹中去查找libsodemo.so找不到的,这是因为我们奇异果App自己的so肯定不在系统文件里面。那么为什么没有从奇异果的安装目录中查找呢?经过分析System.java的源码发现:
Runtime#loadLibrary方法会从传入的classLoader里查找so,那么问题会不会出在传入的ClassLoader身上呢?从日志上分析 loader==null 时才能输出"Library sodemo not found; tried [/vendor/lib/libsodemo.so, /system/lib/libsodemo.so]"的日志。ClassLoader是通过VMStack.getCallingClassLoader()获取的,它是用来获取调用者的ClassLoader。难道反射调用System.loadLibrary会改变调用堆栈,使得VMStack.getCallingClassLoader()获取到的ClassLoader为空吗?经过demo验证,反射调用时发现获取到的classLoader 为null,这时候就会从系统的路径下中查找so而导致加载失败。Robust可通过robust.xml文件配置在补丁中哪些类不设置反射,在<noNeedReflectClass>标签下配置了java.lang.System不反射调用后果然so加载成功了。
那么可以把补丁代码中用到的一些系统类比如Log、File、InputStream等设置为不使用反射吗?不仅减少补丁中的反射代码,同时也增加了易读性,减少性能消耗。尝试在一次构建补丁时,把补丁方法中使用的系统类全部配置为不反射调用,结果不出意外的出问题了。
现象是在kotlin代码的一个读写操作中引用到了FileInputStream和InputStreamReader,同时把这两个类设置了不反射调用,如下:
结果补丁运行后立即报错,错误信息是FileUtilKotlinPatch.readFile方法需要一个java.io.InputStream的参数但是传入了一个com.meituan.sample.FileUtilKotlin的参数。
查看构建补丁时生成的dump文件可以看到补丁中有如下几行代码:
验证字节码时InputeamReader构造函数的参数被认为传入了FileUtilKotlin类导致。所以在kotlin下的代码有些是不能设置不使用反射的,robust的配置文件中标注中说的配置不需要反射处理的类要慎重选择。
自动化构建补丁对于kotlin代码的支持不是很好,除上面提到的配置非反射类之外,在面对when+enum的补丁(混淆情况下)也是遇到了问题。修复的方法中传入了一个枚举类,使用kotlin中的when关键字去匹配各种情况,结果执行到补丁的代码时报错找不到静态变量$EnumSwitchMapping$0。
反编译补丁和apk的代码发现变量$EnumSwitchMapping$0在补丁中被混淆成了a,然而在补丁中为int[] iArr = d.a.$EnumSwitchMapping$0;并没有用混淆后的值,同样的代码在java中生成的补丁为反射调用int[] iArr = (int[]) EnhancedRobustUtils.getStaticFieldValue("a", c.a.class);且运行正常。
那么就有两个问题:
1、为什么这行代码在kotlin中没有使用反射?
从自动化构建脚本上看读取变量值时的处理如下:
从源码可以看出对于变量的读取操作,如果变量是静态且public修饰的则保持不变,否则用反射方式调用。问题应该就出在变量是否有public修饰的问题上。反解apk把kotlin和java代码对比:
kotlin代码编译后有public static修饰:
java代码编译后没有public修饰:
这也就是为什么kotlin代码没有被反射的原因了。
2、对于直接引用的类或变量为什么没有在Smali汇编语言层做替换?
对于直接引用的变量值,robust的自动构建脚本会对smali文件进行逐行遍历并把原值替换成混淆后的值。那为什么没有把$EnumSwitchMapping$0替换成混淆后的值呢?经过日志发现虽然找到了$EnumSwitchMapping$0混淆后的对应关系,但是在执行替换时没有替换成功,替换操作如下:
这样就清楚了,在正则表达式中 $ 是个特殊字符。因此当regex字符串中包含 $ 字符时,须将其转义,以便它被解释为字面字符,也就是说regex应该改为regex = '->\\$EnumSwitchMapping\\$0'才能被正确替换。故这里的替换对于含有特殊字符的是有些隐患的,需要特别注意一下。
Robust本身并不支持对Native代码进行修复,但奇异果App中很多功能使用到了动态链接库,如果不能对Native代码进行热修,那么线上问题需要重新发版的概率依旧很高。
奇异果App使用了一个简单直接的方案来解决该问题。不直接对native代码进行修复,通过so的动态加载替换so库的方式来解决。从源码来看,当调用System.loadLibrary("libName")时,执行流程是这样的:
第三步中从classLoader中查找lib,找到后立即执行doload方法去加载。那么findLibrary最终是从nativeLibraryPathElements的数组中遍历,也就是说只要把修复好的so的路径插入到nativeLibraryPathElements数组的最前面,加载时就一定会优先加载修复后的so。如下:
PatchedClassInfo
这个类主要是混淆后的类名和补丁中转发器的映射关系,
xxPatchControl
类称为转发器,负责把方法转发到对应的补丁方法。
xxPatch
这个是补丁类,包含了修复问题的全部代码。这部分代码较多,主要的代码就是对改动类的一次翻译:把改动方法中调用的方法/字段,全部改为了反射调用,同时解决Proguard造成的混淆、以及内联的问题。
xxInLinePatch
这个类是为了处理内联问题而产生的,把因为内联消失的代码放到了
xxInLinePatch
中。
XXPatchRobustAssist
这个类别是为解决super问题引入的解决办法。
Add
注解加在哪个类上,就会把这个类放入补丁内部。
由于奇异果App对接合作厂商较多,每次升级需打包部署上百个APK升级包,同时会有 APK+插件混合模式。在面对线上问题需要修复时,同时支持几百个补丁包部署,对打补丁包和部署都是个较大的挑战,人工成本巨大且易出错,亟需一套简易快速的自动化部署平台。结合奇异果App业务特性,我们设计了一套完整的自动化部署打包方案如下:
通过该自动化部署,打通升级部署后台和打包平台,实现一键部署/灰度补丁能力,操作人只需关心原升级任务和补丁分支即可创建热修任务,大大降低了理解难度和操作工作量,即使非研发人员也可以操作部署。
04
经过不懈的努力,最终KRobust在奇异果上线了。修复范围、修复效率大幅度提升,同时修复成本大幅度降低。
在修复范围上,我们扩充了对native代码修复的支持,优化对kotlin代码的支持,进一步提升了该方案的能力覆盖度,线上问题可修复达到95%以上。同时借助于原方案的优势,后续适配问题相对较少,较低的android版本也可修复。
在修复效率上,为了进一步提升热修复成功率,奇异果TV新增了push消息和轮询机制保证获取补丁包的实时性,使补丁发布后,App可以第一时间获取补丁并加以修复。线上数据显示,各个步骤补丁下载成功率99.8%、安装成功率99.97、加载的成功率99.97%。在补丁发布后,24小时修复率超过90%,5日修复率超过99%。
在修复成本上,发现线上问题到修复上线,从以前3日左右(包含解决、部署插件+APK和联系合作方审批的时间)到现在24小时内,同时支持300+渠道APK补丁一键部署,大大降低了修复线上问题的成本和时长,避免了大面积客诉和故障。
后续,我们计划进一步提升补丁生成的简易度,如通过自动对比代码差异生成补丁,从而进一步降低补丁生成成本,降低运维人员操作成本。
本文分享自微信公众号 - 爱奇艺技术产品团队(iQIYI-TP)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。