preface

If you want to give a rough definition of what a hot fix is, in a nutshell, what a hot fix does is change the code of an existing application without reinstalling it in order to fix a bug. In the long evolution of hot repair, many implementation methods have appeared, such as replacing ArtMethod (representing framework: Andfix) in native layer, using ClassLoader to load patch (representing framework: Tinker), a hybrid of multiple solutions (representing the framework Sophix, which incorporates the strengths and weaknesses of different solutions), and AOP+ automation patches (representing the framework: Instant Run, Robust), this paper mainly in-depth explanation of reference Instant Run design AOP hot repair scheme, other implementation methods, will be later mentioned, and class Instant Run and these schemes will be compared, from multiple perspectives to better analyze the advantages and disadvantages of different solutions.

Project introduction

Before diving into the details of the Instant Run scenario, here are the benefits: 1. Support for runtime fixes, 2. No need to hook (not compatible with Android version and ROM). When we think about hot fix, we find that it is a lightweight dynamic representation. There are many ways to achieve this dynamic representation. For example, in the API provided by Java, the easiest way to dynamically load code is through the ClassLoader to load the class files conforming to the virtual Machine (JVM, AVM) specification. Therefore, the Classloader solution came into being. This solution will patch the modified class file and load it on the terminal that will be repaired by Classloader. In this way, it is equivalent to class granularity replacement, such as annotations and field modification can be supported. However, due to the limitation of VMS, classes with the same class name cannot be loaded repeatedly by the same ClassLoader, and classes that have been loaded to VMS are subject to stringent uninstallation conditions. Therefore, the hot repair solution using ClassLoader can take effect only after being restarted. So what if we take a step back? As a matter of fact, many scenarios that require hot fixes do not change annotations or fields. Most of the scenarios that require hot fixes are method logic. If you only load methods, you can extract them into the newly generated class without loading the original class. This way you can load new classes without restarting the application. Instant Run is a runtime fix that uses this approach. Of course, loading a new class alone cannot do hot repair at all, because the original class cannot be replaced, so it is necessary to establish a mapping relationship and jump patch logic according to the patch state. Next, according to the content described above, the principle of the thermal repair solution designed by Instant Run is further referred to step by step, and the design ideas are analyzed from the perspective of practical application.

How do I run the patch?

Start with a simple example

Suppose we have a ready patch, and the only application ready for hot fixes is the following class, which has only one method in it

public class HotfixDemo { public String method() { return "bug"; }}Copy the code

If you want to modify this code so that it executes the original logic when no patches are loaded and the logic within the patch is executed when patches are in place, you can make the following changes at compile time using the bytecode tool

public class HotfixDemo {
    public String method() {
        if (Hotfix.needPatch()) {
            return (String)Hotfix.invokePatch();
        } else {
            return "bug";
        }
    }
}

public class Hotfix {
    public static boolean needPatch() {
        //has patch
    }

    public static Object invokePatch() {
        //invoke patch
    }
}
Copy the code

Add a layer of judgment to the original logic of the method. If a patch needs to be applied, the logic provided by the patch pack is executed via Hotfix, otherwise the original logic is executed, thus making the processed method simple and dynamic. Now that the simplest case is out of the way, add some additional methods to the HotfixDemo class and cover all the real methods.

Drill down from extended use cases

In general, not all methods when using hot repair will be repaired, so to judge patch repair range, if you want to use a marker to select method, so the method signature is very appropriate, conform to the virtual machine specification class files can ensure a method’s signature is the only, then increase the method signature widening “logic:

