您的位置:首页 > 移动开发 > 微信开发

源码分析微信热修复框架Tinker的类加载过程

2017-02-10 09:57 716 查看
最近在设计一个安卓热修复的完整方案, 这两天终于有零零散散的时间可以考虑下如何选型了.之前项目中用过阿里的基于"安卓神器"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类是类加载模块的入口,因此从该类开始:

[java] view
plain copy

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做合并,这里截取其中关键的步骤:

[java] view
plain copy

if (isEnabledForDex) {  

            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);  

            if (!loadTinkerJars) {  

                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");  

                return;  

            }  

}  

做了很多安全校验的机制以保证dex可用后,调用TinkerDexLoader.loadTinkerJars()方法.

[java] view
plain copy

 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地址,

[java] view
plain copy

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()方法.

[java] view
plain copy

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.

[java] view
plain copy

/** 

 * 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类,


重点来了~~

[java] view
plain copy

/** 

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

另外完整的热修复是要包括很多辅助模块的,比如安全机制,分发机制,回退机制等,目前还没有类似的开源.或许以后等这部分完成并稳定后安卓团队也可以开源这个大的方案?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: