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:
attachBaseContext
Called when theloadTinker
Method to load the patch,This step replaces the PathClassLoadercreateInlineFence
Create TinkerApplicationInlineFence instance, this kind of used to prevent Art under the effects of inline- 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.
- 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 called
TinkerDexOptimizer.optimizeAll
Dexopt 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 - After the patch is loaded,
Tinker.install
Called when theTinkerLoadResult.parseTinkerResult
Parse 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 - “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.
- Set the mResDir field in the LoadedApk instance of the original app to the new resource pack path
- Create a new AssetManager instance and set its resource path to the new resource bundle path
- 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.
- Pre-register some proxy activities in the original app
- 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
- 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:
-
Parse the patch package inc_Component_meta. TXT, parse the activity node into an ActivityInfo object and save it
-
Hook ServiceManager, ServiceBinderInterceptor fetchTarget method for AMS the client proxy object (BpBinder)
-
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
-
ServiceBinderInterceptor. Will inject ServiceManager. SCache in AMS agent with hooks on step to create a dynamic proxy objects
-
In the same way, hook SOME methods in PMS to prevent errors in Activity verification
-
8.1 the following system through hookActivityThread. MH. MCallBack, before the system call handleLaunchActivity will target the activity to replace back
-
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.