美文网首页Android插件化和热修复
热修复框架 - TinkerApplication启动(二) -

热修复框架 - TinkerApplication启动(二) -

作者: Stan_Z | 来源:发表于2020-08-13 08:20 被阅读0次

    代码:tinker 1.9.14.7

    一、加载dex补丁

    TinkerLoader.tryLoadPatchFilesInternal 会执行TinkerDexLoader.loadTinkerJars,此处开始加载dex补丁。

    /**
     * Load tinker JARs and add them to
     * the Application ClassLoader.
     *
     * @param application The application.
     */
    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;
        }
        //dalvik.system.PathClassLoader
        ClassLoader classLoader = 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;
        }
        // /data/user/0/com.stan.tinkersdkdemo/tinker/patch-f9dfd7d4/dex/tinker_classN.apk
        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);
            //对每个dex进行md5校验
            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));
            }
            legalFiles.add(file);
        }
        //对apk进行md5校验
        // verify merge classN.apk
        if (isVmArt && !classNDexInfo.isEmpty()) {
            File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
            long start = System.currentTimeMillis();
    
            if (application.isTinkerLoadVerifyFlag()) {
                for (ShareDexDiffPatchInfo info : classNDexInfo) {
                    if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
                        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
                        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
                            classNFile.getAbsolutePath());
                        return false;
                    }
                }
            }
            //verify dex file:/data/user/0/com.stan.tinkersdkdemo/tinker/patch-f9dfd7d4/dex/tinker_classN.apk md5, use time: 0
            Log.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
            legalFiles.add(classNFile);
        }
        File optimizeDir = new File(directory + "/" + oatDir);
        //如果系统ota升级了,删除oat文件,重新编译
        if (isSystemOTA) {
            final boolean[] parallelOTAResult = {true};
            final Throwable[] parallelOTAThrowable = new Throwable[1];
            String targetISA;
            try {
                targetISA = ShareTinkerInternals.getCurrentInstructionSet();
            } catch (Throwable throwable) {
                Log.i(TAG, "getCurrentInstructionSet fail:" + throwable);
                // try {
                //     targetISA = ShareOatUtil.getOatFileInstructionSet(testOptDexFile);
                // } catch (Throwable throwable) {
                // don't ota on the front
                deleteOutOfDateOATFile(directory);
    
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
                return false;
                // }
            }
            //删除原来的oat文件夹
            deleteOutOfDateOATFile(directory);
    
            Log.w(TAG, "systemOTA, try parallel oat dexes, targetISA:" + targetISA);
            // change dir
            optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
            //触发dex2oat编译
            TinkerDexOptimizer.optimizeAll(
                application, legalFiles, optimizeDir, true, targetISA,
                new TinkerDexOptimizer.ResultCallback() {
                    long start;
    
                    @Override
                    public void onStart(File dexFile, File optimizedDir) {
                        start = System.currentTimeMillis();
                        Log.i(TAG, "start to optimize dex:" + dexFile.getPath());
                    }
    
                    @Override
                    public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
                        // Do nothing.
                        Log.i(TAG, "success to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
                    }
    
                    @Override
                    public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                        parallelOTAResult[0] = false;
                        parallelOTAThrowable[0] = thr;
                        Log.i(TAG, "fail to optimize dex " + dexFile.getPath() + ", use time " + (System.currentTimeMillis() - start));
                    }
                }
            );
    
            if (!parallelOTAResult[0]) {
                Log.e(TAG, "parallel oat dexes failed");
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, parallelOTAThrowable[0]);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_OTA_INTERPRET_ONLY_EXCEPTION);
                return false;
            }
        }
        try {
            //加载dex
            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;
    }
    

    流程总结:

    • 对/data/user/0/com.stan.tinkersdkdemo/tinker/patch-f9dfd7d4/dex/ 路径下的dex和tinker_classN.apk 进行md5验证
    • 如果系统ota升级了,删除oat文件,重新编译TinkerDexOptimizer.optimizeAll
    • 加载dex SystemClassLoaderAdder.installDexes

    二、OTA升级之后重新编译

    TinkerDexOptimizer.java

    public static boolean optimizeAll(Context context, Collection<File> dexFiles, File optimizedDir,
                                      boolean useInterpretMode, String targetISA, ResultCallback cb) {
        ArrayList<File> sortList = new ArrayList<>(dexFiles);
        // sort input dexFiles with its file length in reverse order.
       //按文件大小进行排序
        Collections.sort(sortList, new Comparator<File>() {
            @Override
            public int compare(File lhs, File rhs) {
                final long lhsSize = lhs.length();
                final long rhsSize = rhs.length();
                if (lhsSize < rhsSize) {
                    return 1;
                } else if (lhsSize == rhsSize) {
                    return 0;
                } else {
                    return -1;
                }
            }
        });
       //起线程OptimizeWorker对每个dexFile进行dex2oat编译
        for (File dexFile : sortList) {
            OptimizeWorker worker = new OptimizeWorker(context, dexFile, optimizedDir, useInterpretMode, targetISA, cb);
            if (!worker.run()) {
                return false;
            }
        }
        return true;
    }
    

    按文件大小进行排序,起线程OptimizeWorker对每个dexFile进行dex2oat编译。

    TinkerDexOptimizer.java
    boolean run() {
        try {
            if (!SharePatchFileUtil.isLegalFile(dexFile)) {
                if (callback != null) {
                    callback.onFailed(dexFile, optimizedDir,
                            new IOException("dex file " + dexFile.getAbsolutePath() + " is not exist!"));
                    return false;
                }
            }
            if (callback != null) {
                callback.onStart(dexFile, optimizedDir);
            }
            String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
            if (!ShareTinkerInternals.isArkHotRuning()) {
               //useInterpretMode初始化时是false
                if (useInterpretMode) {
                    interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
                } else if (Build.VERSION.SDK_INT >= 26
                        || (Build.VERSION.SDK_INT >= 25 && Build.VERSION.PREVIEW_SDK_INT != 0)) {
                    NewClassLoaderInjector.triggerDex2Oat(context, optimizedDir, dexFile.getAbsolutePath());
                } else {
                    DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);
                }
            }
            if (callback != null) {
                callback.onSuccess(dexFile, optimizedDir, new File(optimizedPath));
            }
        } catch (final Throwable e) {
            Log.e(TAG, "Failed to optimize dex: " + dexFile.getAbsolutePath(), e);
            if (callback != null) {
                callback.onFailed(dexFile, optimizedDir, e);
                return false;
            }
        }
        return true;
    }
    

    android 7.1.1及之后的版本走NewClassLoaderInjector.triggerDex2Oat,低版本的直接走 DexFile.loadDex,DexFile.loadDex这个很明显,我前面文章讲过就是加载dex,后续再补充,先看高版本的triggerDex2Oat。

    NewClassLoaderInjector.java
    public static void triggerDex2Oat(Context context, File dexOptDir, String... dexPaths) throws Throwable {
        // Suggestion from Huawei: Only PathClassLoader (Perhaps other ClassLoaders known by system
        // like DexClassLoader also works ?) can be used here to trigger dex2oat so that JIT
        // mechanism can participate in runtime Dex optimization.
        final StringBuilder sb = new StringBuilder();
        boolean isFirst = true;
        for (String dexPath : dexPaths) {
            if (isFirst) {
                isFirst = false;
            } else {
                sb.append(File.pathSeparator);
            }
            sb.append(dexPath);
        }
        final ClassLoader triggerClassLoader = new PathClassLoader(sb.toString(), ClassLoader.getSystemClassLoader());
    }
    

    这里常规来说,DexClassLoader和PatchClassLoader应该都可以加载外部dex,而且DexClassLoader 还可以指定 optimizedDirectory,这里使用PatchClassLoader的唯一原因是华为做了限制。反正小米我之前做的时候没这讲究。

    那么还有一个疑问?为什么用高版本用PathClassLoader加载 低版本用 DexFile.loadDex ?PathClassLoader最终也会调用DexFile.loadDex,想来想去,唯一解释是上报:
    Android 8.0

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
    
        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }
    

    Android 5.0

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    

    7.1.1及其以上版本,PatchClassLoader初始化时会上报tinker apk插件到DexManager,这样才会在idle&charge时由系统统一做一次speed-profile编译。但是Q以下的版本DexFile.loadDex对于过期的odex都会重新编译,所以唯一解释是,这里高版本使用PatchClassLoader只是兼容Q的DexFile.loadDex只走解释模式不做编译。

    1.9.6版本就没有做区分,统一由DexFile.loadDex编译

    public boolean run() {
        try {
            if (!SharePatchFileUtil.isLegalFile(this.dexFile) && this.callback != null) {
                this.callback.onFailed(this.dexFile, this.optimizedDir, new IOException("dex file " + this.dexFile.getAbsolutePath() + " is not exist!"));
                return false;
            }
    
            if (this.callback != null) {
                this.callback.onStart(this.dexFile, this.optimizedDir);
            }
    
            String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
            if (this.useInterpretMode) {
                this.interpretDex2Oat(this.dexFile.getAbsolutePath(), optimizedPath);
            } else {
                DexFile.loadDex(this.dexFile.getAbsolutePath(), optimizedPath, 0);
            }
    
            if (this.callback != null) {
                this.callback.onSuccess(this.dexFile, this.optimizedDir, new File(optimizedPath));
            }
        } catch (Throwable var2) {
            Log.e("Tinker.ParallelDex", "Failed to optimize dex: " + this.dexFile.getAbsolutePath(), var2);
            if (this.callback != null) {
                this.callback.onFailed(this.dexFile, this.optimizedDir, var2);
                return false;
            }
        }
        return true;
    }
    

    三、加载dex

    SystemClassLoaderAdder.java
    public static void installDexes(Application application, ClassLoader 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 = NewClassLoaderInjector.inject(application, loader, dexOptDir, files);
            } else {
                //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);
            }
        }
    }
    

    这里兼容了V4、V14、 V19、V23这么多个版本。看个V23

    V23:
    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;
            }
        }
    }
    

    首先反射拿到反射得到 PathClassLoader 中的 pathList 对象,再将补丁文件通过反射调用makeDexElements 得到补丁文件的 Element[] ,再将补丁包的 Element[] 数组插入到 dexElements 中且在最前面,这样修复dex会在原来dex之前被加载,修复成功,原来dex失效。

    最后,因为Android 7.0 及以上搞了混合编译,具体看 Android N混合编译与对热补丁影响解析

    加载补丁操作做好之后,最后还要检查一下,如果没加载成功就会执行卸载:

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

    相关文章

      网友评论

        本文标题:热修复框架 - TinkerApplication启动(二) -

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