Tinker使用及源码分析

Tinker的补丁加载-dex加载

2020-01-16  本文已影响0人  David_zhou

Tinker和Instant Run的并存
Tinker 非代理模式的启动
Tinker的启动流程

加载补丁入口

前面介绍到了加载补丁的地方主要在TinkerLoader的tryLoad方法中,代码如下:

private void loadTinker() {
        try {
            //reflect tinker loader, because loaderClass may be define by user!
            Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, TinkerApplication.class.getClassLoader());
            Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
            Constructor<?> constructor = tinkerLoadClass.getConstructor();
            tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
        } catch (Throwable e) {
            //has exception, put exception error code
            tinkerResultIntent = new Intent();
            ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
            tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
        }
    }

loaderClassName这个方法是manifest中的真正的application中的创建时传递的类。一般情况是com.tencent.tinker.loader.TinkerLoader这个类,通过反射创建这个类之后,调用TINKER_LOADER_METHOD即tryLoad这个方法。之前我们直接跳过了tryLoad 这个方法分析应用的启动,现在分析下tryLoad这个加载补丁的流程,tryLoad及之后的流程才是tinker的关键所在。

patch文件的检查

TinkerLoader继承自抽象类AbstractTinkerLoader,AbstractTinkerLoader只有一个抽象方法tryLoad。TinkerLoader实现了tryLoad方法,这个方法中主要调用了tryLoadPatchFilesInternal,这个方法执行主要的逻辑。主要代码如下:

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
    final int tinkerFlag = app.getTinkerFlags();// 在RealApplication中传入的参数
     ......省略部分异常检查代码,检查包括是否开启tinker,是否非patch进程,patch文件是否存在等。
    //tinker/patch.info
    //1 获取patch.info.有点疑问,这个patch.info文件是什么时候放到这个目录下的?文件内容是什么?
    File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);

    //old = 641e634c5b8f1649c75caf73794acbdf
    //new = 2c150d8560334966952678930ba67fa8
    File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectoryPath);
    //2 eadAndCheckPropertyWithLock有点玄机,lock 应该是和同步相关,利用文件实现的同步?
    //读取patch.info文件中的内容
    patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
    if (patchInfo == null) {
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
        return;
    }
    ......
        
    //patch-641e634c
    String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
    //tinker/patch.info/patch-641e634c
    String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
    File patchVersionDirectoryFile = new File(patchVersionDirectory);
    //tinker/patch.info/patch-641e634c/patch-641e634c.apk
    final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
    File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);

    ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
    // 3 对patch文件进行检查,包括tinkerId,以及patch中的文件。
    int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
   
    resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());

    final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
    final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
    //check resource
    final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
    
     //only work for art platform oat,because of interpret, refuse 4.4 art oat
        //android o use quicken default, we don't need to use interpret mode
        boolean isSystemOTA = ShareTinkerInternals.isVmArt()
            && ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
            && Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();

        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);
 .......   
}

tryLoadPatchFilesInternal这个方法非常繁琐,主要是能否进行patch的前提进行检查,包括是否开启tinker功能,是否在非patch进程,是否存在patch文件,以及检查patch文件是否符合要求等。另外检查过程中有个疑问,patch.info文件是什么是创建的? 这个答案需要我们来解答。
接下来看tryLoadPatchFilesInternal的剩余一部分,代码如下:

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
        final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
        // 判断逻辑中有“com.huawei.ark.app.ArkApplicationInfo”的类名,貌似是华为的方舟,后续先认为isArkHotRuning结果都是false
        final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();

        if (!isArkHotRuning && isEnabledForDex) {
            //tinker/patch.info/patch-641e634c/dex
            boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
            if (!dexCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:dex check fail");
                return;
            }
        }

        final boolean isEnabledForArkHot = ShareTinkerInternals.isTinkerEnabledForArkHot(tinkerFlag);
        if (isArkHotRuning && isEnabledForArkHot) {
            boolean arkHotCheck = TinkerArkHotLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if (!arkHotCheck) {
                // file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:dex check fail");
                return;
            }
        }

        final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);
        if (isEnabledForNativeLib) {
            //tinker/patch.info/patch-641e634c/lib
            boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if (!libCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
                return;
            }
        }

        //check resource
        final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
        Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
        if (isEnabledForResource) {
            boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
            if (!resourceCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:resource check fail");
                return;
            }
        }
        //only work for art platform oat,because of interpret, refuse 4.4 art oat
        //android o use quicken default, we don't need to use interpret mode
        boolean isSystemOTA = ShareTinkerInternals.isVmArt()
            && ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
            && Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();

        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);

        //we should first try rewrite patch info file, if there is a error, we can't load jar
        if (mainProcess) {
            if (versionChanged) {
                patchInfo.oldVersion = version;
            }
            if (oatModeChanged) {
                patchInfo.oatDir = oatDex;
                // delete interpret odex
                // for android o, directory change. Fortunately, we don't need to support android o interpret mode any more
                Log.i(TAG, "tryLoadPatchFiles:oatModeChanged, try to delete interpret optimize files");
                SharePatchFileUtil.deleteDir(patchVersionDirectory + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
            }
        }

        if (!checkSafeModeCount(app)) {
            resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
            Log.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail");
            return;
        }

        //now we can load patch jar
        if (!isArkHotRuning && isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);

            if (isSystemOTA) {
                // update fingerprint after load success
                patchInfo.fingerPrint = Build.FINGERPRINT;
                patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
                // reset to false
                oatModeChanged = false;

                if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                    ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                    Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                    return;
                }
                // update oat dir
                resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
            }
            if (!loadTinkerJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
        }

        if (isArkHotRuning && isEnabledForArkHot) {
            boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
            if (!loadArkHotFixJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
                return;
            }
        }
     ......
    }

