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 bugsKey.classModify, and then willKey.classPackage as a patch packagePatch.dex.
  • willPatch.dexOn thedexElementsThe first element of the array.
  • According to the parent delegate, it will be found firstPatch.dexIn theKey.classWill load first, and there are bugsKey.classIt won’t be loaded.

When it comes to implementation details, there are some differences between frameworks.

  • Super Patch: Put patch. dexdexElementsThe 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_classessdexElementsThe 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$changeThe member variable that it implementsIncrementalChangeInterface.
  • 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$changenullExecutes the old logic in the method.
  • If the method changes, then:
    • Dynamically generate replacement classesTestActivity$overrideAppPatchesLoaderImplClass.
    • AppPatchesLoaderImplOf the classgetPatchedClassesMethod returns a list of modified classes, based on which,TestActivityIn the$changeIt’s going to be assigned to thetaTestActivity$override.
    • The judgment condition is true,access$dispatch()Method will executeTestActivity$overrideIn the classonCreateMethod, thereby implementing the existingonCreateMethod modification.

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 deliveredPatchesInfoImpl.javaPatch.javapatch.dexTo the client, useDexClassLoaderloadingpatch.dex, reflection getPatchesInfoImpl.javaThis class creates objects.
  • And then pass through the object’sgetPatchedClassesInfoMethod 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 thechangeQuickRedirectField assignment is usedpatch.dexIn thePatch.javaThis classnewThe 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 reflectionaddAssetPathMethod to load an external resource.
  • Put the AssetManager typemAssetsAll 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.
  • callSystem.loadMethod 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 againSystem.loadLoad the so file in that directory.