[Android advanced notes] Hot fix (code, resources, dynamic link library)


1. Thermal repair

1.1. effect

  • Serious bugs need to be solved immediately without repackaging and putting on the shelf.
  • Solve the problem that the version upgrade rate is not high, and the Bug will always affect users who do not upgrade the version.
  • Realize short-term version coverage of small functions, such as festival activities.

1.2. Mainstream thermal repair framework

1.2.1. Mainstream framework

factionsframe
Ali systemAndFix⚠️,Dexposed⚠️,HotFix⚠️,Sophix
Tencent DepartmentTinker , super patch, QFix
Well known companyMeituan Robust,Are you hungry, Amigo⚠️,Mushroom street Aceso⚠️
otherRocooFix⚠️,Nuwa⚠️,AnoleFix⚠️

1.2.2. Frame comparison

characteristicSophixTinkerSuper patchRobust
Immediate effectsupportI won't support itI won't support itI won't support it
Method substitutionsupportsupportsupportsupport
Class substitutionsupportsupportsupportI won't support it
Class structure modificationsupportsupportI won't support itI won't support it
Resource replacementsupportsupportsupportI won't support it
so substitutionsupportsupportI won't support itI won't support it
Patch package sizelesslessmorecommonly
Performance losslessmoremoreless
Intrusive packagingNo invasioninvasioninvasioninvasion

2. Code repair

There are three main schemes for code repair: class loading scheme, underlying replacement scheme and Instant Run scheme.

2.1. Class loading scheme

Tinker, super patch, QFix, Amigo, Nuwa, etc. mainly adopt the class loading scheme.

2.1.1. Dex subcontracting mechanism

[method number 65536 limit]

Since the invoke kind index of the method call instruction of the DVM instruction set is 16bits, that is, 65535 methods can be referenced at most. Therefore, when the number of methods in a DEX file exceeds 65535, a compile time exception will be thrown: com android. dex. DexIndexOverflowException: method ID not in [0, 0xffff]: 65536.

[LinearAlloc limit]

LinearAlloc in DVM is a fixed cache. When the number of methods exceeds the size of the cache, an error will be reported. Therefore, install may be prompted when installing the application_ FAILED_ DEXOPT.

[subcontracting scheme]

  • Resolve 65536 and LinearAlloc restrictions.
  • When packaging, the application code is divided into multiple Dex, and the classes that must be used during application startup and the direct reference classes of these classes are placed in the main Dex, and other codes are placed in the secondary Dex.
  • When the application starts, the main Dex is loaded first, and then the secondary Dex is loaded dynamically after the application starts, which alleviates the 65536 limit of the main Dex and the LinearAlloc limit.

2.1.2. Class loading

During Android class loading, an important step is to call the findClass method of DexPathList.

// Each dex file corresponds to an Element element Element and is arranged in order
private Element[] dexElements;

public Class<?> findClass(String name, List<Throwable> suppressed) {
    // Traverse the array of dex files in turn
    for (Element element : dexElements) {
        // element. The loadClassBinaryName method of DexFile will be called inside findclass to find the class
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        // When this class is found, it returns
        if (clazz != null) {
            return clazz;
        }
    }

    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

2.1.3. Repair scheme

Search process according to class:

  • Class key with Bug Class, and then set the key Class is packaged into patch dex.
  • Patch DEX is placed in the first element of the dexElements array.
  • According to the parents' delegation, patch will be found first Key in DEX Class will be loaded first, but there is a Bug key Class will not be loaded.

Specific to the implementation details, different frameworks have some differences.

  • Super patch: patch DEX is placed in the first element of the dexElements array.
  • Tinker: diff the old apk and the new apk to get a patch DEX, and then patch DEX and classes in mobile apk DEX is merged to generate fix_classess.dex, then fix_classess.dex is placed in the first element of the dexElements array.

2.2. Underlying alternative

AndFix, Dexposed, HotFix and Sophix are the main alternatives.

Advantages: it takes effect immediately without restarting the APP.

2.2.1. ArtMethod

Each method of the Java layer corresponds to the structure of an ArtMethod in ART (including all information of the Java method, including execution entry, access permission, belonging class, code execution address, etc.). As long as the structure content of the original method is replaced with the new structure content, when calling the original method, the instruction actually executed is the instruction of the new method, Hot repair can be achieved.

At art/runtime/art_method.h file defines the structure content of ArtMethod.

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_;
}

2.2.2. Repair scheme

  • Method ①: replace each field in the ArtMethod structure corresponding to the java method to be repaired.
  • Method ②: replace the whole ArtMethod structure corresponding to the java method to be repaired.

Different frameworks adopt different schemes:

  • AndFix adopts method ①. Artmethods of different versions and manufacturers may be different. There is a compatibility problem, resulting in the failure of method replacement.
  • Sophix adopts method ②, and there is no compatibility problem.

No matter which scheme is adopted, since the structure and number of methods of the class are fixed after the class is loaded, the scheme has the following discomfort scenarios:

  • Increase or decrease the number of methods and fields.
  • Change the structure of the original class.