在tryLoadPatchFilesInternal这个方法的第二部分分别对各项标志位进行赋值,包括是否支持dex进行修复,是否支持对资源文件进行修复,是否支持对native文件进行修复等。其中还有sSystemOTA判断,只要用户是ART环境并且做了OTA升级,则在加载dex补丁的时候,就会先把最近一次的补丁全部DexFile.loadDex一遍。这么做的原因是有些场景做了OTA后,oat的规则可能发生变化,在这种情况下去加载上个系统版本oat过的dex就会出现问题。由此可见,将东西做好的代价有点大。可惟其如此,才有真正的价值。这里先假设所有的检查都是通过,先看主流程。检查之后就是开始加载的过程。

开始加载

private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
    ......
     //now we can load patch jar
        if (!isArkHotRuning && isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
            if (isSystemOTA) {
                // update fingerprint after load success
                patchInfo.fingerPrint = Build.FINGERPRINT;
                patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
                // reset to false
                oatModeChanged = false;

                if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                    ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                    Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                    return;
                }
                // update oat dir
                resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
            }
            if (!loadTinkerJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
        }

        if (isArkHotRuning && isEnabledForArkHot) {
            boolean loadArkHotFixJars = TinkerArkHotLoader.loadTinkerArkHot(app, patchVersionDirectory, resultIntent);
            if (!loadArkHotFixJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadArkApkFail");
                return;
            }
        }

        //now we can load patch resource
        if (isEnabledForResource) {
            boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
            if (!loadTinkerResources) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
                return;
            }
        }

        // Init component hotplug support.
        if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
            ComponentHotplug.install(app, securityCheck);
        }

        // Before successfully exit, we should update stored version info and kill other process
        // to make them load latest patch when we first applied newer one.
        if (mainProcess && versionChanged) {
            //update old version to new
            if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                return;
            }

            ShareTinkerInternals.killProcessExceptMain(app);
        }

        //all is ok!
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
        Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
        return;
}

加载补丁的方法因为多个参数而有所区别,这里我们重点看下loadTinkerJars()和loadTinkerResources这两个方法。最后加载成功后将patch的版本更新到文件,然后将其他进程都杀掉,最后将patch成功的信息返回。如果tinker更新成功之后,下次启动是否还需要进行patch的过程呢?按道理是不需要的,此处更新了版本信息应该是出于这个目的。热更的两个点一个是代码更新,一个是资源更新,分别对应于loadTinkerJars和loadTinkerResources这两个方法,接下来我们分析这个两个过程。

代码加载

在loadTinkerJars方法里面调用了一些关于patch文件的信息,这些信息是在checkComplete中准备好的,所以先看下checkComplete这个方法,代码如下:

