国际惯例先贴地址 Tinker开源地址:https://github.com/Tencent/tinkerjava
玩过Dota的童鞋都知道 地精修补匠的大招,咱们但愿发版本能够像它同样作到无限刷新。
Android热补丁技术应该分为如下两个流派:android
Native,表明有阿里的Dexposed、AndFix与腾讯的内部方案KKFix;git
Java,表明有Qzone的超级补丁、大众点评的nuwa、百度金融的rocooFix, 饿了么的amigo以及美团的robust。
Native流派与Java流派都有着本身的优缺点。事实上历来都没有最好的方案,只有最适合本身的。github
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类是类加载模块的入口,所以从该类开始:app
@Overrideide
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进行合并操做时这种状况会更严重,所以合并过程的失败率也会更高。
目前热补丁各式各样,眼花缭乱啊。。。。思密达。。请勿转载使用,~~~~
本文分享自微信公众号 - 喘口仙氣(gh_db8538619cdd)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。