preface

The analysis of important classes is fully annotated, and the code is inGithub.com/ColorfulHor…

In the last article, we analyzed the generation of patch package. This article started from the source code to briefly analyze the synthesis process of patch package. Limited to space, it focused on the analysis of Dex synthesis process, involving knowledge including Dex loading mechanism, Art compilation mechanism, etc.

Tinker a baseline package can be patched with multiple patches, because each patch package is patched on the baseline package, and the synthesized product will be kept in a separate directory without affecting each other or the original APP. It can be removed and returned to the baseline version at any time.

Initialize the Tinker,

To use the API provided by Tinker, you need to call Tinker. install to initialize it. Otherwise, you can only use the TinkerApplicationHelper API. Synthetic patch callback. The parameters are as follows

  • ApplicationLike application agent
  • LoadReporter The callback class that loads the patch, DefaultLoadReporter by default
  • PatchReporter Callback class for synthetic patches. DefaultPatchReporter is the default
  • Listener Class to receive the synthesis patch task, DefaultPatchListener by default
  • ResultServiceClass patch patch synthesis process to synthesize results back to the master class, DefaultTinkerResultService by default
  • UpgradePatchProcessor Class that performs patch synthesis. The default value is UpgradePatch
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter, PatchListener listener, Class
        resultServiceClass, AbstractPatch upgradePatchProcessor) {
    // Create an instance and register some callbacks
    Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
        .tinkerFlags(applicationLike.getTinkerFlags())
        .loadReport(loadReporter)
        .listener(listener)
        .patchReporter(patchReporter)
        // Check whether the patch is verified by MD5
        .tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();
    Tinker.create(tinker);
    // getTinkerResultIntent is the result of the patch loading
    tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor);
    return tinker;
}
Copy the code
public void install(Intent intentResult, Class
        serviceClass, AbstractPatch upgradePatch) {
    sInstalled = true;
    / / patch service set used in patch synthesis UpgradePatch instance, as well as the synthesis results callback class DefaultTinkerResultServiceTinkerPatchService.setPatchProcessor(upgradePatch, serviceClass); . tinkerLoadResult =new TinkerLoadResult();
    tinkerLoadResult.parseTinkerResult(getContext(), intentResult);
    // Callback patch loading result
    loadReporter.onLoadResult(patchDirectory, tinkerLoadResult.loadCode, tinkerLoadResult.costTime);
}
Copy the code

Patch synthesis

After a successful download patch by calling TinkerInstaller. OnReceiveUpgradePatch composite patches, then direct call to the DefaultPatchListener. OnPatchReceived method, This method first calls patchCheck to check whether the patch should be synthesized, and then starts TinkerPatchService to synthesize the patch.

public class DefaultPatchListener implements PatchListener {
    public int onPatchReceived(String path) {
        final File patchFile = new File(path);
        // Difference apk MD5
        final String patchMD5 = SharePatchFileUtil.getMD5(patchFile);
        // Verify the patch
        // Check whether the patch should be synthesized
        // The patch is invalid/the patch is being synthesized/the system is started for the first time after OTA
        // The patch of this version has been loaded. / The patch of this version has been synthesized. / The patch merge failure exceeds the threshold
        final int returnCode = patchCheck(path, patchMD5);
        if (returnCode == ShareConstants.ERROR_PATCH_OK) {
            // Bind TinkerPatchForeService, which runs in the :patch process
            runForgService();
            // Start TinkerPatchService
            TinkerPatchService.runPatchService(context, path);
        } else {
            Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
        }
        returnreturnCode; }}Copy the code

Whether a synthetic patch is required

PatchCheck mainly checks the validity of the patch package and whether the patch of this version has been loaded/synthesized, so as to determine whether the patch needs to be synthesized. It should be noted that the patch cannot be synthesized even after the system is started for the first time after OTA upgrade, because the system is running in interpretation mode and dexopt needs to be performed again. Here is the verification process comment code. The logic related to the interpretMode used for loading patches will be analyzed separately in the loading patch, so there is no need to pay too much attention here.