Sophix combines the respective advantages of the underlying replacement scheme and the class loading scheme. It focuses on the underlying replacement scheme, supplemented by the class loading scheme. When the hot deployment cannot be used, it will be automatically degraded to cold deployment.

2.3. Instant Run scheme

On Android studio version 2.0, a new feature Instant Run is supported to realize the real-time effect (hot plug) of code modification.

Robust and Aceso are the main users of the Instant Run solution.

2.3.1. Principle of instant run

When building Apk for the first time:

  • A member variable of $change is injected into each class, which implements the IncrementalChange interface.
  • In the first line of each method, a 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);
    }
}

When we click the instant run button in Android Studio:

  • If the method does not change, $change is null and the old logic in the method is executed.
  • If the method changes, then:
    • Dynamically generate replacement classes TestActivity$override and AppPatchesLoaderImpl.
    • The getPatchedClasses method of AppPatchesLoaderImpl class will return the list of modified classes. According to this list, $change in TestActivity will be assigned as TestActivity$override.
    • If the judgment condition is true, the access$dispatch() method will execute the onCreate method in the TestActivity$override class to modify the existing onCreate method.

2.3.2. Repair scheme

with Robust take as an example

  • In the compilation and packaging phase, a piece of code is automatically inserted for each method.
  • Dynamic contracting includes patchesinfoimpl Java and patch Java patch DEX to the client and load the patch with DexClassLoader DEX, get patchesinfoimpl Java this class and create an object.
  • Then, through the getpatchedclasseinfo method of this object, get the confused name of the class to be repaired, and then reflect it to get the class in the current running environment.
  • The changeQuickRedirect field is assigned with patch Patch in DEX Java is an object from class new.

3. Resource restoration

Many resource repair of hot repair frameworks refer to the resource repair principle of Instant Run. Since Instant Run is not the source code of Android, it needs to be decompiled to know.

The core logic of Instant Run resource repair is in the monkeyPatchExistingResources method of MonkeyPatcher class.

public class MonkeyPatcher {
    public static void monkeyPatchExistingResources(
        Context context, String externalResourceFile, Collection<Activity> 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 the mmassets in Resources with newAssetManager
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        /* ... */
                    }
                    // Get the topic of the Activity
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            // Put resources Replace m assets in 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;
                /* ...The weak reference collection of Resources can be obtained in different ways according to different SDK versions */
                for (WeakReference<Resources> wr : references) {
                    Resources resources = wr.get();
                    if (resources != null) {
                        try {
                            // Replace the mmassets in each resource 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 new IllegalStateException(e);
        }
    }
}

Resource hot repair is summarized in two steps:

  • Reflection creates a new AssetManager object, and reflection calls the addAssetPath method to load external resources.
  • Replace all references to the mmassets field of AssetManager type with the newly created AssetManager object.

4. Dynamic link library repair

The dynamic link library of Android is mainly so library.

4.1. Loading of so

Loading so mainly uses the load and loadLibarary methods of the System class.

public final class System {
    // If the so name is passed in, the so 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);
    }
    
    // The absolute path of the incoming so. Load the custom external so file directly from this path
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
    }
}

In fact, these two methods finally call the native load method to load the so library. The parameter fileName is the full pathname of the so library on the disk.

nativeLoad calls the LoadNativeLibrary function to load so:

  • Judge whether the so has been loaded and whether the ClassLoader is the same twice to avoid repeated loading of the so.
  • Open so and get the so handle. If the so handle acquisition fails, false will be returned. Create a new SharedLibrary. If the library corresponding to the incoming path is a null pointer, assign the newly created SharedLibrary to the library and store the library in the libraries_ Yes.
  • Find JNI_OnLoad function pointer. Set was according to different conditions_ The value of successful, and finally return the was_successful.

4.2. Register Native method

4.2.1. Statically register Native methods

The header file containing JNI generated by the javah -jni command, and the naming method of the interface is generally Java_< PackageName>_< ClassName>_< Methodname >, when the program is executed, the system will call the corresponding Native method according to this naming rule.

  • The registration method is convenient and simple.
  • JNI function name is too long, poor readability, and cannot be changed flexibly.

4.2.2. Dynamic registration

Register when loading the function library (. a or. so), that is, in JNI_ Register in the onload method.

  • The registration method is troublesome.
  • JNI free function name.

4.3. Repair scheme

  • Insert the so patch into the front of the nativelibrarielement array so that the path of the so patch is returned and loaded first.
  • Call system Load method to take over the loading entry of so.

Tinker uses system Load method loads custom so file.

  • Using BSdiff algorithm, compare the old and new so files to get the differential packet patch so.
  • Using BSpatch algorithm, patch So is merged with the original so file of the application to generate fix so.
  • Fix Save so to the disk directory, and then use system Load loads the so file in this directory.

Keywords: Android

Added by ValdouaD on Fri, 11 Feb 2022 13:17:27 +0200