public static boolean checkComplete(String directory, ShareSecurityCheck securityCheck, String oatDir, Intent intentResult) {
        String meta = securityCheck.getMetaContentMap().get(DEX_MEAT_FILE);
        //not found dex
        if (meta == null) {
            return true;
        }
        LOAD_DEX_LIST.clear();
        classNDexInfo.clear();

        ArrayList<ShareDexDiffPatchInfo> allDexInfo = new ArrayList<>();
        // 解析patch文件中assets/dex_meta.txt的内容
        ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, allDexInfo);

        if (allDexInfo.isEmpty()) {
            return true;
        }

        HashMap<String, String> dexes = new HashMap<>();

        ShareDexDiffPatchInfo testInfo = null;

        for (ShareDexDiffPatchInfo info : allDexInfo) {
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }
            if (!ShareDexDiffPatchInfo.checkDexDiffPatchInfo(info)) {
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
                return false;
            }
            if (isVmArt && info.rawName.startsWith(ShareConstants.TEST_DEX_NAME)) {
                testInfo = info;
            } else if (isVmArt && ShareConstants.CLASS_N_PATTERN.matcher(info.realName).matches()) {
                classNDexInfo.add(info);
            } else {
                dexes.put(info.realName, getInfoMd5(info));
                LOAD_DEX_LIST.add(info);
            }
        }

        if (isVmArt
            && (testInfo != null || !classNDexInfo.isEmpty())) {
            if (testInfo != null) {
                classNDexInfo.add(ShareTinkerInternals.changeTestDexToClassN(testInfo, classNDexInfo.size() + 1));
            }
            dexes.put(ShareConstants.CLASS_N_APK_NAME, "");
        }
        ...... 省略部分代码
        //if is ok, add to result intent
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_DEXES_PATH, dexes);
        return true;
    }

checkComplete中主要检查patch中assets/dex_meta.txt文件的内容,这个文件包含了patch中dex的名字,md5,crc校验等信息。这个文件是在patch打包时生成。生成时机是在配置完成后,大概流程如下:

TinkerPatchPlugin.apply->Runner.inkerPatch->ApkDecoder.patch()->ApkFilesVisitor.visitFile()->UniqueDexDiffDecoder.patch()->DexDiffDecoder.onAllPatchesEnd()

patch文件如何生成,此处先按照上面这样理解,后续再详细分析。这里我们先回过来头接着分析TinkerDexLoader的loadTinkerJars方法。

// private static File testOptDexFile;
private static HashSet<ShareDexDiffPatchInfo> classNDexInfo = new HashSet<>();
// classNDexInfo这个容器中的内容是上面checkComplete中添加进入的。

public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
        if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
            Log.w(TAG, "there is no dex to load");
            return true;
        }

        BaseDexClassLoader classLoader = (BaseDexClassLoader) 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 + "/";

        ArrayList<File> legalFiles = new ArrayList<>();

        for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }

            String path = dexPath + info.realName;
            File file = new File(path);
            // isTinkerLoadVerifyFlag这个标志在项目的applictaion中配置,跳过可以加快速度
            if (application.isTinkerLoadVerifyFlag()) {
                long start = System.currentTimeMillis();
                String checkMd5 = getInfoMd5(info);
                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));
            }
            // 将patch文件中的dex信息加入到legalFiles中
            legalFiles.add(file);
        }
        // verify merge classN.apk
        if (isVmArt && !classNDexInfo.isEmpty()) {
            File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
            long start = System.currentTimeMillis();
            // 跳过对dex进行md5校验的步骤,一般设置为跳过,可以加快速度
            legalFiles.add(classNFile);
        }
        File optimizeDir = new File(directory + "/" + oatDir);
        // 省略isSystemOTA为真的情况
        try {
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp);
        } catch (Throwable e) {
            Log.e(TAG, "install dexes failed");
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
            return false;
        }
        return true;
    }

loadTinkerJars这个方法也是将patch中的信息处理加工,最后调用SystemClassLoaderAdder.installDexes方法开始加载patch中的dex文件。代码如下:

public static void installDexes(Application application, BaseDexClassLoader loader, File dexOptDir, List<File> files, boolean isProtectedApp)
       throws Throwable {
       Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

       if (!files.isEmpty()) {
           //将文件排序
           files = createSortedAdditionalPathEntries(files);
           ClassLoader classLoader = loader;
           if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
               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);
           }
           //install done
           sPatchDexCount = files.size();
           Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

           if (!checkDexInstall(classLoader)) {
               //reset patch dex
               SystemClassLoaderAdder.uninstallPatchDex(classLoader);
               throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
           }
       }
   }

经过一系列的校验,终于到了加载dex文件的地方了。如果是在24以上的机器,就调用AndroidNClassLoader的inject对classloader进行处理,为什么处理的原因在这里Android N混合编译与对热补丁影响解析。inject的过程有点复杂,我们先忽略。接下来按照系统版本分为了四种方式加载补丁,这么做的原因的加载的过程有变化。各个历史版本的代码可以在这里来看AOSPXRef,非常方便。我们先分析下V23中的install过程。代码如下:

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", makePathElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