protected int patchCheck(String path, String patchMd5) {
    final Tinker manager = Tinker.with(context);
    // Whether tinker is enabled
    if(! manager.isTinkerEnabled() || ! ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {return ShareConstants.ERROR_PATCH_DISABLE;
    }
    // Md5 and file validity check
    if (TextUtils.isEmpty(patchMd5)) {
        return ShareConstants.ERROR_PATCH_NOTEXIST;
    }
    final File file = new File(path);
    if(! SharePatchFileUtil.isLegalFile(file)) {return ShareConstants.ERROR_PATCH_NOTEXIST;
    }
    // Cannot be called in patch process
    if (manager.isPatchProcess()) {
        return ShareConstants.ERROR_PATCH_INSERVICE;
    }
    // The patch process is already running
    if (TinkerServiceInternals.isTinkerPatchServiceRunning(context)) {
        return ShareConstants.ERROR_PATCH_RUNNING;
    }
    If the JIT option is incorrectly enabled for systems up to 7.0 (Art was re-introduced after 7.0, some custom ROMs will incorrectly enable this option)
    if (ShareTinkerInternals.isVmJit()) {
        return ShareConstants.ERROR_PATCH_JIT;
    }
    // Information about patch loading during startup
    final TinkerLoadResult loadResult = manager.getTinkerLoadResultIfPresent();
    // Whether you are currently running in explain mode, true indicates that dexopt needs to be re-done
    final booleanrepairOptNeeded = manager.isMainProcess() && loadResult ! =null && loadResult.useInterpretMode;
    if(! repairOptNeeded) {if(manager.isTinkerLoaded() && loadResult ! =null) {
            String currentVersion = loadResult.currentVersion;
            // This version of the patch is already loaded
            if (patchMd5.equals(currentVersion)) {
                returnShareConstants.ERROR_PATCH_ALREADY_APPLY; }}// The patch has been synthesized, but the main process has not been restarted and loaded
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
        try {
            final SharePatchInfo currInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
            if(currInfo ! =null&&! ShareTinkerInternals.isNullOrNil(currInfo.newVersion) && ! currInfo.isRemoveNewVersion) {if (patchMd5.equals(currInfo.newVersion)) {
                    returnShareConstants.ERROR_PATCH_ALREADY_APPLY; }}}catch (Throwable ignored) {
            // Ignored.}}// The number of patch retries exceeds the threshold (20)
    if(! UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)) {return ShareConstants.ERROR_PATCH_RETRY_COUNT_LIMIT;
    }
    return ShareConstants.ERROR_PATCH_OK;
}
Copy the code

TinkerPatchService Pre-operation of a composite patch

TinkerPatchService is an IntentService, running in the patch synthesis process, mainly to do the pre-operation of patch synthesis, the main logic is in the doApplyPatch method.

  • callDefaultPatchReporter.onPatchServiceStartCallback to record the number of retries for synthesizing the patch
  • callUpgradePatch.tryPatchActually start crafting the patch
  • The synthesized callback results toDefaultTinkerResultService, delete the downloaded patch source file, kill the process and restart
public class TinkerPatchService extends IntentService {...public static void runPatchService(final Context context, final String path) {
        ShareTinkerLog.i(TAG, "run patch service...");
        Intent intent = new Intent(context, TinkerPatchService.class);
        // Patch path
        intent.putExtra(PATCH_PATH_EXTRA, path);
        / / synthetic results callback class name, the default DefaultTinkerResultService
        intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
        try {
            context.startService(intent);
        } catch (Throwable thr) {
            ShareTinkerLog.e(TAG, "run patch service fail, exception:"+ thr); }}@Override
    protected void onHandleIntent(Intent intent) {
        // Set priority for foreground service
        increasingPriority();
        doApplyPatch(this, intent);
    }


    private static AtomicBoolean sIsPatchApplying = new AtomicBoolean(false);

    private static void doApplyPatch(Context context, Intent intent) {
        // Since we may retry with IntentService, we should prevent
        // racing here again.
        if(! sIsPatchApplying.compareAndSet(false.true)) {
            ShareTinkerLog.w(TAG, "TinkerPatchService doApplyPatch is running by another runner.");
            return;
        }

        Tinker tinker = Tinker.with(context);
        // Callback event
        tinker.getPatchReporter().onPatchServiceStart(intent);

        if (intent == null) {
            ShareTinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
            return;
        }
        // Patch file path
        String path = getPatchPathExtra(intent);
        if (path == null) {
            ShareTinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
            return;
        }
        File patchFile = new File(path);

        long begin = SystemClock.elapsedRealtime();
        boolean result;
        long cost;
        Throwable e = null;

        PatchResult patchResult = new PatchResult();
        try {
            if (upgradePatchProcessor == null) {
                throw new TinkerRuntimeException("upgradePatchProcessor is null.");
            }
            // Call UpgradePatch tryPatch
            result = upgradePatchProcessor.tryPatch(context, path, patchResult);
        } catch (Throwable throwable) {
            e = throwable;
            result = false;
            tinker.getPatchReporter().onPatchException(patchFile, e);
        }

        cost = SystemClock.elapsedRealtime() - begin;
        tinker.getPatchReporter()
                .onPatchResult(patchFile, result, cost);

        patchResult.isSuccess = result;
        patchResult.rawPatchFilePath = path;
        patchResult.costTime = cost;
        patchResult.e = e;
        / / callback to DefaultTinkerResultService synthesis results
        AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));

        sIsPatchApplying.set(false); }}Copy the code

Start synthesizing patches

