Android插件化和热修复

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

2020-08-13  本文已影响0人  Stan_Z

代码: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;
}

流程总结:

二、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);
        }
上一篇下一篇

猜你喜欢

热点阅读