preface

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

In the last article we looked at the synthesis process of the patch pack, and in this article we will step through the loading process of the patch pack. Patch load mainly dex and resource files to load, for dex file, load is actually converting dex patch Element is inserted into the app PathClassLoader. PathList. The front dexElements array, For dex loading process do not understand the reader can go to see the relevant knowledge or source code; For resource loading, it is mainly to replace the original AssetManager, which is relatively less complicated; In addition, the processing of new activities involves hook related content, so we need to have some understanding of the startup process of the activity.

Application isolation

Tinker requires that the Application class be separated from other logical classes. Tinker uses AnnotationProcessor to generate the Application class. The lifecycle is then delegated to the ApplicationLike class. The Application is not visible to the developer, and all the logical code is placed in ApplicationLike, avoiding Application references to other classes. There is no specific analysis of how Tinker generates Application, because the generated Application does not play any actual role, you can look at the relevant source code if you are interested.

The reason for Application isolation

Tinker replaces PathClassLoader with custom ClassLoader in Application to load subsequent classes because of the crash caused by mixed compilation after Android N. The Application class itself must be loaded by the PathClassLoader first. Therefore, if the Application class references other logical classes, the class will be loaded by the PathClassLoader first. If the Application class is loaded by the custom ClassLoader after the replacement, the class will be loaded by the PathClassLoader. The ClassCastException problem occurs when you use it.

Patch loading time and general process

After integrating with Tinker, the Application entry becomes the ApplicationLike class for developers, but the actual Application entry is still the Application class. The Application class loads the patches before launching ApplicationLike. So the logic for patch loading is in the TinkerApplication class. TinkerApplication does a few things, just to list them briefly:

  1. attachBaseContextCalled when theloadTinkerMethod to load the patch,This step replaces the PathClassLoader
  2. createInlineFenceCreate TinkerApplicationInlineFence instance, this kind of used to prevent Art under the effects of inline
  3. Through TinkerApplicationInlineFence callback ApplicationLike lifecycle methods
public abstract class TinkerApplication extends Application {
    protected void onBaseContextAttached(Context base, long applicationStartElapsedTime, long applicationStartMillisTime) {
        try {
            Reflection calls the tryLoad method of the loader class
            // Since developers can customize the extension loader, the call is reflected according to the loader class name configured in ApplicationLike
            // Load the patch
            loadTinker();
            // loadTinker has replaced app PathClassLoader with cl
            mCurrentClassLoader = base.getClassLoader();
            mInlineFence = createInlineFence(this, tinkerFlags, delegateClassName,
                    tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
                    tinkerResultIntent);
            // Callback ApplicationLike onBaseContextAttached
            TinkerInlineFenceAction.callOnBaseContextAttached(mInlineFence, base);
            //reset save mode
            if (useSafeMode) {
                ShareTinkerInternals.setSafeModeCount(this.0); }}catch (TinkerRuntimeException e) {
            throw e;
        } catch (Throwable thr) {
            throw newTinkerRuntimeException(thr.getMessage(), thr); }}}Copy the code

TinkerApplicationInlineFence stop inline

Method inlining can simply be understood as the optimization behavior of copying code directly to the invocation because the method being called is too simple to push onto the method stack.

If the ApplicationLike method is called directly from the Application class, the ApplicationLike method is inlined to the Application at compile time and executed because the PathClassLoader is replaced, The ClassLoader used to load ApplicationLike has been replaced, and the inlined code and the ClassLoader used to load ApplicationLike have been replaced. The inlined code and the ClassLoader used to load ApplicationLike have been replaced. >=N systems will have a ClassCastException again.

To stop for inline ApplicationLike TinkerApplicationInlineFence call as an intermediary, through the special name plus the try block of code to prevent inlining, While TinkerApplicationInlineFence itself is a Handler.

private Handler createInlineFence(Application app,
                                      int tinkerFlags,
                                      String delegateClassName,
                                      boolean tinkerLoadVerifyFlag,
                                      long applicationStartElapsedTime,
                                      long applicationStartMillisTime,
                                      Intent resultIntent) {
    try {
        // Reflect the ApplicationLike class with the replaced classLoader
        finalClass<? > delegateClass = Class.forName(delegateClassName,false, mCurrentClassLoader);
        finalConstructor<? > constructor = delegateClass.getConstructor(Application.class,int.class, boolean.class,
                long.class, long.class, Intent.class);
        // Create ApplicationLike instance
        final Object appLike = constructor.newInstance(app, tinkerFlags, tinkerLoadVerifyFlag,
                applicationStartElapsedTime, applicationStartMillisTime, resultIntent);
        / / create TinkerApplicationInlineFence reflection
        finalClass<? > inlineFenceClass = Class.forName("com.tencent.tinker.entry.TinkerApplicationInlineFence".false, mCurrentClassLoader);
        finalClass<? > appLikeClass = Class.forName("com.tencent.tinker.entry.ApplicationLike".false, mCurrentClassLoader);
        finalConstructor<? > inlineFenceCtor = inlineFenceClass.getConstructor(appLikeClass); inlineFenceCtor.setAccessible(true);
        return (Handler) inlineFenceCtor.newInstance(appLike);
    } catch (Throwable thr) {
        throw new TinkerRuntimeException("createInlineFence failed", thr); }}Copy the code