By default upgradepatch. tryPatch performs the patch composition logic as follows

  1. ShareSecurityCheckClass to extract xxx_meta files from the patch package and verify the signature
  2. ShareTinkerInternalsParse and verify the validity of TinkerId, signature, MD5, and meta files of the patch
  3. Read and parse data/data/ package name /tinker/patch.info to verify whether the patch can be synthesized. This file records patch loading information of the current version. The corresponding class isSharePatchInfo
  4. Change the name of the patch package to patch-xxx.apk and copy it to the data/data/ package name /tinker/patch-xxx directory
  5. The dex file, resource file and SO library in the patch were synthesized respectively, and dex2OAT operation was performed on the new dex after the synthesis

This paper mainly analyzes the Dex synthesis process, so library and resource file synthesis part is relatively simple, only need to care about the output directory

public class UpgradePatch extends AbstractPatch {
    private static final String TAG = "Tinker.UpgradePatch";

    @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
        Tinker manager = Tinker.with(context);
        final File patchFile = new File(tempPatchPath);
        if(! manager.isTinkerEnabled() || ! ShareTinkerInternals.isTinkerEnableWithSharedPreferences(context)) {return false;
        }

        if(! SharePatchFileUtil.isLegalFile(patchFile)) {return false;
        }
        // This class is used to read meta files in the patch pack
        ShareSecurityCheck signatureCheck = new ShareSecurityCheck(context);
        // Parse and verify the validity of patch TinkerId, signature, MD5, meta files, etc
        int returnCode = ShareTinkerInternals.checkTinkerPackage(context, manager.getTinkerFlags(), patchFile, signatureCheck);
        if(returnCode ! = ShareConstants.ERROR_PACKAGE_CHECK_OK) { manager.getPatchReporter().onPatchPackageCheckFail(patchFile, returnCode);return false;
        }

        String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
        if (patchMd5 == null) {
            return false;
        }
        // Patch package MD5 as the version number
        patchResult.patchVersion = patchMd5;

        ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchMd5:%s", patchMd5);

        // data/data/ package name /tinker
        final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();

