Hot repair
At present, domestic Android hot fix mainstream framework has Ali Andfix,Sopfix, wechat Tinker, Meituan Robsut and so on, not one to list… Among them, Andfix and Sopfix are implemented with native layer, while Tinker and Robsut are implemented with Java layer
The main frame illustration is attached:
The two implementations have their own advantages and disadvantages, so we will not do too much analysis here. This article will simply analyze the basic implementation of Andfix
The flow chart
The principle flow chart of Andfix to realize hot repair is as follows: To put it simply: first, we locate the method of the bug class, repair a new installation package, compare with the old installation package (bug), generate poor subpackage (repair package), replace the method of the bug of the old app. Locate the bug → make a new package → compare the generation of poor subpackage → replace the method of the bug
Distribution of memory
To understand how hot fixes work, we need to know something about Java’s memory distribution, as shown below:
-
Method area: When the JVM uses a class loader to locate a class file and enter it into memory, it extracts the class type information and stores it in the method area, along with static class variables of that type, and in the method table, which is a method table for each class.
-
Heap area: The heap area is mainly where objects are stored. All types of objects and arrays created by Java programs during execution are stored in the heap. The JVM creates a certain type of object memory space in the heap based on the new instruction, but the space created in the heap is not reclaimed by any manual instruction, but is reclaimed by the JVM’s garbage collector.
-
Stack area: Each time a thread is started, the JVM creates a Java stack for it to hold local variables, operands, and exception data in the method. When a thread calls a method, the JVM creates a stack frame based on the method’s bytecode in the method area and pushes the stack frame into the Java stack. When the method is executed, the JVM ejects the stack frame and releases it.
The first thing we need to understand is that the Java files that we’re writing are compiled to generate.class files that are eventually loaded into memory, and that’s the method area, so what we need to do is replace the methods that aren’t buggy in the subpackage with the methods that are buggy in the method area. Therefore, what we want to replace is the Java method running in the Java virtual machine. Of course, we cannot achieve this step through Java technology, so Andfix uses Native to replace the Java method. Here we need to briefly explain the Java virtual machine running mechanism
Vm running mechanism
When the user clicks on the App, the Launcher tells the JVM to load and execute the corresponding ActivityThread class. The process looks something like this:
The file loading (ActivityThread.class) is completed by dexfile. Java, and the internal implementation is implemented by native method. The process is roughly as follows:
When the Main method of ActivityThread executes, it loads the Application class that executes the App, creates an Application object by reflection, and then calls its onCreate() method Step 1: Declare a member variable of type Application ()
// This step does not load the Application. Class bytecode into memory, but generates a symbolic variable of type int in the method area.Copy the code
Supplement: Objects are only loaded into memory if they are actively referenced. Common active references include :new an object, reflection creates an object, JNI’s findClass(), and serialization Step 2: Create the Application object by reflection. This step will load the Application bytecode file into memory, store the created object in the heap, and call onCreate() through the Application object.
As shown above: Objects in the heap point to symbolic variables of type int. This is why we create an object that can be retrieved by the getClass() method (native implementation, using klass variables). When the onCreate() method is called from the Application object, the Application object in the heap points to the int symbol variable, which points to the method table, the onCreate() method is executed, and the onCreate() method is assembled into a stack frame, which is pushed onto the Java stack, and ejected and released after execution
I looked at a bunch of flow charts. There were a lot of question marks?????
Understand the above theory to better understand the principle of Andfix, cut to the chase before there is a problem, if we have a class a method appeared abnormal (program execution can create multiple types of objects are likely to call the method), we should be realized in which region cut after modification method were replacement of all objects to invoke this method is not Can something go wrong? The answer is: the method table, because all object execution methods find the method table through symbolic variables, and then stack the method into a stack frame
ArtMethod structure
Method table can be understood as an array, the array is stored in the ArtMethod structure, load class information to create a method table, instantiate the ArtMethod structure object steps, hidden in the Android source code is relatively deep (focus is not to understand ~), interested can look, here to Android5.x version as an example, road Go to Android-5.0.1_r1 →art→ Runtime and find class_linker.cc and then look at FindClass →DefineClass(define a Class, klass is in this method) LineClass(LineSuperClass) →LinkInterfaceMethod(), finally create method table in LinkInterfaceMethod(), iterate according to the number of methods, instantiate ArtMeth The OD structure is stored in an array
Andfix basic function code implementation
Android Studio creates a JNI project
Create several classes as shown below:
Replace
Annotations are used to indicate the name of the class or method to be fixed
@target (ElementType.METHOD) @Retention(retentionPolicy.runtime) public @interface Replace { String clazz(); // Fix which class String method(); // Which method to fix}Copy the code
- with
MainActivity
At the same level ofCalculator
There is only one class for simulating exception informationcalculator
Method, ran an exception directly
/** * public class Calculator {public intcalculator(){// throw new RuntimeException(); }}Copy the code
The Calculator class in the 3Service directory was created to generate the difference subcontracting, eliminating manual difference subcontracting, and generating the. Class file with the help of the Build function of Android Studio
/** * Emulated the fixed class * Normally you would get the fixed calculator.java compiled.dex file from the back end, omits manual compilation of the package * Calculator.java is compiled into calculator. class using the JavaC command, which can then be packaged into a.dex file using the dex command */ public class Calculator {@replace (clazz ="com.jni.fixdemo.Calculator" ,method = "calculator")
public int calculator() {return1024; }}Copy the code
Need to pass this class to generate the subcontract, namely. Dex file, click rebuild, and will be in the app, build, intermediates, javac \ debug \ compileDebugJavaWithJavac \ classes \ package name generated directory. Class files Bat file is generated under the directory \build-tools\ version number \ under the SDK directory. The dex file can be generated in two ways. The first way is to copy the above files to the directory where dx.bat is located and run the dx command.
Dx –dex –output = C:\Users\tpson\Desktop\out\fix. Dex = C:\Users\tpson\Desktop\out\fix C:\Users\tpson\Desktop\result
Note: C:\Users\tpson\Desktop\out\fix. Dex indicates the output path and file name, and C:\Users\tpson\Desktop\result indicates the. Class file path, which is not the full path. For example, my.class file is actually in C:\Users\tpson\Desktop\result\com\jni\fixdemo\service\Calculator
-
MainActivity is mainly a TextView, two buttons, one Button click on the event to call the exception method, the other Button click on the event, call the logic to replace the exception method, relatively simple not to post code, you can go to Github to view the complete code Andfix basic function now
-
The main function of DexManager is to load differential subpackages (.dex files, which may be. Patch files generated by third-party tools) from SD card, find the replacement method by using Replace annotation, and hand it to native layer for implementation
Note here: The fix.dex file is loaded from SD, so Class information cannot be loaded through class.forname (). Instead, Class information can be obtained through dexfile.loadClass (). When we get the method to be replaced and the name of the Class to which it belongs from the fix.dex file, we load the party to be replaced Class information can be loaded through class.forname () because the Class is loaded into memory through the virtual machine
/** * load. Dex file */ public class DexManager {private Context Context; public DexManager(Context context) { this.context = context; } public void load(File file){ try { DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(), new File(context.getCacheDir(),"opt").getAbsolutePath(), Context.MODE_PRIVATE); Enumeration<String> entries = dexfile.entries ();while(entries.hasMoreElements()){ String clazzName=entries.nextElement(); // use dexfile. loadClass to loadClass clazz = dexFile.loadClass(clazzName, context.getClassLoader());if(clazz! =null) { fixClazz(clazz); } } } catch (IOException e) { e.printStackTrace(); }} private void fixClazz(Class clazz) {// Methods = clazz.getmethods ();for(Method fixMethod: methods) {Replace Replace = fixmethod.getannotation (Replace. Class);if (replace==null) {
continue; } String clazzName = replace.clazz(); String methodName = replace.method(); Class errorClass = class.forname (clazzName); / / get methods need to be replaced, the second parameter argument list, ensure that finding the right Method errorMethod = errorClass. GetDeclaredMethod (methodName, fixMethod.getParameterTypes()); replace(errorMethod,fixMethod); } catch (Exception e) { e.printStackTrace(); }}} public native static void replace(Method errorMethod,Method fixMethod);}} public native static void replace(Method errorMethod,Method fixMethod); }Copy the code
Header file and native layer replacement method implementation
Note: due to the way Andfix is implemented, there are major compatibility issues, so the following code is based on Android6.0, that is, it will work on Android6.0 models, here directly refer to the Andfix source header and native implementation
The header file art_method. H
#include <string.h>
#include <jni.h>
#include <stdio.h>
#include <fcntl.h>
#include <dlfcn.h>
#include
/* C99 */
namespace art {
namespace mirror {
class Object {
public:
// The number of vtable entries injava.lang.Object. static constexpr size_t kVTableLength = 11; static uint32_t hash_code_seed; uint32_t klass_; uint32_t monitor_; }; class Class: public Object { public: static constexpr uint32_t kClassWalkSuper = 0xC0000000; static constexpr size_t kImtSize = 0; //IMT_SIZE; uint32_t class_loader_; uint32_t component_type_; uint32_t dex_cache_; uint32_t dex_cache_strings_; uint32_t iftable_; uint32_t name_; uint32_t super_class_; uint32_t vtable_; uint32_t access_flags_; uint64_t direct_methods_; uint64_t ifields_; uint64_t sfields_; uint64_t virtual_methods_; uint32_t class_size_; pid_t clinit_thread_id_; int32_t dex_class_def_idx_; int32_t dex_type_idx_; uint32_t num_direct_methods_; uint32_t num_instance_fields_; uint32_t num_reference_instance_fields_; uint32_t num_reference_static_fields_; uint32_t num_static_fields_; uint32_t num_virtual_methods_; uint32_t object_size_; uint32_t primitive_type_; uint32_t reference_instance_offsets_; uint32_t status_; static uint32_t java_lang_Class_; }; class ArtField { public: uint32_t declaring_class_; uint32_t access_flags_; uint32_t field_dex_idx_; uint32_t offset_; }; class ArtMethod { public: uint32_t declaring_class_; uint32_t dex_cache_resolved_methods_; uint32_t dex_cache_resolved_types_; uint32_t access_flags_; uint32_t dex_code_item_offset_; uint32_t dex_method_index_; uint32_t method_index_; struct PtrSizedFields { void* entry_point_from_interpreter_; void* entry_point_from_jni_; void* entry_point_from_quick_compiled_code_; } ptr_sized_fields_; }; }}Copy the code
Native layer logic implements native-lib.cpp
#include <time.h>
#include <stdlib.h>
#include <stddef.h>
#include <assert.h>
#include <stdbool.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <utime.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "art_method.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_jni_fixdemo_DexManager_replace(JNIEnv *env, jclass type, jobject errorMethod, Jobject fixMethod) {// get the ArtMethod structure. // Import the ArtMethod header file. // Import the art_method.h file directly art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(errorMethod); art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(fixMethod); reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
Copy the code
For complete demo, go to github Andfix
Andfix compatible
Andfix has version compatibility problems and has stopped updating. Sopfix is not open source and is a charging project (free within 5000 users). The compatibility implementation of Andfix will be analyzed below The implementation of Andfix has a big problem with compatibility. Android_Andfix is compatible with Sophix