public final class TinkerApplicationInlineFence extends Handler {
    private final ApplicationLike mAppLike;

    public TinkerApplicationInlineFence(ApplicationLike appLike) {
        mAppLike = appLike;
    }

    @Override
    public void handleMessage(Message msg) {
        handleMessage_$noinline$(msg);
    }
    // Special naming method
    private void handleMessage_$noinline$(Message msg) {
        // Try block prevents inlining
        try {
            dummyThrowExceptionMethod();
        } finally{ handleMessageImpl(msg); }}...private static void dummyThrowExceptionMethod(a) {
        if (TinkerApplicationInlineFence.class.isPrimitive()) {
            throw newRuntimeException(); }}}Copy the code

Loading a patch Pre-operation

First in TinkerApplication. LoadTinker reflection calls TinkerLoader. TryLoad, the last call to TinkerLoader. TryLoadPatchFilesInternal, this method is mainly to a series of check before loading.

private void loadTinker(a) {
    try {
        // Since the loader class can be customized by the developer, the tinker Loader instance is created by reflection. The default is TinkerLoaderClass<? > tinkerLoadClass = Class.forName(loaderClassName,false, TinkerApplication.class.getClassLoader());
        // Call TinkerLoader tryLoadMethod 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 = newIntent(); ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION); tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e); }}Copy the code
public class TinkerLoader extends AbstractTinkerLoader {
    public Intent tryLoad(TinkerApplication app) {
        ShareTinkerLog.d(TAG, "tryLoad test test");
        // Intent Records the information about loading the patch
        Intent resultIntent = new Intent();
        long begin = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(app, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        returnresultIntent; }}Copy the code

TinkerLoader. TryLoadPatchFilesInternal in patch for a series of check first, and then check after synthesis of dex, so the integrity of the library, the resource file legitimacy, Finally, call TinkerDexLoader, TinkerSoLoader, and TinkerResourceLoader in sequence to load the patch.

In addition, the method contains related logic to explain the mode operation of the system after OTA upgrade, which is separately presented here to avoid confusion when reading the logic of loading patches.

This section describes how to load patch ANR after OTA

Dex dynamically loaded before Android 8.0 will be fully compiled in speed mode, and the OAT file of the old patch dex will become invalid after OTA update of the system. At this time, dex2OAT will be re-executed after app running, which may take a long time and cause ANR. Therefore, before loading the patch in TinkerLoader, determine whether OTA will be run for the first time, and perform dex2OAT operation in Quiken /interpret-only mode for patch DEX to explain the operation mode, and then perform full programming in the background. The general process is as follows.

  1. When TinkerLoader loads the patch, it determines whether the loading is the first time after OTA upgrade of the system. If so, it will be calledTinkerDexOptimizer.optimizeAllDexopt the synthesized dex in explain mode and save it in the data/data/ package name/Tinker /patch-xxx/interpret folder. Load odex and thenSet oatDir to Interpet in patch.info and resultIntent
  2. After the patch is loaded,Tinker.installCalled when theTinkerLoadResult.parseTinkerResultParse intentResult, judge oatDir as interpet, then callbackDefaultPatchListener.onLoadInterpret, and then called againUpgradePatch.tryPatch.Retrace the patch composition process and re-trigger background Dex2OATAnd at the same timeInfo: set oatDir to changingIn order toThe next time the patch is loaded, it will not be loaded in explain mode
  3. “OatDir” is determined to be “changing” in TinkerLoader during patch loading next time, indicating that OAT file has been generated and patch loading in explain mode is no longer required. At this time, isRemoveInterpretOATDir in patch.info is “true”. Delete the data/data/ package name /tinker/patch-xxx/interpret folder next load

Let’s start with the code, take a quick look at the preparation before loading the patch, and then move on to the actual loading process

public class TinkerLoader extends AbstractTinkerLoader {
    private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {... Check whether the patch directory and patch.info file exist// Read the information recorded in patch.info
        patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
        if (patchInfo == null) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
            return;
        }

        final boolean isProtectedApp = patchInfo.isProtectedApp;
        resultIntent.putExtra(ShareIntentUtil.INTENT_IS_PROTECTED_APP, isProtectedApp);
        // Last loaded patch version
        String oldVersion = patchInfo.oldVersion;
        // Current patch version to load
        String newVersion = patchInfo.newVersion;
        String oatDex = patchInfo.oatDir;
        // There is no patch
        if (oldVersion == null || newVersion == null || oatDex == null) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_CORRUPTED);
            return;
        }
        // Whether the main process (app running process)
        boolean mainProcess = ShareTinkerInternals.isInMainProcess(app);
        boolean isRemoveNewVersion = patchInfo.isRemoveNewVersion;

        if (mainProcess) {
            final String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
            // The patch clearing operation does not take effect immediately, but only when the next load is loaded
            If the patch is loaded, reset patch.info and kill all processes except the main process
            if (isRemoveNewVersion) {
                if(patchName ! =null) {
                    // The same version indicates that the patch has been loaded before
                    final boolean isNewVersionLoadedBefore = oldVersion.equals(newVersion);
                    // Reset patch information
                    if (isNewVersionLoadedBefore) {
                        oldVersion = "";
                    }
                    newVersion = oldVersion;
                    patchInfo.oldVersion = oldVersion;
                    patchInfo.newVersion = newVersion;
                    patchInfo.isRemoveNewVersion = false;
                    SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
                    // Delete the patch file
                    String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
                    SharePatchFileUtil.deleteDir(patchVersionDirFullPath);

                    if (isNewVersionLoadedBefore) {
                        // If the patch has been loaded, kill other processes
                        ShareTinkerInternals.killProcessExceptMain(app);
                        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
                        return; }}}// Whether to delete the odex file generated by the explain compilation
            if (patchInfo.isRemoveInterpretOATDir) {
                // Override patch.info to remove the odex flag, then kill other processes and delete the odex file
                patchInfo.isRemoveInterpretOATDir = false;
                SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
                ShareTinkerInternals.killProcessExceptMain(app);
                String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
                // data/data/ package name /tinker/patch-xxx/interpet
                SharePatchFileUtil.deleteDir(patchVersionDirFullPath + "/" + ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH);
            }
        }

        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OLD_VERSION, oldVersion);
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_NEW_VERSION, newVersion);
        // oldVersion differs from newVersion, indicating that a new patch has been synthesized but not loaded yet
        booleanversionChanged = ! (oldVersion.equals(newVersion));// changing indicates that the background dex2OAT is complete, and switches to unexplained mode
        // Use the odex file generated by dex2OAT to delete the odex file that explains the mode
        boolean oatModeChanged = oatDex.equals(ShareConstants.CHANING_DEX_OPTIMIZE_PATH);
        oatDex = ShareTinkerInternals.getCurrentOatMode(app, oatDex);
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, oatDex);

        String version = oldVersion;
        if (versionChanged && mainProcess) {
            version = newVersion;
        }
        if (ShareTinkerInternals.isNullOrNil(version)) {
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_INFO_BLANK);
            return;
        }
        // The name of the patch to be loaded is patch-641E634C
        String patchName = SharePatchFileUtil.getPatchVersionDirectory(version);
        // Patch path data/data/ package name /tinker/ patch-641E634c
        String patchVersionDirectory = patchDirectoryPath + "/" + patchName;

        File patchVersionDirectoryFile = new File(patchVersionDirectory);

        final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
        // Patch file: data/data/ package name /tinker/patch-md5/patch-md5.apkFile patchVersionFile = (patchVersionFileRelPath ! =null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);

        // This class is used to read meta files in patch packages for verification
        ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
        // Verify the validity of patch TinkerId, signature, MD5, and meta files
        int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
        
        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_CONFIG, securityCheck.getPackagePropertiesIfPresent());

        // Whether to synthesize dex
        final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
        // Whether ark compiler
        final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();

        if(! isArkHotRuning && isEnabledForDex) {// Parse dex_meta to check whether the dex to be loaded and the corresponding odex exist
            boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, oatDex, resultIntent);
            if(! dexCheck) {return; }}final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);

        if (isEnabledForNativeLib) {
            // Check the validity of the so library
            boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if(! libCheck) {return; }}final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
        ShareTinkerLog.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
        if (isEnabledForResource) {
            // Verify patch resource correctness
            boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
            if(! resourceCheck) {return; }}// Check whether the system has undergone OTA upgrade. After the system is started for the first time after OTA, run in explain mode first
        // This operation is not required after 8.0
        boolean isSystemOTA = ShareTinkerInternals.isVmArt()
            && ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
            && Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();

        resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, isSystemOTA);

        if (mainProcess) {
            if (versionChanged) {
                patchInfo.oldVersion = version;
            }

            if (oatModeChanged) {
                patchInfo.oatDir = oatDex;
                patchInfo.isRemoveInterpretOATDir = true; }}// Determine whether it is in safe mode, that is, whether the patch fails to be loaded for more than three times. Delete the patch for rollback after three times
        if(! checkSafeModeCount(app)) {if (mainProcess) {
                // The main process kills other processes and then deletes them directly
                patchInfo.oldVersion = "";
                patchInfo.newVersion = "";
                patchInfo.isRemoveNewVersion = false;
                SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
                ShareTinkerInternals.killProcessExceptMain(app);

                String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
                SharePatchFileUtil.deleteDir(patchVersionDirFullPath);

                resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
                return;
            } else {
                // Set patchInfo isRemoveNewVersion to true for non-main processes and delete it next time in main processesShareTinkerInternals.cleanPatch(app); }}if(! isArkHotRuning && isEnabledForDex) {// Load dex, isSystemOTA = true to explain mode loading
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);

            if (isSystemOTA) {
                // update fingerprint after load success
                patchInfo.fingerPrint = Build.FINGERPRINT;
                OatDir = interpet; // Patch loading succeeded at first startup after OTA
                patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
                oatModeChanged = false;

                if(! SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {return;
                }
                // Set "oatDir" to interpret in the intent
                resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
            }
            if(! loadTinkerJars) {return; }}if (isEnabledForResource) {
            // Load the resource file
            boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
            if(! loadTinkerResources) {return; }}// TODO
        if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
            ComponentHotplug.install(app, securityCheck);
        }

        if(! AppInfoChangedBlocker.tryStart(app)) { ShareTinkerLog.w(TAG,"tryLoadPatchFiles:AppInfoChangedBlocker install fail.");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_BAIL_HACK_FAILURE);
            return;
        }

        // 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 || oatModeChanged)) {
            //update old version to new
            if(! SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) { ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL); ShareTinkerLog.w(TAG,"tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                return;
            }

            ShareTinkerInternals.killProcessExceptMain(app);
        }

        //all is ok!
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
        ShareTinkerLog.i(TAG, "tryLoadPatchFiles: load end, ok!"); }}Copy the code