        File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory);
        // data/data/ package name /tinker/patch.info
        File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory);
        // Read the package_meta.txt content
        final Map<String, String> pkgProps = signatureCheck.getPackagePropertiesIfPresent();
        if (pkgProps == null) {
            ShareTinkerLog.e(TAG, "UpgradePatch packageProperties is null, do we process a valid patch apk ?");
            return false;
        }

        final String isProtectedAppStr = pkgProps.get(ShareConstants.PKGMETA_KEY_IS_PROTECTED_APP);
        final booleanisProtectedApp = (isProtectedAppStr ! =null && !isProtectedAppStr.isEmpty() && !"0".equals(isProtectedAppStr));
        // Read the information about the last synthesized patch
        SharePatchInfo oldInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);

        SharePatchInfo newInfo;

        if(oldInfo ! =null) {
            // The patch has been loaded
            if (oldInfo.oldVersion == null || oldInfo.newVersion == null || oldInfo.oatDir == null) {
                ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:onPatchInfoCorrupted");
                manager.getPatchReporter().onPatchInfoCorrupted(patchFile, oldInfo.oldVersion, oldInfo.newVersion);
                return false;
            }

            if(! SharePatchFileUtil.checkIfMd5Valid(patchMd5)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:onPatchVersionCheckFail md5 %s is valid", patchMd5);
                manager.getPatchReporter().onPatchVersionCheckFail(patchFile, oldInfo, patchMd5);
                return false;
            }
            // Is the patch currently loaded in explain mode (first run after OTA)
            // Patch is loaded in explain mode in TinkerLoader, oatDir is set to interpret
            final boolean usingInterpret = oldInfo.oatDir.equals(ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
            // Check whether the patch has been synthesized. If the current patch has been loaded, it will not be synthesized
            if(! usingInterpret && ! ShareTinkerInternals.isNullOrNil(oldInfo.newVersion) && oldInfo.newVersion.equals(patchMd5) && ! oldInfo.isRemoveNewVersion) { ShareTinkerLog.e(TAG,"patch already applied, md5: %s", patchMd5);

                // The patch has been synthesized successfully. The number of retries is reset to 1
                UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);

                return true;
            }
            // Set oatDir to "changing" when loading patches in explain mode, so that the patch will not be loaded in explain mode next time
            final String finalOatDir = usingInterpret ? ShareConstants.CHANING_DEX_OPTIMIZE_PATH : oldInfo.oatDir;
            newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, isProtectedApp, false, Build.FINGERPRINT, finalOatDir, false);
        } else {
            // No patches have been loaded
            newInfo = new SharePatchInfo("", patchMd5, isProtectedApp, false, Build.FINGERPRINT, ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH, false);
        }

        final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
        // data/data/ package name /tinker/patch-xxx
        final String patchVersionDirectory = patchDirectory + "/" + patchName;

        ShareTinkerLog.i(TAG, "UpgradePatch tryPatch:patchVersionDirectory:%s", patchVersionDirectory);

        // data/data/ package name /tinker/patch-xxx/patch-xxx.apk
        File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));

        try {
            if(! patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {// Copy the patch package to destPatchFile
                SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
                ShareTinkerLog.w(TAG, "UpgradePatch copy patch file, src file: %s size: %d, dest file: %s size:%d", patchFile.getAbsolutePath(), patchFile.length(), destPatchFile.getAbsolutePath(), destPatchFile.length()); }}catch (IOException e) {
            ShareTinkerLog.e(TAG, "UpgradePatch tryPatch:copy patch file fail from %s to %s", patchFile.getPath(), destPatchFile.getPath());
            manager.getPatchReporter().onPatchTypeExtractFail(patchFile, destPatchFile, patchFile.getName(), ShareConstants.TYPE_PATCH_FILE);
            return false;
        }

        // Synthesize dex file and perform dex2OAT
        if(! DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile, patchResult)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }
        // Ark compiler related processing
        if(! ArkHotDiffPatchInternal.tryRecoverArkHotLibrary(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {return false;
        }
        // BSDiff merge so library, verify process is similar to merge dex, finally merge bspatch.patchfast
        if(! BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, try patch library failed");
            return false;
        }
        // BSDiff merges resource files. The verification process is similar to merging dex. Finally, bspatch.patchfast is synthesized
        Data /data/ package name /tinker/patch-xxx/res/resources.apk
        if(! ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, try patch resource failed");
            return false;
        }

        // Check for missing dex2OAT products as Vivo/OPPO will asynchronously execute Dex2OAT
        if(! DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, check dex opt file failed");
            return false;
        }
        // Write the composite patch information to patch.info
        if(! SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) { ShareTinkerLog.e(TAG,"UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
            return false;
        }

        // Reset the number of patch compositions recorded
        UpgradePatchRetry.getInstance(context).onPatchResetMaxCheck(patchMd5);
        return true; }}Copy the code

Dex preliminary synthesis

Patch synthesis is the most critical dex file synthesis, we focus on the analysis of this part of the logic, here or list a simple process

  1. The entrance isDexDiffPatchInternal.tryRecoverDexFilesMethod, and then calls topatchDexExtractViaDexDiff
  2. patchDexExtractViaDexDiffIn the methodextractDexDiffInternalsMethod does some prevalidation of the dex and begins composition
  3. The last calldexOptimizeDexFilesDexopt operation was performed on the synthesized dex, and the product was stored in data/data/ package name/Tinker/patch-XXX/Odex
public class DexDiffPatchInternal extends BasePatchInternal {
    private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile, PatchResult patchResult) {
        // data/data/ Package name /tinker/patch-xxx/dex
        String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
        / / synthetic dex
        if(! extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {return false;
        }

        File dexFiles = new File(dir);
        File[] files = dexFiles.listFiles();
        // Store the resultant file
        List<File> legalFiles = new ArrayList<>();
        if(files ! =null) {
            for (File file : files) {
                final String fileName = file.getName();
                // may have directory in android o
                if (file.isFile()
                    &&  (fileName.endsWith(ShareConstants.DEX_SUFFIX)
                      || fileName.endsWith(ShareConstants.JAR_SUFFIX)
                      || fileName.endsWith(ShareConstants.PATCH_SUFFIX))
                ) {
                    legalFiles.add(file);
                }
            }
        }

        ShareTinkerLog.i(TAG, "legal files to do dexopt: " + legalFiles);
        // Store the dexopt product, data/data/ package name /tinker/patch-xxx/odex
        final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
        // The dexopt command is triggered
        returndexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile, patchResult); }}Copy the code

Dex synthesis

DexDiffPatchInternal. Do the divverify extractDexDiffInternals method for dex, specific implementation in DexPatchApplier, general process is as follows

  1. The dex_meta file in the patch package was parsed and the changed DEX information was resolved into ShareDexDiffPatchInfo object to load the patchList
  2. For the Art platform,checkClassNDexFilesMethod Check whether data/data/ package name /tinker/patch-xxx/dex/ tinker_classn. apk exists (this file is packaged by all new dex after the patch is synthesized under Art) and determine whether the patch has been loaded. For the Dalvik platform, tinker_classn. apk is not packaged, but each Dex is checked to see if it needs to be synthesized
  3. After traversing patchList, start to synthesize dex in turn and callpatchDexFileAt last,DexPatchApplier.executeAndSaveToDex is synthesized, and the internal algorithm is DexDiff. After synthesis, dex is saved to data/data/ package name/Tinker /patch-xxx/dex
  4. After the synthesis of each dex, md5 verification is required to ensure the correctness of the synthesis of this dex. After the synthesis of all dex, all dex should be packaged into Tinker_classn. apk under Art

