源码分析微信热修复框架Tinker的类加载过程
Posted 开源中国联盟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码分析微信热修复框架Tinker的类加载过程相关的知识,希望对你有一定的参考价值。
最近在设计一个安卓热修复的完整方案, 这两天终于有零零散散的时间可以考虑下如何选型了.之前项目中用过阿里的基于"安卓神器"Xposed框架的Dexposed,非常惊艳.但毕竟也有一年没更新了,很多东西都被后起之秀比如AndFix超越了~ 而且由于之前项目的特殊性,应用只安装在4.1和4.4系统上,这一点神奇的避开了了Dexposed的硬伤不支持5.0+.但做普通APP就绕不开这个硬伤了.
正好微信开源了Tinker, 赶着在休假前一天的半夜翻一翻源码, 做点分析,应该算是"全网首发"Tinker源码解析了.
先简单介绍下目前的两种实现热修复的流派, 以Dexposed和AndFix为首的Native流, 以Nuwa, ClassLoader(QZONE)为首的Dex(也叫Java)流.
Native流核心是替换函数,将Java方法的属性设为native转到JNI层处理,在JNI中又把方法指针指向了Java Hook,在hook中回调其他Java方法,Java->Native Hook->Java Fix,最终回调到任意的目标方法.
Dex流核心是替换dex,有点像插件动态加载,原理是虚拟机在加载类--即从类名映射到class文件的过程--时顺序遍历系统中dexElements(记住这个成员名)数组,dexElements持有应用所有dex,一旦其中element能够成功加载立即返回目标类对应的class.这就给了聪明的人们启发: 如果能将自己的"私货"dex插入dexElements数组并保证它的顺序在最前,岂不是可以完美实现将class替换成"私货"? 接下来就顺理成章了,java中夹带私货的标准流程都是利用反射机制,这次也不例外.通过反射层层获取各种成员各种变量,最后获取到dexElements这个成员,将这个数组arrayCopy一份,顺便在复制出来的数组第0个位置放上自己的dex,最后将复制体set回dexElements,走私完成.
顺便说一下走私的时机问题,和虚拟机有关的、和context有关的,一般都在Application的attachBaseContext()函数中做入口,onCreate()也没问题.
这样一来系统加载任何类时都会先去私货dex中找有没有相关的,就达到了替换类的目标.
这里有个问题很关键,也是Tinker的最大亮点,dvm有一条规则,一个类如果引用了另一个类,一般是要求他们由同一个dex加载.刚才的流程显然犯规了,私货肯定不和原来的类是同一个dex.但为什么MultiDex这类分包方案不犯规呢?是因为判断犯规有个条件,即如果类没有被打上IS_PREVERIFIED标记则不会触发判定.如果类在静态代码块或构造函数中引用到了不在同一个dex的文件则不会有IS_PREVERIFIED标记.因此最直接的办法就是手动在所有类的构造函数或static函数中加上一行引用其他dex的方法,这个dex出于性能考虑只有一个空的类比如class A {}.这个dex叫做hack dex, 给所有类加引用的步骤叫做"插桩".当然了,手动插桩是不现实的,一般会用JavaAssist做字节码层面的修改,其实我觉得用AspectJ也可以~好处是源码级的改动,不需要做字节码的操作,但不知道为什么目前为止没见人这么用过.
---------------------我是分割线--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
铺垫完毕,接下来开始剖析Tinker源码.
dev分支上是最新的Tinker1.6.1版本,从类名可以知道Tinker处理了类的加载,资源的加载以及so库的加载.我们的关注点在类加载上,根据经验判断,TinkerLoader类是类加载模块的入口,因此从该类开始:
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) { boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent); if (!loadTinkerJars) { Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail"); return; } }
做了很多安全校验的机制以保证dex可用后,调用TinkerDexLoader.loadTinkerJars()方法.
public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult) { if (dexList.isEmpty()) { Log.w(TAG, "there is no dex to load"); return true; } PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader(); if (classLoader != null) { Log.i(TAG, "classloader: " + classLoader.toString()); } else { Log.e(TAG, "classloader is null"); ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL); return false; } String dexPath = directory + "/" + DEX_PATH + "/"; File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH); .... }
ArrayList<File> legalFiles = new ArrayList<>(); final boolean isArtPlatForm = ShareTinkerInternals.isVmArt(); for (ShareDexDiffPatchInfo info : dexList) { //for dalvik, ignore art support dex if (isJustArtSupportDex(info)) { continue; } String path = dexPath + info.realName; File file = new File(path); if (tinkerLoadVerifyFlag) { long start = System.currentTimeMillis(); String checkMd5 = isArtPlatForm ? info.destMd5InArt : info.destMd5InDvm; if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) { //it is good to delete the mismatch file ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH); intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH, file.getAbsolutePath()); return false; } Log.i(TAG, "verify dex file:" + file.getPath() + ", md5 use time: " + (System.currentTimeMillis() - start)); } legalFiles.add(file); } try { SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles); } catch (Throwable e) { Log.e(TAG, "install dexes failed"); // e.printStackTrace(); intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e); ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION); return false; }
接着遍历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 dex2opt if (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; } } } <span style="white-space:pre"> </span>... }
V19.install()中先通过反射获取BaseDexClassLoader中的dexPathList,然后调用了ShareReflectUtil.expandFieldArray().值得一提的是微信对异常的处理很细致,用List<IOException>接收dexElements数组中每一个dex加载抛出的异常而不是笼统的抛出一个大异常.
接着跟到shareutil包下的ShareReflectUtil类,
重点来了~~
/** * Replace the value of a field containing a non null array, by a new array containing the * elements of the original array plus the elements of extraElements. * * @param instance the instance whose field is to be modified. * @param fieldName the field to modify. * @param extraElements elements to append at the end of the array. */ public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field jlrField = findField(instance, fieldName); 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); }
不要被它的注释误导了,这里不是替换普通的Field,调用这个方法的入参fieldName正是上一步中的”dexElements”,在这么不起眼的一个工具类中终于找到了Dex流派的核心方法:
和开头说的Dex流的实现几乎一模一样,我们可以看到Tinker本质仍然是用dexElements中位置靠前的Dex优先加载类来实现热修复: )
Tinker虽然原理不变,但它也有拿得出手的重大优化:传统的插桩步骤会导致第一次加载类时耗时变长.应用启动时通常会加载大量类,所以对启动时间的影响很可观.Tinker的亮点是通过全量替换dex的方式避免unexpectedDEX,这样做所有的类自然都在同一个dex中.但这会带来补丁包dex过大的问题,由此微信自研了DexDiff算法来取代传统的BsDiff,极大降低了补丁包大小,又规避了运行性能问题又减小了补丁包大小,可以说是Dex流派的一大进步.
Tinker源码的解析到此结束了,以后有机会再研究下resource,so库等是如何热修复的~
另外完整的热修复是要包括很多辅助模块的,比如安全机制,分发机制,回退机制等,目前还没有类似的开源.或许以后等这部分完成并稳定后安卓团队也可以开源这个大的方案?
以上是关于源码分析微信热修复框架Tinker的类加载过程的主要内容,如果未能解决你的问题,请参考以下文章