Load the patch

The patch loading part focuses on dex loading, resource file loading and related processing of new activities.

Dex load

Dex load mainly logic in TinkerDexLoader. LoadTinkerJars method, this method to do some checking first, then call to SystemClassLoaderAdder. InstallDexes method to load, If the system is booted for the first time after OTA, the old OAT files need to be deleted, and then dex2OAT reload is performed to explain the mode.

public class TinkerDexLoader {
    // The dex to be loaded in dalvik
    private static final ArrayList<ShareDexDiffPatchInfo> LOAD_DEX_LIST = new ArrayList<>();

    // Dex to be loaded under art
    private static HashSet<ShareDexDiffPatchInfo> classNDexInfo = new HashSet<>();

    public static boolean loadTinkerJars(final TinkerApplication application, String directory, 
        String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
        if (LOAD_DEX_LIST.isEmpty() && classNDexInfo.isEmpty()) {
            return true;
        }

        ClassLoader classLoader = TinkerDexLoader.class.getClassLoader();
        if(classLoader ! =null) {}else {
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
            return false;
        }
        String dexPath = directory + "/" + DEX_PATH + "/";
        // Set of dex to load
        ArrayList<File> legalFiles = new ArrayList<>();

        for (ShareDexDiffPatchInfo info : LOAD_DEX_LIST) {
            // Skip unchanged non-primary dex in dalvik
            if (isJustArtSupportDex(info)) {
                continue;
            }
            String path = dexPath + info.realName;
            File file = new File(path);

            if (application.isTinkerLoadVerifyFlag()) {
                long start = System.currentTimeMillis();
                String checkMd5 = getInfoMd5(info);
                // Verify MD5 in dex MD5 and dex_meta
                if(! SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) { ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH); intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH, file.getAbsolutePath());return false;
                }
            }
            legalFiles.add(file);
        }
        if(isVmArt && ! classNDexInfo.isEmpty()) {// All dex is synthesized into tinker_classn. apk and dex2OAT is formed into tinker_classn. odex
            // The resultant tinker_classn. apk file will be automatically found under art
            File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
            long start = System.currentTimeMillis();

            if (application.isTinkerLoadVerifyFlag()) {
                for (ShareDexDiffPatchInfo info : classNDexInfo) {
                    // Verify MD5 in dex MD5 and dex_meta
                    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;
                    }
                }
            }
            legalFiles.add(classNFile);
        }
        File optimizeDir = new File(directory + "/" + oatDir);

        if (isSystemOTA) {
            final boolean[] parallelOTAResult = {true};
            final Throwable[] parallelOTAThrowable = new Throwable[1];
            String targetISA;
            try {
                // Get the CPU instruction set
                targetISA = ShareTinkerInternals.getCurrentInstructionSet();
            } catch (Throwable throwable) {
                deleteOutOfDateOATFile(directory);
                intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_INTERPRET_EXCEPTION, throwable);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_GET_OTA_INSTRUCTION_SET_EXCEPTION);
                return false;
            }
            // Delete invalid OAT files
            deleteOutOfDateOATFile(directory);

            // data/data/ package name /tinker/patch-xxx/interpret
            optimizeDir = new File(directory + "/" + INTERPRET_DEX_OPTIMIZE_PATH);
            // Explain the mode dex2OATTinkerDexOptimizer.optimizeAll(......) ;if(! parallelOTAResult[0]) {
                ShareTinkerLog.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 {
            final boolean useDLC = application.isUseDelegateLastClassLoader();
            // Inject classLoader starts loading
            SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp, useDLC);
        } catch (Throwable e) {
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
            return false;
        }
        return true; }}Copy the code

