preface
The understanding of Tinker’s principle has remained at the extensive cognitive level, but for the details of the principle of code repair, the principle of resource repair, the algorithm principle of DEX subcontract have not personally read the source code, so Tinker will be divided into two pieces of source code interpretation.
Tinker
Principles of code Repair
The patch takes effect after our application is restarted. We can start with TinkerApplication and look at the source code
private void onBaseContextAttached(Context base) {
try{... loadTinker(); mCurrentClassLoader = base.getClassLoader(); mInlineFence = createInlineFence(this, tinkerFlags, delegateClassName,
tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
tinkerResultIntent);
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
Tinker calls onBaseContextAttached when the Application starts at attachBaseContext, LoadTinker is used to load loaderClassName through the class loader of The TinkerApplication. If the developer does not customize it, the class loaded here is the TinkerLoader. Then call his tryLoad method
// TinkerApplication.java
private void loadTinker(a) {
try {
//reflect tinker loader, because loaderClass may be define by user!Class<? > tinkerLoadClass = Class.forName(loaderClassName,false, TinkerApplication.class.getClassLoader()); Method 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
// TinkerLoader.java
public Intent tryLoad(TinkerApplication app) {
Log.d(TAG, "tryLoad test test");
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
Copy the code
// TinkerLoader.java
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
// Check some code before patch loading.final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);
// Whether huawei Hongmeng OS is used
final boolean isArkHotRuning = ShareTinkerInternals.isArkHotRuning();
// Resource, dex, so check.// Only work for art platform Oat, because of interpret, refuse 4.4 art Oat
//android o use quicken default, we don't need to use interpret mode
boolean isSystemOTA = ShareTinkerInternals.isVmArt()
&& ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint)
&& Build.VERSION.SDK_INT >= 21 && !ShareTinkerInternals.isAfterAndroidO();
//now we can load patch jar
if(! isArkHotRuning && isEnabledForDex) {// Code patch loaded
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);
if (isSystemOTA) {
// update fingerprint after load success
patchInfo.fingerPrint = Build.FINGERPRINT;
patchInfo.oatDir = loadTinkerJars ? ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH : ShareConstants.DEFAULT_DEX_OPTIMIZE_PATH;
// reset to false
oatModeChanged = false;
if(! SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) { ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL); Log.w(TAG,"tryLoadPatchFiles:onReWritePatchInfoCorrupted");
return;
}
// update oat dir
resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_OAT_DIR, patchInfo.oatDir);
}
if(! loadTinkerJars) { Log.w(TAG,"tryLoadPatchFiles:onPatchLoadDexesFail");
return; }}// Ignore the adaptive code.//now we can load patch resource
if (isEnabledForResource) {
// Resource patch loading
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, patchVersionDirectory, resultIntent);
if(! loadTinkerResources) { Log.w(TAG,"tryLoadPatchFiles:onPatchLoadResourcesFail");
return; }}// Init component hotplug support.
if ((isEnabledForDex || isEnabledForArkHot) && isEnabledForResource) {
ComponentHotplug.install(app, securityCheck);
}
// Omit some code
// This includes killing tinker related processes outside the main process
// Notification of successful callback loading.return;
}
Copy the code
TinkerLoader# tryLoadPatchFilesInternal mainly do the following things:
- Verification of Tinker functions (including whether Tinker is open, obtaining and verifying manifest files)
- The current execution path must be in the main process
- Verify patch files, patch contents (including dex, Resource, and SO) with lists, and store related information objects in corresponding list objects
- Code patch load (TinkerDexLoader loadTinkerJars)
- Resources patch load (TinkerResourceLoader loadTinkerResources)
- Kill processes other than the main process
Based on a lot of code, not listed here, in TinkerDexLoader. LoadTinkerJars if tinkerLoadVerifyFlag set up mainly for, will make some md5 check security, and then the OAT against doing some patches of optimization, Then through SystemClassLoaderAdder. InstallDexes installation patch work
// SystemClassLoaderAdder.java
public static void installDexes(Application application, ClassLoader loader, File dexOptDir, List<File> files, boolean isProtectedApp)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if(! files.isEmpty()) {// Sort by dex
files = createSortedAdditionalPathEntries(files);
// Get the Class loader of TinkerDexLoader. Since there is no specific class loader to handle, the PathClassloade under DVM should be used
ClassLoader classLoader = loader;
// The APK and SDK of the unhardened application are greater than or equal to 24
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
classLoader = NewClassLoaderInjector.inject(application, loader, dexOptDir, files);
} else {
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else{ V4.install(classLoader, files, dexOptDir); }}//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if(! checkDexInstall(classLoader)) {//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw newTinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL); }}}Copy the code
V23. Install (classLoader, files, dexOptDir)
// V23.java
private static void install(ClassLoader loader, List
additionalClassPathEntries, File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throwe; }}}Copy the code
As you can see, Tinker finally replaces the dexElements collection object in the DexPathList object by calling ‘DexPathList#makeDexElements’ in the hook classloader. The Dex patch is loaded successfully.
Tinker code patch loading code process we have gone through, back to see why the principle of code patch repair, first we need to understand the class loading mechanism and Android class loaders.
Class loading mechanism and The Android class loader
We all know that Android’s class loading mechanism follows the JVM’s parent delegation model, but what is the parent delegation model?
The parent delegate model requires that all class loaders have their own parent class loaders, except for the top-level start class loaders. When a classloader receives a classload request, it first does not attempt to load the class itself. Instead, it delegates the request to the parent classloader. This is true at every level of classloaders, so all load requests should eventually be sent to the top level of the starting classloader. Only when the parent loader reports that it cannot complete the load request (it did not find the required class in its search scope) will the child loader attempt to complete the load itself.
To see if the parent delegate mechanism is still used in DVM, take a look at the code
// /libcore/ojluni/src/main/java/java/lang/ClassLoader.java
protectedClass<? > loadClass(String name,boolean resolve)
throws ClassNotFoundException
{
// Check if the corresponding class has already been loaded
// First, check if the class has already been loadedClass<? > c = findLoadedClass(name);if (c == null) {
try {
if(parent ! =null) {
c = parent.loadClass(name, false);
} else{ c = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// If the parent class loader throws, the parent class loader cannot complete the load request
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// When the parent class loader cannot be loaded
// Call its own findClass method to loadc = findClass(name); }}return c;
}
Copy the code
When a class is loaded, the loaded class is used; otherwise, the loadClass of the parent loader is called; if the parent loader is empty, the startup class loader is used as the parent loader. If the parent class loader fails to load, Call your own findClass method to load.
The Android classloader differs from the JVM classloader in that it loads a Dex instead of a Class file. Let’s take a look at the inheritance of the Android classloader under 8.0
PathClassLoader
// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null.null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent); }}Copy the code
The first entry to the constructor, dexPath, is a set of dex related file paths separated by “:”. LibrarySearchPath represents a set of so file paths. Delimited by a file delimiter, possibly null. It inherits from BaseDexClassLoader, and the specific method is implemented by the parent class. Part of the code is captured below, and the specific source code can be seen here
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
@Override
protectedClass<? > findClass(String name)throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// Use DexPathList to find classes
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
@Override public String toString(a) {
return getClass().getName() + "[" + pathList + "]"; }}Copy the code
According to the source code, it can be seen that when BaseDexClassLoader is created, it maintains a DexPathList object internally. When searching for classes, resources, Dex or So, it obtains them indirectly through DexPathList
// /libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
final class DexPathList {
private Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {... ArrayList<IOException> suppressedExceptions =new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); .// omit other code
}
* Makes an array of dex/resource path elements, one per element of * the given array. */
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions, ClassLoader loader) {
Element[] elements = new Element[files.size()];
int elementsPos = 0;
/* * Open all files and load the (direct or contained) dex files up front. */
for (File file : files) {
if (file.isDirectory()) {
// We support directories for looking up resources. Looking up resources in
// directories is useful for running libcore tests.
elements[elementsPos++] = new Element(file);
} else if (file.isFile()) {
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
if(dex ! =null) {
elements[elementsPos++] = new Element(dex, null); }}catch (IOException suppressed) {
System.logE("Unable to load dex file: "+ file, suppressed); suppressedExceptions.add(suppressed); }}else {
DexFile dex = null;
try {
dex = loadDexFile(file, optimizedDirectory, loader, elements);
} catch (IOException suppressed) {
/* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */
suppressedExceptions.add(suppressed);
}
if (dex == null) {
elements[elementsPos++] = new Element(file);
} else {
elements[elementsPos++] = newElement(dex, file); }}}else {
System.logW("ClassLoader referenced unknown path: "+ file); }}if(elementsPos ! = elements.length) { elements = Arrays.copyOf(elements, elementsPos); }return elements;
}
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file, loader, elements);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); }}/**
* Finds the named class in one of the dex files pointed at by
* this instance. This will find the one in the earliest listed
* path element. If the class is found but has not yet been
* defined, then this method will define it in the defining
* context that this instance was constructed with.
*
* @param name of class to find
* @param suppressed exceptions encountered whilst finding the class
* @return the named class or {@code null} if the class is not
* found in any of the dex files
*/
publicClass<? > findClass(String name, List<Throwable> suppressed) {for(Element element : dexElements) { Class<? > clazz = element.findClass(name, definingContext, suppressed);if(clazz ! =null) {
returnclazz; }}if(dexElementsSuppressedExceptions ! =null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null; }}Copy the code
When a PathClassLoader is looking for a class, it internally iterates through the dexElements array using DexPathList#findClass. It actually finds the class using Element#findClass and returns immediately if it finds it. For the dexElements array object, makeDexElements is used to construct the corresponding array when DexPathList is initialized. Element is the static inner class of DexPathList. Its findClass method eventually calls the DexFile#loadClassBinaryName method
/*package*/ static class Element {
/** * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory * (only when dexFile is null). */
private final File path;
private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;
private boolean initialized;
/** * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath * should be null), or a jar (in which case dexZipPath should denote the zip file). */
public Element(DexFile dexFile, File dexZipPath) {
this.dexFile = dexFile;
this.path = dexZipPath;
}
public Element(DexFile dexFile) {
this.dexFile = dexFile;
this.path = null;
}
public Element(File path) {
this.path = path;
this.dexFile = null;
}
publicClass<? > findClass(String name, ClassLoader definingContext, List<Throwable> suppressed) {returndexFile ! =null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null; }}Copy the code
This gives us a rough idea of how Tinker fixes work:
- According to the class loading mechanism, when a class is loaded, the system checks whether the requested class is loaded again. If so, the system returns the request directly
- Tinker can ensure that the fix takes effect as long as the dex containing the fix code is loaded before the original dex
- Because Tinker sorts the dex, the new dex comes first, and the self-generated testDex comes last
- Hook access
BaseDexClassLoader
The inside of thepathList
Object, throughDexPathList
themakeDexElements
Method to reset the interiorelements
Array, so thatClassLoader
In the callfindClass
You can find the patch related class first, so that orginal class will not be loaded later
Tinker
Resources to repair
Resource loading and obtaining process
Before we look at the Tinker resource repair principle, we need to understand the resource acquisition and loading principle. When we call getResources, we are actually calling getResources of the internal mBase. It is known that the Activity inherits from ContextWrapper and maintains a ContextImpl mBase object inside. So we’re going to focus on the ContextImpl#getResource method
ContextImpl#getResource
mResources
ContextImpl#createResources
ResourceManager
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
final String[] splitResDirs;
final ClassLoader classLoader;
try {
splitResDirs = pi.getSplitPaths(splitName);
classLoader = pi.getSplitClassLoader(splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
return ResourcesManager.getInstance().getResources(activityToken,
pi.getResDir(),
splitResDirs,
pi.getOverlayDirs(),
pi.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfig,
compatInfo,
classLoader);
}
Copy the code
ResourceManager is a singleton. It maintains the ResourcesImpl cache set with ResourcesKey as the key. When getResources is called, the resourceImpl in the match cache is first used. In the case of an unmatched ResourceImpl object, a ResourceImpl object is created, which is a concrete implementation of Resource
public @Nullable Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
final ResourcesKey key = newResourcesKey( resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfig ! =null ? new Configuration(overrideConfig) : null.// CopycompatInfo); classLoader = classLoader ! =null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
}
Copy the code
We can look at the code for creating a Resource
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
/ / omit through getOrCreateResourcesForActivityLocked/getOrCreateResourcesLocked lookup cache the code logic.// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
// If no corresponding cache exists, create a ResourcesImpl object
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
synchronized (this) {
ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
if(existingResourcesImpl ! =null) {
resourcesImpl.getAssets().close();
resourcesImpl = existingResourcesImpl;
} else {
// Add this ResourcesImpl to the cache.
// Cache update
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
}
final Resources resources;
if(activityToken ! =null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
returnresources; }}Copy the code
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
/ / create AssertManager
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
// Create a new ResourcesImpl object and hold the AssetManager object reference
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
Copy the code
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
AssetManager assets = new AssetManager();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if(key.mResDir ! =null) {
if (assets.addAssetPath(key.mResDir) == 0) {
Log.e(TAG, "failed to add asset path " + key.mResDir);
return null; }}if(key.mSplitResDirs ! =null) {
for (final String splitResDir : key.mSplitResDirs) {
if (assets.addAssetPath(splitResDir) == 0) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null; }}}if(key.mOverlayDirs ! =null) {
for (finalString idmapPath : key.mOverlayDirs) { assets.addOverlayPath(idmapPath); }}if(key.mLibDirs ! =null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources."); }}}}return assets;
}
Copy the code
The addAssetPath method is used to add the path maintenance for the Resource. The corresponding instance is held by the newly created ResourceImpl object. Mresourcesimpl.getassets ().getResourcetext (id) Access to resources is ultimately associated with the AssertManager, as shown in the diagram below
So if we do Resource repair, should be the need to maintain Resources cache ResourceManager singleton processing, so that the corresponding creation of Resource, You can use AssertManager#addAssetPath to add a new resource path to restore the resource. Here’s how Tinker does it.
Tinker
Resources to repair
In the previous section, we learned about the resource repair related entry code in TinkerResourceLoader#loadTinkerResources
public static boolean loadTinkerResources(TinkerApplication application, String directory, Intent intentResult) {
if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
return true;
}
String resourceString = directory + "/" + RESOURCE_PATH + "/" + RESOURCE_FILE;
File resourceFile = new File(resourceString);
// Omit the security check code.// Remove catch code related to patch uninstallation in case of loading failure
TinkerResourcePatcher.monkeyPatchExistingResources(application, resourceString);
// Ignore the monitoring code tiker loads for runtime resources.return true;
}
Copy the code
Then the core code of resource repair, continue to see
public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
final ApplicationInfo appInfo = context.getApplicationInfo();
final Field[] packagesFields;
PackagesFiled is an mPackages object in ActivityThread. Its type is ArrayMap. Key is the package name, and value is LoadedApk
// resourcePackagesFiled is an mResourcePackages object in ActivityThread. Its type is ArrayMap. Key is the package name, and value is LoadedApk
if (Build.VERSION.SDK_INT < 27) {
packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
} else {
packagesFields = new Field[]{packagesFiled};
}
// Walk through packagesFields to get all loadedapKs
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;
}
// resDir is the mResDir object in LoadedApk, that is, the resource file path
final String resDirPath = (String) resDir.get(loadedApk);
if (appInfo.sourceDir.equals(resDirPath)) {
// Set resDir to patch resource file path by hookresDir.set(loadedApk, externalResourceFile); }}}// Create a new AssetManager instance and point it to the resources installed under
// Create a new assetManager object and add the patch resource path by calling the addAssetPath method through reflection
if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
if(stringBlocksField ! =null&& ensureStringBlocksMethod ! =null) {
stringBlocksField.set(newAssetManager, null);
ensureStringBlocksMethod.invoke(newAssetManager);
}
// Traverses the Resource cache set in ResourceManager
// perform traversal
for (WeakReference<Resources> wr : references) {
final Resources resources = wr.get();
if (resources == null) {
continue;
}
// Set the AssetManager of the Resources instance to our brand new one
// Hook the mAssets object in the resourceImpl to the newly created assertManager above
try {
//pre-N
// assetsFiled is the mAssets in the resourceImpl
// Replace the assertManager object in resourceImpl with our newly created object
assetsFiled.set(resources, newAssetManager);
} catch (Throwable ignore) {
// N
final Object resourceImpl = resourcesImplFiled.get(resources);
// for Huawei HwResourcesImpl
final Field implAssets = findField(resourceImpl, "mAssets");
implAssets.set(resourceImpl, newAssetManager);
}
// Resources has an internal cache pool for TypedArrays that needs to be cleared in case you get the same old resource
clearPreloadTypedArrayIssue(resources);
// Update resources
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
// Ignore the webView adaptation on Android N.// Open the only_use_to_test_tinker_resource. TXT tinker internal test resource file to verify whether the resource is successfully loaded
if(! checkResUpdate(context)) {throw newTinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL); }}Copy the code
It can be seen that Tinker’s resource repair steps are as follows:
- Hook to obtain apK description file loadedApk, hook to set the internal maintenance mRes as the resource patch path
- Create an AssetManager and pass
AssetManager#addAssetPath
Add a resource patch path, use Hook to obtain resources cache in ResourceManager, and set mAsset held in ResourceImpl to the newly created AssetManager - Clear the TypedArrays cache pool and update the resource
reference
- Android Advanced Decryption
- In-depth Understanding of the JVM Third Edition
- Tinker Github Wiki
- Android hotfix Tinker access and source code analysis
- Android resource loading mechanism analysis
- Plug-in resource processing