An article to understand the principle of hot repair class loading scheme!

ClassLoader type

Java  ClassLoader can load jar files and Class files (essentially load Class files). This is not applicable in Android, because both DVM and ART are no longer Class files, but dex files.

The ClassLoader types in Android are  ClassLoader similar to those in Java and are  also divided into two types, namely 系统 ClassLoader and 自定义 ClassLoader. Among them, Android  系统 ClassLoader includes three types, namely  BootClassLoader, PathClassLoaderand  DexClassLoader, and Java system class loader also includes three types, namely  Bootstrap ClassLoaderExtensions ClassLoader and  App ClassLoader.

BootClassLoader

When the Android system starts, it will be used  BootClassLoader to pre-load common classes. Unlike in Java  BootClassLoader , it is not implemented by C/C++ code, but by Java. BootClassLoade The code is as follows

// libcore/ojluni/src/main/java/java/lang/ClassLoader.java
class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }

    ...
}

BootClassLoader Yes  ClassLoader inner class, and inherited from  ClassLoaderBootClassLoader It is a singleton class. It should be noted that  **BootClassLoader** the access modifier is default and can only be accessed in the same package, so we cannot call it directly in the application .

PathClassLoader

The Android system uses  PathClassLoader to load system classes and application classes. If it is to load non-system application classes, data/app/$packagenamethe dex file and the apk file or jar file containing dex will be loaded  . No matter what kind of file is loaded, it will eventually be Load the dex file. In order to facilitate understanding, we will collectively refer to the dex file and the apk file or jar file containing dex as the dex related file. PathClassLoader is not recommended for development and direct use.

// libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java

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

PathClassLoaderInherited from  BaseDexClassLoader, the obvious  PathClassLoader method implementation is in  BaseDexClassLoader .

PathClassLoader The construction method has three parameters:

  • dexPath: The path collection of dex files and apk files or jar files containing dex. Multiple paths are separated by file separators. The default file separator is':'.
  • librarySearchPath: A collection of paths containing C/C++ libraries, multiple paths are separated by file separators, which can be null
  • parent:ClassLoader 的 parent

DexClassLoader

DexClassLoader You can load dex files and apk files or jar files containing dex, and also support loading from an SD card, which means that  DexClassLoader dex related files can be loaded without the application being installed. Therefore, it is the basis of hot repair and plug-in technology.

public class DexClassLoader extends BaseDexClassLoader {

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

DexClassLoader The parameter of the construction method is more  PathClassLoader than one  optimizedDirectory parameter. optimizedDirectory What does the parameter  represent? When the application is loaded for the first time, in order to improve the startup speed and execution efficiency in the future, the Android system will optimize the dex-related files to a certain extent and generate a  ODEX file. Then when the application is run again, just load it The optimized  ODEX file will do, saving the time to optimize each time, and the parameter  optimizedDirectory represents ODEX the path of the storage  file, this path must be an internal storage path. PathClassLoader No argument  optimizedDirectory, because  PathClassLoader already the default parameter  optimizedDirectory path is: /data/dalvik-cache. DexClassLoader Also inherited from  BaseDexClassLoader , the method implementation is also  BaseDexClassLoader in.

Regarding the above  ClassLoader creation process in the Android system, the process is involved here  Zygote , which is not the focus of this article, so I will not discuss it here.

ClassLoader inheritance relationship

image.png

  • ClassLoader It is an abstract class in which ClassLoader the main functions are defined  . BootClassLoader Is its internal class.
  • SecureClassLoader The code of the class and  JDK8 the  SecureClassLoaderclass in is the same, it inherits the abstract class  ClassLoader. SecureClassLoader It is not  ClassLoader the realization of the class, but the expanded  ClassLoader class to join the function of permissions, and strengthen the  ClassLoader security.
  • URLClassLoader The code of the  class and  JDK8 the  URLClassLoaderclass in is the same, it inherits from it  SecureClassLoader, and is used to load classes and resources from jar files and folders through the URL path.
  • BaseDexClassLoader Inherited from  ClassLoader, is ClassLoader the concrete implementation class of the abstract  class, PathClassLoaderand  DexClassLoader both inherit it.

Let’s take a look at the several types of class loaders needed to run an Android program

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var classLoader = this.classLoader

        // 打印 ClassLoader 继承关系
        while (classLoader != null) {
            Log.d("MainActivity", classLoader.toString())
            classLoader = classLoader.parent
        }
    }
}

The  MainActivity class loader printed, and the printing of the current parent class loader loader, the loader until no parent, then the cycle terminates. The print result is as follows:

com.zhgqthomas.github.hotfixdemo D/MainActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk"],nativeLibraryDirectories=[/data/app/com.zhgqthomas.github.hotfixdemo-2/lib/arm64, /oem/lib64, /system/lib64, /vendor/lib64]]]

com.zhgqthomas.github.hotfixdemo D/MainActivity: java.lang.BootClassLoader@4d7e926

You can see that there are two kinds of class loaders, one is  PathClassLoader, the other is  BootClassLoader. DexPathList There are many paths in it, including /data/app/com.zhgqthomas.github.hotfixdemo-2/base.apk the location where  the sample application is installed on the phone.