SystemClassLoaderAdder installDexes methods according to the judgment system version, In android7.0 and above version called when NewClassLoaderInjector. Inject create new PathClassLoader replace app original PathClassLoader, avoid android7.0 after to mix compilation and hotfixes influence, We talked about that before. For the following version 7.0, direct call SystemClassLoaderAdder. InjectDexesInternal method, insert the patch dex to app PathClassLoader pathList. DexElements front. . After completion of loading by TinkerTestDexLoad isPatch whether patch load is successful, TinkerTestDexLoad. IsPatch to false in the loader, were the test after loaded patches. Dex in the class to true.

public class SystemClassLoaderAdder {
    // Check whether the patch has successfully loaded the class
    public static final String CHECK_DEX_CLASS = "com.tencent.tinker.loader.TinkerTestDexLoad";
    public static final String CHECK_DEX_FIELD = "isPatch";

    public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files,
                                    boolean isProtectedApp, boolean useDLC) throws Throwable {

        if(! files.isEmpty()) { files = createSortedAdditionalPathEntries(files); ClassLoader classLoader = loader;if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
                // Create a new ClassLoader after 7.0 to replace the original ClassLoader to avoid mixed compilation problems
                classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, useDLC, files);
            } else {
                // Hardening also does not replace the ClassLoader
                // Before android7.0 insert dex directly into the original ClassLoader's pathList dexElements first
                injectDexesInternal(classLoader, files, dexOptDir);
            }
            sPatchDexCount = files.size();
            / / reflection TinkerTestDexLoad isPatch determine whether loaded successfully
            if(! checkDexInstall(classLoader)) { SystemClassLoaderAdder.uninstallPatchDex(classLoader);throw newTinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL); }}}}Copy the code