public class HotfixDemo { public String method() { if (Hotfix.needPatch("method()Ljava/lang/String;" )) { return (String)Hotfix.invokePatch("method()Ljava/lang/String;" ); } else { return "bug"; } } // etc... } public class Hotfix { public static boolean needPatch(String identity) { // identity method need patch } public static  String invokePatch(String identity) { // invoke identity method patch } }Copy the code

Ok, now that the logic for method distribution in a single class is basically done, let’s expand the scope again. Now we have more than HotifxDemo in our application, we need new fields to distinguish between different classes. Following the flow above, it would be natural to add an additional parameter to needPatch() and invokePatch(), distinguished by the class fully qualified name, as shown below

public class HotfixDemo { public String method() { if (Hotfix.needPatch(HotfixDemo.class.getName(), "method()Ljava/lang/String;" )) { return (String)Hotfix.invokePatch(HotfixDemo.class.getName(), "method()Ljava/lang/String;" ); } else { return "bug"; } } } public class Hotfix { public static boolean needPatch(String className, String identity) { MethodPatchInfo methodInfo = classesPatchInfo.get(className); if (methodInfo == null) { return false; } return methodInfo.needPatch(identity); } public static String invokePatch(String className, String identity) { //invoke identity method patch } }Copy the code

At this point, we first check whether the class of the method is in the fix list, and then check whether the method needs to be fixed. In this implementation, generally hot fix frame will be to maintain a class name and the corresponding class hot repair information table, the table at the time of loaded patches for data populated, if apply the scheme to apply above all of the method, it can make each method will be an additional step from mapping table (classPatchInfo) query operation, and, Considering the concurrency of multiple threads, locking operations on the map (updating/uninstalling patches will change the map) can be very expensive for methods that have few logical instructions. So is there a way to optimize? Since a Class can also be a container itself, why not just put MethodPatchInfo in a Class instead of a mapping table that maintains the mapping between classes and MethodPatchInfo?

public class HotfixDemo { public static MethodPatchInfo sInfo; public String method() { MethodPatchInfo info = sInfo; // Local variable assignment if (info! = null && info.needPatch("method()Ljava/lang/String;" )) { return (String)info.invokePatch("method()Ljava/lang/String;" ); } else { return "bug"; }}}Copy the code

Add a static field for each class, assign a value to sInfo when the patch is installed, and when the method is executed, just check whether sInfo is empty to know whether the method in the current class should perform the patch logic. In this way, each class maintains the distribution logic of its own repair methods, trading a negligible amount of space for time without the need for additional retrieval operations at all. Pay extra attention to the local variable assignment in line 5, which is to keep the reference to sInfo intact in the scope of the method in case the needPatch() or invokePatch() method has an NPE after another thread unloads the patch.

How to make patches

In the previous article, we analyzed how to modify the app code through injection so that the app can support dynamic development while being transparent to the development. Then, there are at least three more issues that need to be addressed in order to complete the hot fix process

  1. How to locate the repair area
  2. How to correctly forward the execution of the APP method to the corresponding method in the patch during actual execution
  3. What is the form of logic for the patching method

Location repair range

Since there is no way to know what the future fixes will be when the app goes live, the scope of the fixes must be carried in the patch file. Positioning to a specific method of repair, in addition to the signature must be the fully qualified name, method, must also have a new patch fully qualified name of a class, the class fully qualified name and method signatures can be get at run time through injection parameters, so only maintain a restoration of the fully qualified class name and patch fully qualified name of a mapping relation. We can put the mapping table that maintains the mapping into a Class (or a normal file), as follows

public class PatchMetaInfo { public Map<String, String> loadPatchDeliverInfo() { HashMap<String, String> result = new HashMap<>(); result.put("com.test.HotfixDemo", "com.test.HotfixDemo$Patch"); return result; }}Copy the code

We can have the **$Patch class inherit MethodPatchInfo and implement the specific needPatch() and invokePatch() logic. Once we get the map, we can create instances of **$Patch by reflection and inject each key into the corresponding class.

com.test.HotfxiDemo.sInfo = ReflectHelper.newInstance("com.test.HotfixDemo$Patch");
Copy the code

The next time the fixed method is executed, sInfo is not empty and the method provided by sInfo can be executed directly. Next, the logical execution comes into the implementation of MethodPatchInfo.

Patch logic call

NeedPathch (), as the name implies, is the method we use to determine whether the currently executing method is in the fix list. The fix list is a collection of the method signatures of all the methods being changed, which are passed as arguments in the injected calling code. This method can also be written at patch time

public class HotfixDemo$Patch {
    private HashSet<String> mHotfixRangeSet = new HashSet<>();

