1. Hot repair
1.1. Effect of
- Serious bugs that need to be fixed immediately without having to repackage the shelves.
- Solution The version upgrade rate is not high, and bugs may affect users who do not upgrade the version.
- Achieve short – time version coverage of small functions, such as festival activities.
1.2. Mainstream thermal repair framework
1.2.1. Mainstream framework
faction | The framework |
---|---|
Ali is | AndFix⚠ ️,Dexposed⚠ ️,HotFix⚠ ️,Sophix |
Tencent is | Tinker, Super patch, QFix |
Well-known companies | Meituan Robust,Hungry? Amigo⚠ ️,Mushroom street Aceso⚠ ️ |
other | RocooFix⚠ ️,Nuwa⚠ ️,AnoleFix⚠ ️ |
1.2.2. Framework comparison
features | Sophix | Tinker | Super patch | Robust |
---|---|---|---|---|
Effective immediately | support | Does not support | Does not support | Does not support |
Methods to replace | support | support | support | support |
Class to replace | support | support | support | Does not support |
Class structure modification | support | support | Does not support | Does not support |
Resources to replace | support | support | support | Does not support |
So to replace | support | support | Does not support | Does not support |
Patch pack size | smaller | smaller | larger | general |
Performance loss | smaller | larger | larger | smaller |
Invasive packing | No intrusion | intrusion | intrusion | intrusion |
2. Code fixes
There are three schemes for code repair: class loading scheme, low-level replacement scheme and Instant Run scheme.
2.1. Class loading scheme
The main class loading solutions are Tinker, Super Patch, QFix, Amigo and Nuwa.
2.1.1. Dex subcontracting mechanism
【 Method number 65536 limit 】
Since the invoke-kind index of the DVM instruction set is 16bits, it can reference up to 65535 methods. Therefore, when a method in dex file number more than 65535, exception will be thrown compile-time: com. Android. Dex. DexIndexOverflowException: method ID not in [0, 0 XFFFF] : 65536.
LinearAlloc restriction
The LinearAlloc in DVM is a fixed cache and an error is reported when the number of methods exceeds the size of the cache. Therefore, INSTALL_FAILED_DEXOPT may be prompted during application installation.
[Subcontracting scheme]
- Resolve 65536 constraints and LinearAlloc constraints.
- When packaging, the application code is divided into multiple Dex, and the classes that must be used for application startup and their direct reference classes are placed in the primary Dex, and the other code is placed in the secondary Dex.
- When the application starts, the primary Dex is loaded first, and then the secondary Dex is dynamically loaded after the application starts, thus alleviating the 65536 limitation of the primary Dex and the LinearAlloc limitation.
2.1.2. Class loading
An important step in the Android class loading process is to call DexPathList’s findClass method.
// For each dex file, there is an Element, arranged in order
private Element[] dexElements;
publicClass<? > findClass(String name, List<Throwable> suppressed) {// Iterate through the dex file array
for (Element element : dexElements) {
// Element. findClass internally calls DexFile's loadClassBinaryName method to find the classClass<? > clazz = element.findClass(name, definingContext, suppressed);// When the class is found, return
if(clazz ! =null) {
returnclazz; }}if(dexElementsSuppressedExceptions ! =null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Copy the code
2.1.3. Repair plan
According to the search process of the class:
- Classes that will have bugs
Key.class
Modify, and then willKey.class
Package as a patch packagePatch.dex
. - will
Patch.dex
On thedexElements
The first element of the array. - According to the parent delegate, it will be found first
Patch.dex
In theKey.class
Will load first, and there are bugsKey.class
It won’t be loaded.
When it comes to implementation details, there are some differences between frameworks.
- Super Patch: Put patch. dex
dexElements
The first element of the array. - Tinker: Diff the old and new APK to get a patch.dex, merge patch.dex with class. dex in the mobile APK to generate fix_classess.dex, and put fix_classess
dexElements
The first element of the array.
2.2. Low-level replacement scheme
The main underlying alternatives are AndFix, Dexposed, HotFix and Sophix.
Advantages: It takes effect immediately without restarting the APP.
2.2.1. ArtMethod
Each method in the Java layer corresponds to an ArtMethod structure in ART (containing all the information of the Java method, including the execution entry, access permission, owning class and code execution address, etc.). As long as the structure content of the original method is replaced by the new structure content, when the original method is called, The instructions that are actually executed are the instructions of the new method, and the hot repair can be implemented.
In the art/runtime/art_method.h file, the structure content of ArtMethod is defined.
class ArtMethod FINAL {
/ *... * /
GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
uint16_t imt_index_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;
void* data_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
}
Copy the code
2.2.2. Repair plan
- Method 1: Replace each field in the ArtMethod structure corresponding to the Java method to be repaired.
- Method 2: Replace the ArtMethod structure corresponding to the Java method to be repaired.
Different frameworks use different solutions:
- AndFix uses method ①, different versions and different manufacturers may be different ArtMethod, there are compatibility problems, resulting in method replacement failure.
- The Sophix uses method ② and does not have compatibility problems.
In either case, since the class structure and number of methods are fixed once the class is loaded, this scenario has the following uncomfortable scenarios:
- Increase or decrease the number of methods and fields.
- Change the structure of the existing class.
The Sophix combines the best of both low-level replacement and classloading solutions, with low-level replacement as the primary and classloading solutions as the secondary, and automatically demoted to cold deployment when hot deployment is not available.
2.3. Instant Run scheme
Instant Run, a new feature supported in Android Studio 2.0, enables real-time (hot-swappable) implementation of code changes.
Robust and Aceso are the main ones who adopt the Instant Run scheme.
2.3.1. Principle of Instant Run
When you first build Apk:
- One is injected into each class
$change
The member variable that it implementsIncrementalChange
Interface. - In the first line of each method, a section of judgment execution logic is inserted.
public class TestActivity {
// Inject a member of type IncrementalChange
IncrementalChange localIncrementalChange = $change;
public void onCreate(Bundle savedInstanceState){
// When localIncrementalChange is not null, access$dispatch may be executed to replace the old logic
if(localIncrementalChange ! =null) {
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;) V".new Object[] { this, paramBundle });
return;
}
super.onCreate(savedInstanceState); }}Copy the code
When we hit Android Studio’s InstantRun button:
- If the method does not change, then
$change
为null
Executes the old logic in the method. - If the method changes, then:
- Dynamically generate replacement classes
TestActivity$override
和AppPatchesLoaderImpl
Class. AppPatchesLoaderImpl
Of the classgetPatchedClasses
Method returns a list of modified classes, based on which,TestActivity
In the$change
It’s going to be assigned to thetaTestActivity$override
.- The judgment condition is true,
access$dispatch()
Method will executeTestActivity$override
In the classonCreate
Method, thereby implementing the existingonCreate
Method modification.
- Dynamically generate replacement classes
2.3.2. Repair plan
Robust, for example
- A section of code is automatically inserted for each method during the compile packaging phase.
- This parameter is dynamically delivered
PatchesInfoImpl.java
和Patch.java
的patch.dex
To the client, useDexClassLoader
loadingpatch.dex
, reflection getPatchesInfoImpl.java
This class creates objects. - And then pass through the object’s
getPatchedClassesInfo
Method to get the obfuscated name of the class you want to fix, and then reflect it back to the class in the current runtime environment. - One of the
changeQuickRedirect
Field assignment is usedpatch.dex
In thePatch.java
This classnew
The object that comes out.
3. Restore resources
Many hot repair frameworks use the principles of Instant Run for resource repair. Since Instant Run is not Android source, you need to decompile to know.
Instant Run resources repair core logic in MonkeyPatcher monkeyPatchExistingResources method of a class.
public class MonkeyPatcher {
public static void monkeyPatchExistingResources( Context context, String externalResourceFile, Collection
activities)
{
if (externalResourceFile == null) {
return;
}
try {
Reflection creates a new AssetManager
AssetManager newAssetManager = AssetManager.class.getConstructor(
new Class[0]).newInstance(new Object[0]);
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath".new Class[]{String.class});
mAddAssetPath.setAccessible(true);
Reflection calls the addAssetPath method to load external resources
if (((Integer) mAddAssetPath.invoke(
newAssetManager, new Object[]{externalResourceFile})).intValue() == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
"ensureStringBlocks".new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
if(activities ! =null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
// Replace mAssets in Resources with newAssetManager
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
/ *... * /
}
// Get the Activity's theme
Resources.Theme theme = activity.getTheme();
try {
try {
// Replace mAssets in resources. Theme with newAssetManager
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
/ *... * /
}
/ *... * /
} catch (Throwable e) {
/ *... * /
}
}
Collection<WeakReference<Resources>> references = null;
/ *... Get the collection of weak references */ for Resources in different ways, depending on the SDK version
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if(resources ! =null) {
try {
// Replace mAssets in each Resources with newAssetManager
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
/ *... * /} resources.updateConfiguration( resources.getConfiguration(), resources.getDisplayMetrics()); }}}}catch (Throwable e) {
throw newIllegalStateException(e); }}}Copy the code
Resource hotfix can be summarized in two steps:
- Reflection creates a new AssetManager object and is called by reflection
addAssetPath
Method to load an external resource. - Put the AssetManager type
mAssets
All references to the fields are replaced with the newly created AssetManager object.
4. Dynamic link library repair
Android’s dynamic link library is mainly the so library.
4.1 loading of so
So is loaded primarily with the Load and loadLibarary methods of the System class.
public final class System {
// If you pass in the name of so, the file will be loaded directly from the system directory.
// The system path includes /data/data/${package_name}/lib, /system/lib, /vender/lib, etc
public static void load(String filename) {
Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
// Pass in the absolute path of so from which to load custom external so files directly
public static void loadLibrary(String libname) { Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname); }}Copy the code
In fact, both methods finally call the native method nativeLoad to load so library. FileName is the full path name of so library on disk.
NativeLoad calls LoadNativeLibrary to load so:
- Check whether so has been loaded and whether the two classloaders are the same to avoid repeated loading of SO.
- Open so and get the so handle, or return false if the so handle fails to get. Create a new SharedLibrary. If you pass a null pointer to path library, assign the newly created SharedLibrary to library and store the library in Libraries_.
- Look for the function pointer to JNI_OnLoad, set was_successful as the case may be, and return was_successful.
4.2. Register Native methods
4.2.1. Static registration of Native methods
Javah-jni header files containing JNI are generated by the javah-jni command. The interface is generally named Java_
_
. When the program is executed, the system will call the corresponding Native methods according to this naming rule.
- Registration is convenient and simple.
- JNI function names are too long and unreadable to change flexibly.
4.2.2. Dynamic registration
This is done when the library (.a or.so) is loaded, in the JNI_OnLoad method.
- Registration is troublesome.
- JNI function names can be freely named.
4.3. Repair plan
- Insert the so patch at the front of the NativeLibraryElement array so that the path of the so patch is returned and loaded first.
- call
System.load
Method to take over the load entry of so.
Tinker uses the system. load method to load custom.so files.
- BSdiff algorithm was used to compare the old and new SO files to obtain the subcontract patch.so.
- Using BSpatch algorithm, patch.so is merged with the original so file of the application to generate fix.so.
- Save fix.so to your disk directory and use it again
System.load
Load the so file in that directory.