Dex patch Insert

SystemClassLoaderAdder. InjectDexesInternal method according to different android version to take different approaches to insert patch dex, we simply see V23. Install.

private static final class V23 {

    private static void install(ClassLoader loader, List
       
         additionalClassPathEntries, File optimizedDirectory)
       
        throws IllegalArgumentException, IllegalAccessException,
        NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
        / / get BaseDexClassLoader pathList
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        . / / makePathElements reflection calls DexPathList makePathElements/makeDexElements Element array
        // Insert the Element array generated by patch dex into dexpathList.dexElements
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                ShareTinkerLog.w(TAG, "Exception in makePathElement", e);
                throwe; }}}}Copy the code

This replacement

Mentioned in the previous article has loaded this replacement, NewClassLoaderInjector. CreateNewClassLoader according to different version and configuration to create the corresponding agent for this, . Then call NewClassLoaderInjector doInject replace App everywhere in this instance for the broker, this makes the runtime from the agent first load patch in this class, from the original after this load.

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
private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
        Thread.currentThread().setContextClassLoader(classLoader);
        / / ContextWrapper mBase is the app ContextImpl instance, LoadedApk. Create makeApplication
        final Context baseContext = (Context) findField(app.getClass(), "mBase").get(app);
        try {
            // Replace mClassLoader in ContextImpl
            findField(baseContext.getClass(), "mClassLoader").set(baseContext, classLoader);
        } catch (Throwable ignored) {
            // There is no mClassLoader in ContextImpl before 8.0
        }
        // App ContextImpl mPackageInfo is a LoadedApk instance
        final Object basePackageInfo = findField(baseContext.getClass(), "mPackageInfo").get(baseContext);
        // Replace the ClassLoader for LoadedApk
        findField(basePackageInfo.getClass(), "mClassLoader").set(basePackageInfo, classLoader);

        if (Build.VERSION.SDK_INT < 27) {
            final Resources res = app.getResources();
            try {
                // Replace ClassLoader in Resources
                findField(res.getClass(), "mClassLoader").set(res, classLoader);

                final Object drawableInflater = findField(res.getClass(), "mDrawableInflater").get(res);
                if(drawableInflater ! =null) {
                    // Replace ClassLoader in DrawableInflater
                    findField(drawableInflater.getClass(), "mClassLoader").set(drawableInflater, classLoader); }}catch (Throwable ignored) {
                // Ignored.}}}Copy the code

Resource file loading

Load the resource bundle logic is not complicated, the main logic in TinkerResourceLoader. LoadTinkerResources method, the first check arsc file md5, Then call TinkerResourcePatcher. MonkeyPatchExistingResources method really start loading resource bundles, the main process is as follows.

  1. Set the mResDir field in the LoadedApk instance of the original app to the new resource pack path
  2. Create a new AssetManager instance and set its resource path to the new resource bundle path
  3. Set Resources. MAssets to the newly created AssetManager, clear the typedArray cache in Resources of the original app, and refresh the resource configuration

One point to note that set the new AssetManager resource path, after the 7.0 system in the case of the app using the Shared repository should also call AssetManager. AddAssetPathAsSharedLibrary. ApplicationInfo. SharedLibraryFiles storage app use Shared repository, the Shared resource is like so libraries, can be Shared resource bundle for use in other applications. If your app uses a shared repository, you may encounter a problem where the resource ID in the SharedLibrary R class is inconsistent with the Package ID in the AssetManager after hotfix. Add the patch resource pack to the shared resource library. See #1372 SharedLibrary in Android Resource Management

public class TinkerResourceLoader {
    public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {
        if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
            return true;
        }
        // data/data/ package name /tinker/patch-xxx/res/resources.apk
        String resourceString = directory + "/" + RESOURCE_PATH +  "/" + RESOURCE_FILE;
        File resourceFile = new File(resourceString);
        if (application.isTinkerLoadVerifyFlag()) {
            // Verify arSC file MD5
            if(! SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) { ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);return false; }}try {
           // Load the resource bundle
           TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);
        } catch (Throwable e) {
            // Failed to load resources
            try {
                SystemClassLoaderAdder.uninstallPatchDex(application.getClassLoader());
            } catch (Throwable throwable) {
                ShareTinkerLog.e(TAG, "uninstallPatchDex failed", e);
            }
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
            return false;
        }
        return true; }}Copy the code
