美文网首页Tinker使用及源码分析
Tinker的补丁加载-dex加载

Tinker的补丁加载-dex加载

作者: David_zhou | 来源:发表于2020-01-16 20:14 被阅读0次

    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补丁加载

    相关文章

      网友评论

        本文标题:Tinker的补丁加载-dex加载

        本文链接:https://www.haomeiwen.com/subject/ielqzctx.html