Code + detailed comments:

public class DexDiffPatchInternal extends BasePatchInternal {...private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
        patchList.clear();
        // patchList is loaded after dex_meta is parsed
        ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
        if (patchList.isEmpty()) {
            return true;
        }
        // data/data/ Package name /tinker/patch-xxx/dex
        File directory = new File(dir);
        if(! directory.exists()) { directory.mkdirs(); }//I think it is better to extract the raw files from apk
        Tinker manager = Tinker.with(context);
        ZipFile apk = null;
        ZipFile patch = null;
        try {
            ApplicationInfo applicationInfo = context.getApplicationInfo();
            if (applicationInfo == null) {
                // Looks like running on a test Context, so just return without patching.
                ShareTinkerLog.w(TAG, "applicationInfo == null!!!!");
                return false;
            }

            String apkPath = applicationInfo.sourceDir;
            / / the original apk
            apk = new ZipFile(apkPath);
            / / patches
            patch = new ZipFile(patchFile);
            // The patch is synthesized under art, all old dex and patch dex are synthesized, and then packaged as tinker_classn. apk, dalvik does not pack dex
            // Determine whether the tinker_classn. apk file needs to be generated
            if (checkClassNDexFiles(dir)) {
                ShareTinkerLog.w(TAG, "class n dex file %s is already exist, and md5 match, just continue", ShareConstants.CLASS_N_APK_NAME);
                return true;
            }
            for (ShareDexDiffPatchInfo info : patchList) {
                long start = System.currentTimeMillis();

                final String infoPath = info.path;
                String patchRealPath;
                if (infoPath.equals("")) {
                    patchRealPath = info.rawName;
                } else {
                    patchRealPath = info.path + "/" + info.rawName;
                }

                String dexDiffMd5 = info.dexDiffMd5;
                String oldDexCrc = info.oldDexCrC;
                DestMd5InDvm field value is "0" if the destMd5InDvm is not the primary dex and the dex is not changed. This dex does not need to be synthesized under Dalvik
                if(! isVmArt && info.destMd5InDvm.equals("0")) {
                    ShareTinkerLog.w(TAG, "patch dex %s is only for art, just continue", patchRealPath);
                    continue;
                }
                String extractedFileMd5 = isVmArt ? info.destMd5InArt : info.destMd5InDvm;

                if(! SharePatchFileUtil.checkIfMd5Valid(extractedFileMd5)) { ShareTinkerLog.w(TAG,"meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, extractedFileMd5);
                    manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
                    return false;
                }
                // data/data/ package name /tinker/patch-xxx/dex/dex name, which is used to store the synthesized dex
                File extractedFile = new File(dir + info.realName);

                // Check whether the synthesized dex (which has not been synthesized yet, if it exists, it has been synthesized before) already exists. If it exists, the synthesized dex has been synthesized
                // If the dex exists, check whether it is consistent with the MD5 of the pre-synthesized dex recorded in the patch package. If the dex is inconsistent, delete the existing dex
                if (extractedFile.exists()) {
                    if (SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
                        //it is ok, just continue
                        ShareTinkerLog.w(TAG, "dex file %s is already exist, and md5 match, just continue", extractedFile.getPath());
                        continue;
                    } else {
                        ShareTinkerLog.w(TAG, "have a mismatch corrupted dex "+ extractedFile.getPath()); extractedFile.delete(); }}else {
                    extractedFile.getParentFile().mkdirs();
                }
                // Patch dex in the patch package
                ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
                // old dex
                ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);
                if (oldDexCrc.equals("0")) {
                    // If oldDexCrc is 0, the dex is new
                    if (patchFileEntry == null) {
                        ShareTinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }

                    // Extract the dex from the patch package to data/data/ package name /tinker/patch-xxx/dex/
                    if(! extractDexFile(patch, patchFileEntry, extractedFile, info)) { ShareTinkerLog.w(TAG,"Failed to extract raw patch file " + extractedFile.getPath());
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false; }}else if (dexDiffMd5.equals("0")) {
                    If oldDexCrc is not "0" and dexDiffMd5 is "0", the dex is not changed
                    // In this case, the old dex needs to be copied to the patch dex directory in ART and ignored in Dalvik
                    if(! isVmArt) {continue;
                    }

                    if (rawApkFileEntry == null) {
                        ShareTinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }

                    //check source crc instead of md5 for faster
                    String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
                    // Old dex CRC check (Old dex CRC recorded in patch package and old dex CRC in current APK)
                    if(! rawEntryCrc.equals(oldDexCrc)) { ShareTinkerLog.e(TAG,"apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }

                    // Copy the old dex from the current APK to data/data/ package name /tinker/patch-xxx/dex/
                    extractDexFile(apk, rawApkFileEntry, extractedFile, info);

                    if(! SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) { ShareTinkerLog.w(TAG,"Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        SharePatchFileUtil.safeDeleteFile(extractedFile);
                        return false; }}else {
                    // The old dex and patch dex must exist in this branch
                    if (patchFileEntry == null) {
                        ShareTinkerLog.w(TAG, "patch entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }

                    if(! SharePatchFileUtil.checkIfMd5Valid(dexDiffMd5)) { ShareTinkerLog.w(TAG,"meta file md5 invalid, type:%s, name: %s, md5: %s", ShareTinkerInternals.getTypeString(type), info.rawName, dexDiffMd5);
                        manager.getPatchReporter().onPatchPackageCheckFail(patchFile, BasePatchInternal.getMetaCorruptedCode(type));
                        return false;
                    }
                    // Check whether the old dex exists in the APK
                    if (rawApkFileEntry == null) {
                        ShareTinkerLog.w(TAG, "apk entry is null. path:" + patchRealPath);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }
                    // Old dex CRC check (Old dex CRC recorded in patch package and old dex CRC in current APK)
                    String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
                    if(! rawEntryCrc.equals(oldDexCrc)) { ShareTinkerLog.e(TAG,"apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        return false;
                    }
                    // Patch synthesis, after synthesis, write synthesis result dex to extractedFile(data/data/ package name /tinker/patch-xxx/dex)
                    // Internal through the DexPatchApplier class to synthesize the patch, algorithm implementation code is not specific analysis
                    patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
                    // Check whether the MD5 value of the dex is the same as the md5 value pre-synthesized during patch package installation
                    if(! SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) { ShareTinkerLog.w(TAG,"Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
                        manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
                        SharePatchFileUtil.safeDeleteFile(extractedFile);
                        return false;
                    }

                    ShareTinkerLog.w(TAG, "success recover dex file: %s, size: %d, use time: %d", extractedFile.getPath(), extractedFile.length(), (System.currentTimeMillis() - start)); }}// Art packages all the synthesized dex as tinker_classn.apk
            if(! mergeClassNDexFiles(context, patchFile, dir)) {return false; }}catch (Throwable e) {
            throw new TinkerRuntimeException("patch " + ShareTinkerInternals.getTypeString(type) + " extract failed (" + e.getMessage() + ").", e);
        } finally {
            SharePatchFileUtil.closeZip(apk);
            SharePatchFileUtil.closeZip(patch);
        }
        return true; }}Copy the code

Dex2oat was triggered to compile and synthesize dex

Dex2oat should be operated on each dex after the dex is successfully synthesized, so as not to affect the loading speed when Dex2OAT is triggered during operation. Tinker triggers Dex2OAT with the help of PathClassLoader, and the brief process is as follows

  1. DexDiffPatchInternal.dexOptimizeDexFilesMethod does a pre-operation and then calls toTinkerDexOptimizer.optimizeAllMethod, called separately for each dex/ APk/JAR fileOptimizeWorker.runMethods Dex2OAT was prepared
  2. Called on android8.0 and aboveNewClassLoaderInjector.triggerDex2OatMethods throughDelegateLastClassLoaderorTinkerClassLoaderTrigger dex2OAT, direct call below 8.0DexFile.loadDex
  3. Note that Android 10 will no longer call dex2OAT from the application process, only accept OAT files generated by the system, so you need to passtriggerPMDexOptOnDemandMethod reflection calls the PMS performDexOptSecondary method to try again to trigger dex2OAT

DexDiffPatchInternal dexOptimizeDexFiles pre operation

Data /data/ package name /tinker/patch-xxx/oat//xxx.odex, 8.0 indicates data/data/ package name /tinker/patch-xxx/odex/xxx.dex, where ISA indicates the CPU architecture, such as ARM64

private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile, final PatchResult patchResult) {
    final Tinker manager = Tinker.with(context);
    optFiles.clear();
    if(dexFiles ! =null) {
        // data/data/ package name /tinker/patch-xxx/odex
        File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
        if(! optimizeDexDirectoryFile.exists() && ! optimizeDexDirectoryFile.mkdirs()) {return false;
        }
        // add opt files
        for (File file : dexFiles) {
            // Get the output path of dexopt product
            // Data /data/ package name /tinker/patch-xxx/oat/
      
       /xxx.odex
      
            Data /data/ package name /tinker/patch-xxx/odex/xxx.dex
            String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
            optFiles.add(newFile(outputPathName)); }...// Whether to use the DelegateLastClassLoader class
        final boolean useDLC = TinkerApplication.getInstance().isUseDelegateLastClassLoader();
        final boolean[] anyOatNotGenerated = {false};
        // Start parallel dexoptTinkerDexOptimizer.optimizeAll( context, dexFiles, optimizeDexDirectoryFile, useDLC, ......) ; . }return true;
}
Copy the code

Optimizeworker.run versions trigger dex2OAT

This method NewClassLoaderInjector. TriggerDex2Oat function and the analysis of the triggerPMDexOptOnDemand will separate out. In addition, it should be noted that useInterpretMode calls interpretDex2Oat to interpretmode dex2OAT, which is to cope with the failure of odex after OTA upgrade of the system. Details will be explained in the patch loading section.

private static class OptimizeWorker {
    boolean run(a) {
        try{...// Data /data/ package name /tinker/patch-xxx/oat/
      
       /xxx.odex
      
            Data /data/ package name /tinker/patch-xxx/odex/xxx.dex
            String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
            if(! ShareTinkerInternals.isArkHotRuning()) {if (useInterpretMode) {
                    // This branch is executed the first time the system runs after OTA
                    interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
                } else if (Build.VERSION.SDK_INT >= 26
                        || (Build.VERSION.SDK_INT >= 25&& Build.VERSION.PREVIEW_SDK_INT ! =0)) {
                    // Trigger dex2OAT by loading dex through PathClassLoader/
                    NewClassLoaderInjector.triggerDex2Oat(context, optimizedDir,
                                                          useDLC, dexFile.getAbsolutePath());
                    // https://developer.android.google.cn/about/versions/10/behavior-changes-10?hl=zh-cn#system-only-oat
                    Android10 will no longer call dex2OAT from the application process, only accept OAT files generated by the system
                    / / oat_file_manager. Cc OatFileManager: : OpenDexFilesFromOat oat_file_assistant. No longer call MakeUpToDate
                    // Here the PMS triggers the background dex2OAT again
                    triggerPMDexOptOnDemand(context, dexFile.getAbsolutePath(), optimizedPath);
                } else {
                    // Trigger dex2OAT using DexFile directly under Android8.0
                    DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0); }}if(callback ! =null) {
                callback.onSuccess(dexFile, optimizedDir, newFile(optimizedPath)); }}catch (final Throwable e) {
            if(callback ! =null) {
                callback.onFailed(dexFile, optimizedDir, e);
                return false; }}return true; }}Copy the code

Dex2oat is triggered by the PathClassLoader

NewClassLoaderInjector. TriggerDex2Oat calls to NewClassLoaderInjector. CreateNewClassLoader, Use DelegateLastClassLoader to trigger dex2OAT if useDLC=true and Android version greater than 8.1, otherwise Create TinkerClassLoader to trigger dex2OAT. In fact, when dex2OAT is triggered, the effect of the two Classloaders is the same. Finally, dex2OAT is triggered by DexPathList. The newly created ClassLoader is only used to trigger Dex2OAT.

NewClassLoaderInjector. CreateNewClassLoader another call in patch loading logic, Use the new classerLoader to replace the original Application PathClassLoader when loading patches to avoid the impact of mixed compilation and hot patches after android7.0. So I think it’s better to write this method as two separate functions to avoid confusion when reading the code. The only thing we need to know in this article is that both DelegateLastClassLoader and TinkerClassLoader trigger dex2OAT. Further analysis of them will be left for patch loading in the next article.

DelegateLastClassLoader is a new class from android8.1 that inherits from PathClassLoader and implements the last-look strategy.

  1. Look for classes from boot Classpath
  2. Look for classes from the dexPath of the classLoader
  3. Finally, the class is looked up from the classLoader’s parents
  4. Finally, the lookup policy does not comply with the parent delegate, and the class is finally looked up from the Parent ClassLoader

TinkerClassLoader does a similar job here, rewriting the findClass method so that classes are found first from TinkerClassLoader and then from the original PathClassLoader

private static ClassLoader createNewClassLoader(ClassLoader oldClassLoader,
                                                    File dexOptDir,
                                                    boolean useDLC,
                                                    String... patchDexPaths) throws Throwable {
        // Reflection gets the DexPathList field of the BaseDexClassLoader
        // Old oldClassLoader is the PathClassLoader of the current app
        final Field pathListField = findField(
                Class.forName("dalvik.system.BaseDexClassLoader".false, oldClassLoader),
                "pathList");
        final Object oldPathList = pathListField.get(oldClassLoader);

        final StringBuilder dexPathBuilder = new StringBuilder();
        final booleanhasPatchDexPaths = patchDexPaths ! =null && patchDexPaths.length > 0;
        if (hasPatchDexPaths) {
            for (int i = 0; i < patchDexPaths.length; ++i) {
                if (i > 0) { dexPathBuilder.append(File.pathSeparator); } dexPathBuilder.append(patchDexPaths[i]); }}// Splice the dex file path that requires dex2Oat
        final String combinedDexPath = dexPathBuilder.toString();

        // Reflect the nativeLibraryDirectories field in the DexPathList, so library directories
        final Field nativeLibraryDirectoriesField = findField(oldPathList.getClass(), "nativeLibraryDirectories"); .// splice the so library path
        final String combinedLibraryPath = libraryPathBuilder.toString();

        ClassLoader result = null;
        if (useDLC && Build.VERSION.SDK_INT >= 27) {
            // https://developer.android.google.cn/reference/dalvik/system/DelegateLastClassLoader
            // https://www.androidos.net.cn/android/10.0.0_r6/xref/libcore/dalvik/src/main/java/dalvik/system/DelegateLastClassLoader.j ava
            // DelegateLastClassLoader is new after android8.1 and inherits from PathClassLoader to implement the last-look policy
            result = new DelegateLastClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
            // Set the previous PathClassLoader to be the parent of the created DelegateLastClassLoader
            final Field parentField = ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(result, oldClassLoader);
        } else {
            // Do the same thing as DelegateLastClassLoader
            result = new TinkerClassLoader(combinedDexPath, dexOptDir, combinedLibraryPath, oldClassLoader);
        }

        Android8.0 replaces the original PathClassLoader with the new classLoader in the PathList
        // Android 8.0 does not support multiple classloaders using the same DexFile object to define classes at the same time, so it cannot be replaced
        if (Build.VERSION.SDK_INT < 26) {
            findField(oldPathList.getClass(), "definingContext").set(oldPathList, result);
        }

        return result;
    }
Copy the code

TriggerPMDexOptOnDemand ensures that dex2oat is triggered after Android10

Due to android10’s change of dex2oat strategy, further processing must be done here. The main process of tinker is as follows

  1. QueryPerformDexOptSecondaryTransactionCode reflex () method gets used to identifyPackageManagerService.performDexOptSecondaryThe transaction code of the method
  2. Reflection ServiceManager gets the PMS client proxy IBinder object
  3. Call with binderPackageManagerService.performDexOptSecondaryAnd trigger Dex2OAT in Quicken mode
private static void triggerPMDexOptOnDemand(Context context, String dexPath, String oatPath) {
    if (Build.VERSION.SDK_INT < 29) {
        return;
    }
    try {
        final File oatFile = new File(oatPath);
        if (oatFile.exists()) {
            return;
        }
        boolean performDexOptSecondarySuccess = true;
        try {
            / / call the PMS. PerformDexOptSecondary dex2oat trigger
            performDexOptSecondary(context, oatPath);
        } catch (Throwable thr) {
            performDexOptSecondarySuccess = false;
        }
        SystemClock.sleep(1000);
        if(! performDexOptSecondarySuccess || ! oatFile.exists()) {// The execution fails. If the system is huawei, perform additional processing
            if ("huawei".equalsIgnoreCase(Build.MANUFACTURER) || "honor".equalsIgnoreCase(Build.MANUFACTURER)) { registerDexModule(context, dexPath, oatPath); }}... }Copy the code
public static void performDexOptSecondary(Context context, String oatPath) throws IllegalStateException {
    try {
        final File oatFile = new File(oatPath);
        / / reflection for PMS agent used in ipc tag PMS. TransactionCode performDexOptSecondary methods
        final int transactionCode = queryPerformDexOptSecondaryTransactionCode();
        final String packageName = context.getPackageName();
        // Dex2OAT compile mode
        final String targetCompilerFilter = "quicken";
        final boolean force = false;

        finalClass<? > serviceManagerClazz = Class.forName("android.os.ServiceManager");
        final Method getServiceMethod = ShareReflectUtil.findMethod(serviceManagerClazz, "getService", String.class);
        // Get the PMS remote proxy
        final IBinder pmBinder = (IBinder) getServiceMethod.invoke(null."package");
        if (pmBinder == null) {
            throw new IllegalStateException("Fail to get pm binder.");
        }
        // Retry 20 times
        final int maxRetries = 20;
        for (int i = 0; i < maxRetries; ++i) {
            Throwable pendingThr = null;
            try {
                // Call PMS performDexOptSecondary directly with binder
                performDexOptSecondaryImpl(pmBinder, transactionCode, packageName, targetCompilerFilter, force);
            } catch (Throwable thr) {
                pendingThr = thr;
            }
            SystemClock.sleep(3000);
            if (oatFile.exists()) {
                break;
            }
            if (i == maxRetries - 1) {
                if(pendingThr ! =null) {
                    throw pendingThr;
                }
                if(! oatFile.exists()) {throw new IllegalStateException("Expected oat file: " + oatFile.getAbsolutePath() + " does not exist."); }}}... }Copy the code

Here for PackageManagerService. PerformDexOptSecondary methods will no longer opened, are interested can go to read the source code itself, since patch synthesis process has been parsed.

Afterword.

Follow the source code all the way down obviously can feel the author for framework layer understanding, different Android versions of the framework changes for Tinker’s impact is very big, manufacturers for ROM customization modification will lead to a series of problems, to solve these problems is not easy things. I do not fully understand some details, and I hope to point out any mistakes.