class TinkerResourcePatcher {
    private static final String TAG = "Tinker.ResourcePatcher";
    private static final String TEST_ASSETS_VALUE = "only_use_to_test_tinker_resource.txt";

    // A collection of weak references to Resources saved in ResourcesManager
    private static Collection<WeakReference<Resources>> references = null;
    private static Object currentActivityThread = null;
    // AssetManager for the new resource bundle
    private static AssetManager newAssetManager = null;

    // AssetManager.addAssetPath
    private static Method addAssetPathMethod = null;
    / / AssetManager. AddAssetPathAsSharedLibrary > = android7.0 need to call
    private static Method addAssetPathAsSharedLibraryMethod = null;
    / / AssetManager. MStringBlocks fields
    private static Field stringBlocksField = null;
    // AssetManager.ensureStringBlocks
    private static Method ensureStringBlocksMethod = null;

    // Resources.mAssets
    private static Field assetsFiled = null;
    // Resources.mResourcesImpl
    private static Field resourcesImplFiled = null;
    // LoadedApk.mResDir
    private static Field resDir = null;
    // ActivityThread.mPackages
    private static Field packagesFiled = null;
    // ActivityThread.mResourcePackages
    private static Field resourcePackagesFiled = null;
    // ApplicationInfo.publicSourceDir
    private static Field publicSourceDirField = null;
    
    public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
        // data/data/ package name /tinker/patch-xxx/res/resources.apk
        if (externalResourceFile == null) {
            return;
        }

        final ApplicationInfo appInfo = context.getApplicationInfo();

        final Field[] packagesFields;
        if (Build.VERSION.SDK_INT < 27) {
            packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
        } else {
            packagesFields = new Field[]{packagesFiled};
        }
        // Walk through the LoadedApk in activityThread
        for (Field field : packagesFields) {
            final Object value = field.get(currentActivityThread);

            for(Map.Entry<String, WeakReference<? >> entry : ((Map<String, WeakReference<? >>) value).entrySet()) {final Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                final String resDirPath = (String) resDir.get(loadedApk);
                // Find the LoadedApk instance of the original app and set its mResDir to the new bundle path
                if(appInfo.sourceDir.equals(resDirPath)) { resDir.set(loadedApk, externalResourceFile); }}}/ / newAssetManager for the newly created AssetManager, call AssetManager. AddAssetPath set the path for the new resource bundles
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }

        / / > = android7.0 and used under the condition of the Shared resource also need to call addAssetPathAsSharedLibrary
        if (shouldAddSharedLibraryAssets(appInfo)) {
            for (String sharedLibrary : appInfo.sharedLibraryFiles) {
                if(! sharedLibrary.endsWith(".apk")) {
                    continue;
                }
                if (((Integer) addAssetPathAsSharedLibraryMethod.invoke(newAssetManager, sharedLibrary)) == 0) {
                    throw new IllegalStateException("AssetManager add SharedLibrary Fail"); }}}// Re-create the resource string index
        if(stringBlocksField ! =null&& ensureStringBlocksMethod ! =null) {
            stringBlocksField.set(newAssetManager, null);
            ensureStringBlocksMethod.invoke(newAssetManager);
        }
        // Iterate over Resources in ResourcesManager
        for (WeakReference<Resources> wr : references) {
            final Resources resources = wr.get();
            if (resources == null) {
                continue;
            }
            try {
                // Set resources.massets to the newly created AssetManager
                assetsFiled.set(resources, newAssetManager);
            } catch (Throwable ignore) {
                / / android7.0 after the field for the Resources. The mResourcesImpl. MAssets
                final Object resourceImpl = resourcesImplFiled.get(resources);
                final Field implAssets = findField(resourceImpl, "mAssets");
                implAssets.set(resourceImpl, newAssetManager);
            }
            // Clear the typedArray cache in Resources
            clearPreloadTypedArrayIssue(resources);
            / / internal call AssetManager setConfiguration, refresh the allocation of resources
            resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
        }

        // Handle issues caused by WebView on Android N.
        // Issue: On Android N, if an activity contains a webview, when screen rotates
        // our resource patch may lost effects.
        // for 5.x/6.x, we found Couldn't expand RemoteView for StatusBarNotification Exception
        // after android7.0, if the activity contains a webView, the patch resource will be invalid after the screen is rotated
        if (Build.VERSION.SDK_INT >= 24) {
            try {
                if(publicSourceDirField ! =null) {
                    / / reset ApplicationInfo publicSourceDirpublicSourceDirField.set(context.getApplicationInfo(), externalResourceFile); }}catch (Throwable ignore) {
            }
        }
        // Run the test.dex command to check whether patch resources are loaded successfully
        if(! checkResUpdate(context)) {throw newTinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL); }}}Copy the code

