Don't understand the principle of thermal repair? After reading this, I'll show you the handwritten code

Keywords: Android Java Mobile network

Analysis of Android hot fix principle

The purpose of this article is to let you know what is thermal repair. Please contact me for specific implementation details. After we understand the principle, we can carry the handwritten code smoothly. Please remember one sentence:
The paper must be light at last, and we must do it.
The code that we need to write is our own code. Otherwise, you can read it and recite it. At last, we can't write excellent code in our work

What is thermal repair

Hot fix: enables applications to update without reinstallation, helping applications quickly build dynamic repair capabilities.

In the early days, when we encountered bugs, we usually released a version urgently. However, this Bug may be a simple line of code. For this line of code, it's a bit overqualified to perform full or incremental update and iteration of a version. And the popularity of the new version will take time. With the upgrading habits of Android users, even the relatively active wechat will take more than 10 days to cover 50% of users. Using thermal repair technology, it can cover more than 70% in one day. This is also based on the small patch size, which can be downloaded directly using the mobile network.

Hot fix development process:

At present, there are many hot fix technologies in Android industry. Each major manufacturer has launched its own hot fix solutions, and the technical solutions used are different. The QZone super patch is based on the dex subcontracting scheme, and the dex subcontracting is based on the Java class loading mechanism ClassLoader.

Introduction to ClassLoader

Any Java program is composed of one or more class files. When the program is running, the class file needs to be loaded into the virtual machine before it can be used. The class loading mechanism of Java is responsible for loading these class files The function of ClassLoader is simply to load the class file and provide it to the program runtime. There is a ClassLoader field inside each class object to identify which ClassLoader it is loaded by.

class Class<T> {
  ...
  private transient ClassLoader classLoader;
  ...
}

ClassLoader is an abstract class, and its main implementation classes are:

  • BootClassLoader
    Used to load Android Framework layer class file
  • PathClassLoader
    For Android application class loader. You can load the specified DEX, as well as classes.dex in jar, zip, apk
  • DexClassLoader
    Used to load the specified DEX, as well as classes.dex in jar, zip, apk

    Many blogs say that the PathClassLoader can only load the dex of the installed apk, but in fact, the PathClassLoader can load the dex in the sdcard as well as the DexClassLoader.

Log.e(TAG, "Activity.class By:" + Activity.class.getClassLoader() +" Load");
Log.e(TAG, "MainActivity.class By:" + MainActivity.class.getClassLoader() +" Load");

//Output:
Activity.class By: java.lang.BootClassLoader@d3052a9 Load
MainActivity.class By: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories[/data/app/com.enjoy.enoyfix-1/lib/x86, /system/lib, /vendor/lib]]] Load

The relationship between them is as follows:

The common parent of PathClassLoader and DexClassLoader is BaseDexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
   public DexClassLoader(String dexPath,String optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory),librarySearchPath, parent);
    }
}
public class PathClassLoader extends BaseDexClassLoader {
     public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
         super(dexPath, null, librarySearchPath, parent);
    }
}

You can see that the only difference between the two is that to create a DexClassLoader, you need to pass an optimized directory parameter, and it will be created as a File object and passed to super, while the PathClassLoader will directly give null. So both can load the specified DEX, as well as classes.dex in jar, zip and apk

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());

File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader= new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());

The optimizedDirectory parameter is the directory of odex. In fact, when the ClassLoader in Android loads dex, it will first optimize the dex through dexopt to generate the odex file. The default path when optimizedDirectory is null is: / data / Dalvik cache. For security reasons, this directory needs to use app private directory, such as getCodeCacheDir()

In the API 26 source code, mark the optimized directory of DexClassLoader as deprecated. The implementation is also changed to:

javapublicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){super(dexPath,null,librarySearchPath,parent);}

It's the same as PathClassLoader!

Parental delegation mechanism

To create ClassLoader, you need to receive a ClassLoaderparent parameter. This parent loads for the parent class. That is, when a class loader receives a request to load a class, it first delegates the load task to the parent class loader, recursively in turn. If the parent class loader can complete the class load task, it returns successfully. Only when the parent class loader cannot complete the load task, it can load by itself. This is the parent delegation mechanism!

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{

    // Check whether the class is loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        long t0 = System.nanoTime();
        try {
            if (parent != null) {
                //If the parent is not null, the loadClass line of the parent is called to load
                c = parent.loadClass(name, false);
            } else {
                //If the parent is null, call BootClassLoader to load
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {

        }

        if (c == null) {
            // If you can't find it, find it yourself
            long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
    return c;
}

Therefore, the ClassLoader we created ourselves: newpathclassloader ("/ sdcard / XX. DEX", getclassloader()); not only can we obtain the Class in xx.dex, but also the Class loaded in its parent ClassLoader.

findClass

When all the parent classloaders fail to load the Class, their findClass method is called. findClass is defined in ClassLoader as:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

In fact, any ClassLoader subclass can override loadClass and findClass. In general, if you don't want to use parental delegation, override loadClass to modify its implementation. Rewriting findClass means that when the parent ClassLoader cannot find a Class under the parent delegation, it defines how to find a Class. Our PathClassLoader will be responsible for loading the classes written by itself in programs such as MainActivity, and using the parent ClassLoader to load the activities in the Framework. Note that the PathClassLoader does not override the loadClass, so we can see how the findClass in the PathClassLoader is implemented.

public BaseDexClassLoader(String dexPath, FileoptimizedDirectory,String
                        librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath,
                                    optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //Find the specified class
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" +                                                      name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
             cnfe.addSuppressed(t);
        }
            throw cnfe;
    }
    return c;
}

The implementation is very simple. Find the class from the pathList. Continue to the DexPathList

public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
    //.........
    // splitDexPath is implemented to return list < File >. Add (dexpath)
    // makeDexElements will go to list < File >. Add (dexpath) and use DexFile to load the dex file to return the Element array

this.dexElements =makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
    //.........
}
public Class findClass(String name, List<Throwable> suppressed) {
     //Get DexFile representing Dex from element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            //Find class
            Class clazz = dex.loadClassBinaryName(name,definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

hot fix

There is an Element array in the PathClassLoader, and a dexFile member in the Element class represents a dex file, that is, if there are X dex in APK, then there are X elements in the Element array.

For class searching, the code for (element element: dexelements) knows that the array will search from front to back.

The element array in the PathClassLoader is: [patch.dex, classes.dex, classes2. DEX]. If there is a copy of Key.class in both patch.dex and classes2.dex, when the class is searched, the DexFile in dexElements will be obtained circularly. If the Key.class is found, it will be returned immediately, regardless of whether the DexFile in the subsequent element can be loaded into Key.class.

Therefore, you can make a patch.dex file (patch package) for the class with Bug, and then download the patch.dex from the server to save it to a path when the program starts, and then create an Element object through the file path of patch.dex, and then insert the Element object into the dexElements array header in the pathList of PathClassLoader of our program Department. In this way, the repair class in patch.dex will be loaded preferentially when the class in which the Bug occurs is loaded, so as to solve the Bug. This is the principle of QQ space hot fix. The dexElements array of pathList in PathClassLoader is used by reflection Hook.

summary

In the process of hot fix implementation, the necessary technologies include reflection, class loading mechanism and the content of ClassLoader in the source code of framehook layer. At the same time, if we need to continue the automatic patch generation, we need to master the content of gradle programming.

Each part of the article contains a series of interview points for BAT interview, which build a complete knowledge system, hot fix. Later, I will refine the knowledge in it. If you think there is a problem, you can communicate with me~

Posted by wilburforce on Mon, 25 Nov 2019 21:21:02 -0800