    public HotfixDemo$Patch() {
        mHotfixRangeSet.add("method()Ljava/lang/String;")
    }

    public boolean needPatch(String methodSignature) {
        return mHotfixRangeSet.contains(methodSignature);
    }
}
Copy the code

If the execution determines that the fix is within the scope, then you need to invoke the invokePatch() method to execute the logic of the specific patch method. The invokePatch() method can be regarded as a fixed invokePatch method without hot repair logic, so its return value and parameters must match the original method. Of course, this match does not necessarily mean that the original method, we can use Object[] to accept various parameter lists, and replace all return values with Object. The method declaration looks something like this: Object invokePatch(String methodSignature, Object[] params). Next, call the different fix methods inside the method, where we still need the method signature

public class HotfixDemo$Patch {
    public Object invokePatch(String methodSignature, Object[] params) {
        switch (methodSignature) {
            case "method()Ljava/lang/String;";
                return method();
            default:
                break;
        }
    }

    public String method() {
        return "fix";
    }
}
Copy the code

As you can see, these fixed processes can be determined during the generation of the patch. When the code is injected at the beginning, it only needs to pass different parameters to the unified interface to execute the logic of the method specified in the patch. Here finally the whole execution process is strung together, let’s comb through it first

After the application is endowed with hot repair capability, when executing a method, it checks whether the patch corresponding to the current class exists (sInfo! = null), if it exists, it will continue to determine whether the currently executed method is within the repair scope of the repair class (**$Patch#needPatch()). If both conditions meet, it will call **$Patch#invokePatch() to execute the specific repaired method. Otherwise, the original logic is executed. With the process sorted out, we still have one last question, which is also the most important one. What is the logic of the repair method in the patch?

Actual Patch logic

In this paper, we used to do on the sample of the method is very simple, the statement on only one return value without parameters, and the method also only deal with the logic of a string constant in the body, but in general, a method of the content is more complicated, besides there are various types of parameters and return values, still can call other methods in the method of logic, operation processing field, So let’s do a slightly more complicated example

public class HotfixDemo { private static int sNumberA = 1; protected int mNumberB = 2; public String mString = "3"; public String method() { String msg = sNumberA + ":" + mNumberB + mString; print(msg); return msg; } private void print(String msg) { System.out.println(msg); }}Copy the code

In this example, the method() method handles HotfixDemo fields and calls a member method. If we change the logic to copy all the methods into HotfixDemo$Patch, it will not work. As a class not associated with HotfixDemo, these fields and methods are not declared in logic. In HotfixDemo$Patch, we need to process and call HotfixDemo’s field methods, but we can’t access private fields by reflection. Through this “translation”, we can get the following patch:

Public class HotfixDemo$Patch {public class HotfixDemo$Patch {/** * * @param host Running instance of the original class */ Public String Method (HotfixDemo host) {int sNumberA = reflecthelper. getField(host, "sNumberA"); int mNumberB = ReflectHelper.getField(host, "mNumberB"); String mString = host.mString; String MSG = sNumberA + ":" + mNumberB + mString; ReflectHelper.invokeMethod(host, msg); return msg; }}Copy the code

Here we add a parameter to the method signature that passes an instance of the class being repaired so that we can access the members of the class being repaired. This parameter can be passed in invokePatch() using this. To this, an entire process of hot fix all go down, but above a lot of code, such as HotfixDemo $Patch class, actually has certain rules to follow, at every time of release patches, these logical artificially through written words cost is too high and error-prone, so we can consider the Patch generated process fully automated, This will be covered in detail in the next section.