Add a New Activity

Since the new activity is not registered in the manifest, it needs to use hook to bypass the detection of AMS before it can be started.

  1. Pre-register some proxy activities in the original app
  2. Intercept the startActivity process when jumping to the new activity, modify the call parameters through the dynamic proxy, and replace the related parameters of target activity with those of the proxy activity to pass the AMS check
  3. Intercepts where AMS calls the client to actually start the activity, and then changes the proxy activity back to target Activity

Hook related knowledge can be simply understood through this blog, the principle is similar, hook points have a little difference. The general process of Hook in Tinker is as follows:

  1. Parse the patch package inc_Component_meta. TXT, parse the activity node into an ActivityInfo object and save it

  2. Hook ServiceManager, ServiceBinderInterceptor fetchTarget method for AMS the client proxy object (BpBinder)

  3. ServiceBinderInterceptor. Decorate to create this BpBinder dynamic proxy, through AMSInterceptHandlerhook for AMS. Call startActivity, Replace the Target Activity with the tinker Loader’s pre-registered proxy activity

  4. ServiceBinderInterceptor. Will inject ServiceManager. SCache in AMS agent with hooks on step to create a dynamic proxy objects

  5. In the same way, hook SOME methods in PMS to prevent errors in Activity verification

  6. 8.1 the following system through hookActivityThread. MH. MCallBack, before the system call handleLaunchActivity will target the activity to replace back

  7. 8.1 and above system through hookActivityThread mInstrumentation, will replace it as TinkerHackInstrumentation, modify the relevant methods implementation will target the activity to replace back.

Componenthotplug. install starts the hook

ServiceBinderInterceptor and HandlerMessageInterceptor Interceptor implementation class, Interceptor. FetchTarget need hooks for instance, Interceptor. class creates a dynamic proxy for this object. Interceptor.inject replaces the original instance with a dynamic proxy.

public final class ComponentHotplug {
     public synchronized static void install(TinkerApplication app, ShareSecurityCheck checker) throws UnsupportedEnvironmentException {
        if(! sInstalled) {try {
                // Parse inc_COMPONent_meta, parse the XML activity node into an ActivityInfo object
                if (IncrementComponentManager.init(app, checker)) {
                    / / ServiceManager. GetService for AMS client proxy objects, and then create the proxy object dynamic proxy objects, hook method such as startActivity
                    sAMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.ACTIVITY_MANAGER_SRVNAME, new AMSInterceptHandler(app));
                    // Same hook PMS
                    sPMSInterceptor = new ServiceBinderInterceptor(app, EnvConsts.PACKAGE_MANAGER_SRVNAME, new PMSInterceptHandler());
                    sAMSInterceptor.install();
                    sPMSInterceptor.install();
                    if (Build.VERSION.SDK_INT < 27) {
                        // Android 8.1 below
                        // ActivityThread.mH
                        final Handler mH = fetchMHInstance(app);
                        // hook activityThread.mh, replace h.callback with MHMessageHandler
                        sMHMessageInterceptor = new HandlerMessageInterceptor(mH, new MHMessageHandler(app));
                        sMHMessageInterceptor.install();
                    } else {
                        / / > = 8.1 hook ActivityThread mInstrumentation, his replacement for TinkerHackInstrumentation
                        sTinkerHackInstrumentation = TinkerHackInstrumentation.create(app);
                        sTinkerHackInstrumentation.install();
                    }
                    sInstalled = true; }}catch (Throwable thr) {
                uninstall();
                throw newUnsupportedEnvironmentException(thr); }}}}Copy the code

AMSInterceptHandler replace the activity

Intercepts all methods that call AMS to start the activity and replaces target Activity with a proxy