Parent delegation model

The class loader uses the parental delegation mode to find a Class. The so-called parental delegation mode is to first determine whether the Class has been loaded. Entrusted to the top level BootstrapClassLoader, if  the Class is found, it will return directly, if it is not found, it will continue to look down one by one, and if it has not been found, it will finally be handed over to itself to search.  This is  the implementation logic in JDK,   and there are differences  in  the logic processing of methods in Android  .BootstrapClassLoader ClassLoaderClassLoaderfindBootstrapClassOrNull

// ClassLoader.java

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 委托父加载器进行查找
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

The above code is easy to understand. First, it will find out whether the loaded class has been loaded. If it is returned directly, otherwise it will be delegated to the parent loader to search, and the findBootstrapClassOrNull method will be called until there is no parent loader  .

Let's take a look at   how it is implemented findBootstrapClassOrNull in  JDK and  Androidin respectively

// JDK ClassLoader.java

    private Class<?> findBootstrapClassOrNull(String name)
    {
        if (!checkName(name)) return null;

        return findBootstrapClass(name);
    }

JDK The Central  findBootstrapClassOrNull Committee will eventually be handed over  BootstrapClassLoader to find the  Class file. As mentioned above  BootstrapClassLoader , it findBootstrapClass is implemented by C++, so it  is a native method

// JDK ClassLoader.java private native Class<?> findBootstrapClass(String name);

findBootstrapClassOrNull The implementation  in Android  JDK is different

// Android 
    private Class<?> findBootstrapClassOrNull(String name)
    {
        return null;
    }

Android Because it does not need to be used,  BootstrapClassLoader this method returns directly null

It is the use of the class loader to find the parent delegation mode adopted by the Class, so the order in which the class loader loads the dex related files can be modified by reflection to achieve the purpose of hot repair

Class loading process

Through the above analysis, we can see

  • PathClassLoader Can load dex files in Android system
  • DexClassLoader You can load dex/zip/apk/jar files in any directory  , but you must specify it optimizedDirectory.

These two classes known by the code just inherited  BaseDexClassLoader, the specific implementation is still a  BaseDexClassLoadercomplete come.

BaseDexClassLoader

// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

public class BaseDexClassLoader extends ClassLoader {

    ...

    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
    }

    /**
     * @hide
     */
    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }

    ...

    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    ...
}

By  BaseDexClassLoader know constructor to initialize the most important thing is  pathList that is  DexPathList this class, which is mainly used to manage documents dex

// libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions); // 查找逻辑交给 DexPathList
        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;
    }

BaseDexClassLoader The most important thing is this  findClass method, which is used to load the corresponding file in the dex  class file. And in the end it is handed over to the  DexPathList class to handle the implementation findClass

DexPathList

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

final class DexPathList {
    ...

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

    ...

    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
       ...

        this.definingContext = definingContext;

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ...
    }

}

Looking  DexPathList at the code of the core constructor, we can see that the DexPathList class  Element is stored  dex 路径 through the makeDexElements function , and the dex related file is loaded through the  function, and the Element collection is returned 

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) { // 判断是否是 dex 文件
                  // Raw dex file (not inside a zip/jar).
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else { // 如果是 apk, jar, zip 等文件
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                    // 将 dex 文件或压缩文件包装成 Element 对象,并添加到 Element 集合中
                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
              if (dex != null && isTrusted) {
                dex.setTrusted();
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

Generally speaking, DexPathList the constructor is to encapsulate dex related files (may be dex, apk, jar, zip, these types are defined at the beginning) into an  Element object, and finally add it to the  Element collection

In fact, Android's class loader either PathClassLoader, or DexClassLoader, they recognize only the last dex files, and  loadDexFileis the core method to load dex files can be extracted from dex jar, apk, zip in

// libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

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

It DexPathList has been initialized in  the constructor  dexElements, so this method is easy to understand. It just traverses the Element array. Once a class with the same class name and name is found, this class is returned directly, and null is returned if it cannot be found.

Hot fix implementation

Through the above analysis, we can know that running a  Android program is used  PathClassLoader, that is  BaseDexClassLoader, the dex related files in the apk will be stored in   the  properties of BaseDexClassLoader the  pathListobject  dexElements.

Then the principle of hot fix is ​​to put the dex related files of the corrected bug into  dexElements the head of the collection, so that when traversing, it will first traverse the repaired dex and find the repaired class, because of the parent delegation mode of the class loader, in the old dex Classes with bugs have no chance to play. In this way, it is possible to fix the existing bug class without releasing a new version

Manually implement the hot repair function

According to the principle of hot repair above, the corresponding ideas can be summarized as follows

  1. Create a  BaseDexClassLoader subclass  DexClassLoader loader
  2. Load the repaired class.dex (the repair package downloaded by the server)
  3. dexElements Combine own and system  , and set the dexElements priority of freedom 
  4. Through reflection technology, assigned to the system pathList

Guess you like

Origin blog.csdn.net/Java_Yhua/article/details/110223104