通过反射拿到BaseDexClassLoader中的pathList,然后调用expandFieldArray更改这个成员变量中dexElements,用makePathElements组装好的Elements数组替换原来的dexElements的值。类的加载都是由BaseDexClassLoader中的findclass来查找后加载,而findclass就是从pathList这个数组中查找的。如果扩展了原来BaseDexClassLoader的pathList变量,从其中加入patch中的class,那就能替换原来出问题的class。所以我们想怎样让pathList这个变量中加入我们前面patch文件中dex的class。DexPathList这个类中有个 Element数组dexElements,我们可以通过反射的方式将Element数组更改,按照类加载器的原理,加载到想要的类后就会直接返回,因此如果我们只要将patch中dex插入到Element数组前面。之后加载特定类就会从patch中的dex加载修复后的类,而不会从原有Element数组加载原来的类。插队的操作是在expandFieldArray这个方法中完成,代码如下:

/**
     * 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);
    }

通过数组拷贝的方式将patch中dex生成的Element插入到数组的前面,而patch中的dex如何转为成Element呢?答案在makePathElements方法中,代码如下:

 /**
 * A wrapper around
* {@code private static final dalvik.system.DexPathList#makePathElements}.
 */
private static Object[] makePathElements(
            Object dexPathList, ArrayList<File> files, File optimizedDirectory,
            ArrayList<IOException> suppressedExceptions)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            Method makePathElements;
            try {
                makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
                    List.class);
            } catch (NoSuchMethodException e) {
                Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
                try {
                    makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
                } catch (NoSuchMethodException e1) {
                    Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
                    try {
                        Log.e(TAG, "NoSuchMethodException: try use v19 instead");
                        return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
                    } catch (NoSuchMethodException e2) {
                        Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
                        throw e2;
                    }
                }
            }

            return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
        }

这个方法也是通过反射调用DexPathList中的makePathElements方法,最终将dex转变为Elelments。下面看下makePathElements方法,代码如下:

private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
           List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
     Element[] elements = new Element[files.size()];
     int elementsPos = 0;
     /*
      * Open all files and load the (direct or contained) dex files up front.
      */
     for (File file : files) {
         if (file.isDirectory()) {
             // We support directories for looking up resources. Looking up resources in
             // directories is useful for running libcore tests.
             elements[elementsPos++] = new Element(file);
         } else if (file.isFile()) {
             String name = file.getName();

             DexFile dex = null;
             if (name.endsWith(DEX_SUFFIX)) {
                 // Raw dex file (not inside a zip/jar).
                 try {
                     dex = loadDexFile(file, optimizedDirectory, loader, elements);
                     if (dex != null) {
                         elements[elementsPos++] = new Element(dex, null);
                     }
                 } catch (IOException suppressed) {
                     System.logE("Unable to load dex file: " + file, suppressed);
                     suppressedExceptions.add(suppressed);
                 }
             } else {
                 try {
                     dex = loadDexFile(file, optimizedDirectory, loader, elements);
                 } catch (IOException suppressed) {
                     /*
                      * IOException might get thrown "legitimately" by the DexFile constructor if
                      * the zip file turns out to be resource-only (that is, no classes.dex file
                      * in it).
                      * Let dex == null and hang on to the exception to add to the tea-leaves for
                      * when findClass returns null.
                      */
                     suppressedExceptions.add(suppressed);
                 }

                 if (dex == null) {
                     elements[elementsPos++] = new Element(file);
                 } else {
                     elements[elementsPos++] = new Element(dex, file);
                 }
             }
             if (dex != null && isTrusted) {
               dex.setTrusted();
             }
         } else {
             System.logW("ClassLoader referenced unknown path: " + file);
         }
     }
     if (elementsPos != elements.length) {
         elements = Arrays.copyOf(elements, elementsPos);
     }
     return elements;
 }

在这个方法中通过loadDexFile 将dex转化为Elelments。然后将Element返回。至此我们分析了dex的校验,加载,以及最后的插入到dexPathList中。其他版本最后的install方法也比较类似,其他流程的讲解参考Android 热修复方案Tinker(三) Dex补丁加载.这篇文章讲的比较详细,可以结合不同版本的代码学习下。

参考链接

感谢微信的开源,让tinker成为了小厂的基础设置,让很多应用开发者摆脱了热更的诸多麻烦。

感谢先行者的详细分析和无私分享。功力尚浅,请不吝指教。

Android 热修复方案Tinker(三) Dex补丁加载

上一篇下一篇

猜你喜欢

热点阅读