public class AMSInterceptHandler implements BinderInvocationHandler {
    private Object handleStartActivity(Object target, Method method, Object[] args) throws Throwable {
        int intentIdx = -1;
        for (int i = 0; i < args.length; ++i) {
            if (args[i] instanceof Intent) {
                intentIdx = i;
                break; }}if(intentIdx ! = -1) {
            // target activity intent
            final Intent newIntent = new Intent((Intent) args[intentIdx]);
            / / replace the activity
            processActivityIntent(newIntent);
            args[intentIdx] = newIntent;
        }
        return method.invoke(target, args);
    }
    private void processActivityIntent(Intent intent) {
        // The original activity package name and class name
        String origPackageName = null;
        String origClassName = null;
        if(intent.getComponent() ! =null) {
            origPackageName = intent.getComponent().getPackageName();
            origClassName = intent.getComponent().getClassName();
        } else{... }if (IncrementComponentManager.isIncrementActivity(origClassName)) {
            final ActivityInfo origInfo = IncrementComponentManager.queryActivityInfo(origClassName);
            final boolean isTransparent = hasTransparentTheme(origInfo);
            // Get the name of tinker's pre-registered proxy activity class based on the activity information to jump to
            // The Tinker loader manifest pre-registers some activities, such as ActivityStubs$STDStub_00
            final String stubClassName = ActivityStubManager.assignStub(origClassName, origInfo.launchMode, isTransparent);
            // Replace the original activity with the proxy activitystoreAndReplaceOriginalComponentName(intent, origPackageName, origClassName, stubClassName); }}private void storeAndReplaceOriginalComponentName(Intent intent, String origPackageName, String origClassName, String stubClassName) {
        final ComponentName origComponentName = new ComponentName(origPackageName, origClassName);
        // Set the classLoader used to resolve the parcel
        ShareIntentUtil.fixIntentClassLoader(intent, mContext.getClassLoader());
        // Add the original componentName to the intent bundle so that AMS can replace it after the intent has finished processing
        intent.putExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT, origComponentName);
        // Intent sets componentName to represent the activity
        final ComponentName stubComponentName = newComponentName(origPackageName, stubClassName); intent.setComponent(stubComponentName); }}Copy the code

MHMessageHandler reduction activity,

Hook ActivityThread. MH. MCallBack, let it calls to MessageHandler. The handleMessage

public class MHMessageHandler implements MessageHandler {
     @Override
    public boolean handleMessage(Message msg) {
        int what = msg.what;
        if (what == LAUNCH_ACTIVITY) {
            try {
                final Object activityClientRecord = msg.obj;
                if (activityClientRecord == null) {
                    return false;
                }
                / / reflection ActivityClientRecord. Intent startActivity before access to ams intent
                final Field intentField = ShareReflectUtil.findField(activityClientRecord, "intent");
                final Intent maybeHackedIntent = (Intent) intentField.get(activityClientRecord);
                if (maybeHackedIntent == null) {
                    ShareTinkerLog.w(TAG, "cannot fetch intent from message received by mH.");
                    return false;
                }
                ShareIntentUtil.fixIntentClassLoader(maybeHackedIntent, mContext.getClassLoader());
                // Get the original Activity ComponentName
                final ComponentName oldComponent = maybeHackedIntent.getParcelableExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
                if (oldComponent == null) {
                    ShareTinkerLog.w(TAG, "oldComponent was null, start " + maybeHackedIntent.getComponent() + " next.");
                    return false;
                }
                final Field activityInfoField = ShareReflectUtil.findField(activityClientRecord, "activityInfo");
                / / agent activityInfo
                final ActivityInfo aInfo = (ActivityInfo) activityInfoField.get(activityClientRecord);
                if (aInfo == null) {
                    return false;
                }
                / / the original activityInfo
                final ActivityInfo targetAInfo = IncrementComponentManager.queryActivityInfo(oldComponent.getClassName());
                if (targetAInfo == null) {
                    return false;
                }
                // Since the proxy activity does not have screenOrientation information, the target activity information needs to be restored
                fixActivityScreenOrientation(activityClientRecord, targetAInfo.screenOrientation);
                // Copy the target activityInfo field to the proxy activityInfo and replace componentName with target
                fixStubActivityInfo(aInfo, targetAInfo);
                maybeHackedIntent.setComponent(oldComponent);
                maybeHackedIntent.removeExtra(EnvConsts.INTENT_EXTRA_OLD_COMPONENT);
            } catch (Throwable thr) {
                ShareTinkerLog.e(TAG, "exception in handleMessage.", thr); }}return false; }}Copy the code

TinkerHackInstrumentation reduction activity,

TinkerHackInstrumentation inheritance Instrumentation, through reflection will be completed Instrumentation in ActivityThread hook, The logic is similar to that of the AMSInterceptHandler except that the hook is different.

Afterword.

Loading patches this part through a lot of reflection for the framework layer Hack, with the android version changes and the tightening of high version permissions, it will be more and more difficult to adapt, compatibility is difficult to guarantee, for maintainers obstacles and long.

The end of three articles, since the Tinker source analysis of the end, I can be a small harvest, but also hope to help readers.