Tinker Android热补丁框架

国际惯例先贴地址 Tinker开源地址:https://github.com/Tencent/tinker

玩过Dota的童鞋都知道 地精修补匠的大招,我们希望发版本可以像它一样做到无限刷新。 Android热补丁技术应该分为以下两个流派:

  • Native,代表有阿里的Dexposed、AndFix与腾讯的内部方案KKFix;
  • Java,代表有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust。 Native流派与Java流派都有着自己的优缺点。事实上从来都没有最好的方案,只有最适合自己的。

Native的代表Dexposed/AndFix;最大挑战在于稳定性与兼容性,而且native异常排查难度更高。另一方面,由于无法增加变量与类等限制,无法做到功能发布级别; java的代表Qzone;最大挑战在于性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题;

微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不 再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex 合并,然后整体替换掉旧的DEX,达到修复的目的。

这里有个问题很关键,Tinker的亮点使用了QQ空间插桩的效果来规避Android的校验机制。NUWA分析里面有具体介绍。简单来说dvm有一条规则: 一个类如果引用了另一个类,一般是要求他们由同一个dex加载.上面的流程显然犯规了,补丁肯定不和原来的类是同一个dex.但为什么MultiDex这 类分包方案不犯规呢?是因为判断犯规有个条件,即如果类没有被打上IS_PREVERIFIED标记则不会触发判定.如果类在静态代码块或构造函数中引用 到了不在同一个dex的文件则不会有IS_PREVERIFIED标记.因此最直接的办法就是手动在所有类的构造函数或static函数中加上一行引用其 他dex的方法,这个dex出于性能考虑只有一个空的类比如class A {}.这个dex叫做hack dex, 给所有类加引用的步骤叫做"插桩".这也是目前nuwa目前所使用的手段,当然了,手动插桩是不现实的,一般会用JavaAssist做字节码层面的修 改,但好像用AspectJ也可以~好处是源码级的改动,不需要做字节码的操作,但目前没人这么搞过 首先看下源码,最新源码是dev分支tags 1.6.2 https://github.com/Tencent/tinker/tree/dev/tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader

2016-10-08 09:51:30屏幕截图.png

从类名可以知道Tinker处理了类的加载,资源的加载以及so库的加载.我们的关注点在类加载上,根据经验判断,TinkerLoader类是类加载模块的入口,因此从该类开始:

@Override

public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {

Intent resultIntent = new Intent();

long begin = SystemClock.elapsedRealtime();

tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);

long cost = SystemClock.elapsedRealtime() - begin;

ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);

return resultIntent;

}

TinkerLoader.tryLoad()很明显就是加载dex的入口函数,这里微信统计了加载时间,并进入tryLoadPatchFilesInternal()方法.这个方法较长,主要是对新旧两个dex做合并,这里截取其中关键的步骤:

if (isEnabledForDex) {

//tinker/patch.info/patch-641e634c/dex

boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);

if (!dexCheck) {

//file not found, do not load patch

Log.w(TAG, "tryLoadPatchFiles:dex check fail");

return;

}

}

做了很多安全校验的机制以保证dex可用后,调用TinkerDexLoader.loadTinkerJars()方法. loadTinkerJars()获取PathClassLoader并读取dex与dvm优化后的odex地址,

具体代码请查看原文(http://www.jianshu.com/p/11acde51ff0b)
或请点击下方查看原文

接着遍历dexList,过滤md5不符校验不通过的,调用SystemClassLoaderAdder的 installDexs()方法.

public static void installDexes(Application application, 
PathClassLoader loader, File dexOptDir, List<File> files)throws Throwable {
if (!files.isEmpty()) {

ClassLoader classLoader = loader;if (Build.VERSION.SDK_INT >= 24) {

classLoader = AndroidNClassLoader.inject(loader, application);

}//because in dalvik, if inner class is not the same classloader with it 
wrapper class.//it won't fail at dex2optif (Build.VERSION.SDK_INT >= 23) {

V23.install(classLoader, files, dexOptDir);

} else if (Build.VERSION.SDK_INT >= 19) {

V19.install(classLoader, files, dexOptDir);

} else if (Build.VERSION.SDK_INT >= 14) {

V14.install(classLoader, files, dexOptDir);

} else {

V4.install(classLoader, files, dexOptDir);

}if (!checkDexInstall()) {throw new TinkerRuntimeException(
ShareConstants.CHECK_DEX_INSTALL_FAIL);

}
}
}

可以看到Tinker对不同系统版本分开做了处理,这里我们就看使用最广泛的Android4.4到Android5.1.

/**

 * Installer for platform versions 19.

 */private static final class V19 {private static void install(
ClassLoader loader, List<File> additionalClassPathEntries,

File optimizedDirectory)

throws IllegalArgumentException, IllegalAccessException,

NoSuchFieldException, InvocationTargetException, 
NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of

 * dalvik.system.BaseDexClassLoader. We modify its

 * dalvik.system.DexPathList pathList field to append additional DEX

 * file entries.

 */Field pathListField = ShareReflectUtil.findField(loader, "pathList");

Object dexPathList = pathListField.get(loader);

ArrayList<IOException> suppressedExceptions = 
new ArrayList<IOException>();

ShareReflectUtil.expandFieldArray(dexPathList,
 "dexElements", makeDexElements(dexPathList,
 new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,

suppressedExceptions));if (suppressedExceptions.size() > 0) {
 for (IOException e : suppressedExceptions) {

Log.w(TAG, "Exception in makeDexElement", e);throw e;

}

}

}

V19.install()中先通过反射获取BaseDexClassLoader中的dexPathList,然后调用了 ShareReflectUtil.expandFieldArray().值得一提的是微信对异常的处理很细致,用List接收dexElements 数组中每一个dex加载抛出的异常而不是笼统的抛出一个大异常.

接着跟到shareutil包下的ShareReflectUtil类,不要被它的注释误导了,这里不是替换普通的Field,调用这个方法的入参fieldName正是上一步中的”dexElements”,在这么不起眼的一个工具类中终于找到了Dex流派的核心方法。

/**
public static void expandFieldArray(Object instance, String fieldName, 
Object[] extraElements)

throws NoSuchFieldException, IllegalArgumentException,
 IllegalAccessException {

Field jlrField = findField(instance, fieldName);
 //这句是关键,这里的jlrField也就是所谓的dexElements

Object[] original = (Object[]) jlrField.get(instance);

Object[] combined = (Object[]) Array.newInstance(
 original.getClass().getComponentType(), 
 original.length + extraElements.length);
 // NOTE: changed to copy extraElements first, for patch load first

System.arraycopy(extraElements, 0, combined, 0, extraElements.length);

System.arraycopy(original, 0, combined, 
 extraElements.length, original.length);

jlrField.set(instance, combined);

}

Tinker本质仍然是用dexElements中位置靠前的Dex优先加载类来实现热修复: )(ps:并没有传说那么先进)

Tinker虽然原理不变,但它也有拿得出手的重大优化:传统的插桩步骤会导致第一次加载类时耗时变长.应用启动时通常会加载大量类,所以对启动时 间的影响很可观.Tinker的亮点是通过全量替换dex的方式避免unexpectedDEX,这样做所有的类自然都在同一个dex中.但这会带来补丁 包dex过大的问题,由此微信自研了DexDiff算法来取代传统的BsDiff,极大降低了补丁包大小,又规避了运行性能问题又减小了补丁包大小,可以 说是Dex流派的一大进步.

简单来说,在编译时通过新旧两个Dex生成差异path.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的 Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利 用Dex的格式来减少差异的大小。它的粒度是Dex格式的每一项,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone 的粒度为class。

关于微信所使用的三种算法,如图所示

BsDiff;它格式无关,但对Dex效果不是特别好,而且非常不稳定。当前微信对于so与部分资源,依然使用bsdiff算法;

DexMerge;它主要问题在于合成时内存占用过大,一个12M的dex,峰值内存可能达到70多M;

DexDiff;通过深入Dex格式,实现一套diff差异小,内存占用少以及支持增删改的算法。

由于微信发布的Android_N混合编译与对热补丁影响解析,所以在tinker中完全使用了新的Dex,那样既不出现Art地址错乱的问题,在Dalvik也无须插桩。当然考虑到补丁包的体积,我们不能直接将新的Dex放在里面。但我们可以将新旧两个Dex的差异放到补丁包中

关于算法这块不再做过多介绍,根据腾讯bugly说后面会出文章详细说明。

整体的流程如下:

从流程图来看,同样可以很明显的找到这种方式的特点:

优势: 合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行 性能提高。兼容性和稳定性比较高。 开发者透明,不需要对包进行额外处理。 不足: 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。 合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。

目前热补丁各式各样,眼花缭乱啊。。。。思密达。。请勿转载使用,~~~~

原文发布于微信公众号 - Android历练记(gh_db8538619cdd)

原文发表时间:2016-10-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏芋道源码1024

Dubbo 源码解析 —— 简单原理、与spring融合

前言 结束了集群容错和服务发布原理这两个小专题之后,有朋友问我服务引用什么时候开始,本篇为服务引用的启蒙篇.之前是一直和大家一起看源码,鉴于Talk is ch...

3644
来自专栏雨过天晴

原 荐 C# FTP 坑了我两天的一个坑

1.3K6
来自专栏Phoenix的Android之旅

Dagger2 Android应用:@Component和@Module

这部分会介绍一下DI的主要概念,包括Component,Module,但不涉及和Android有关的具体代码。

1402
来自专栏zhisheng

Java研发方向如何准备BAT技术面试

背景: 最近BAT等各大互联网巨头们的校招陆陆续续都准备开始了,可能对于在校的大多数学生来说,不知道如何正确衡量自己掌握的技术,更不知道BAT这...

1.5K4
来自专栏BaronTalk

在Android项目中使用Java8

前言 在过去的文章中我介绍过Java8的一些新特性,包括: Java8新特性第1章(Lambda表达式) Java8新特性第2章(接口默认方法) Java8新特...

3166
来自专栏JackieZheng

Java豆瓣电影爬虫——小爬虫成长记(附源码)

  以前也用过爬虫,比如使用nutch爬取指定种子,基于爬到的数据做搜索,还大致看过一些源码。当然,nutch对于爬虫考虑的是十分全面和细致的。每当看到屏幕上唰...

40711
来自专栏章鱼的慢慢技术路

浅谈单片机中C语言与汇编语言的转换

4393
来自专栏吉浦迅科技

DAY35:阅读流程控制语句

1524
来自专栏JackieZheng

Nutch源码阅读进程3---fetch

走了一遍Inject和Generate,基本了解了nutch在执行爬取前的一些前期预热工作,包括url的过滤、规则化、分值计算以及其与mapreduce的联系紧...

2295
来自专栏FreeBuf

kill.exe溢出漏洞分析与EXP讨论

* 本文原创作者:zzz66686,本文属FreeBuf原创奖励计划,未经许可禁止转载 1. 前言 前几日,笔者在exploit-db上发现了一个kill.ex...

2459

扫码关注云+社区

领取腾讯云代金券