源码分析微信热修复框架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);

        ....

}

loadTinkerJars()获取PathClassLoader并读取dex与dvm优化后的odex地址,

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库等是如何热修复的~

另外完整的热修复是要包括很多辅助模块的,比如安全机制,分发机制,回退机制等,目前还没有类似的开源.或许以后等这部分完成并稳定后安卓团队也可以开源这个大的方案?

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值