This type of

In Java, classloaders can load JAR files and Class files (essentially loading Class files). This does not apply in Android because both DVM and ART are no longer loading Class files, but dex files.

The Type of The Android ClassLoader is similar to that of the Java ClassLoader. There are two types of classloaders: system ClassLoader and custom ClassLoader. The Android ClassLoader includes three types: BootClassLoader, PathClassLoader, and DexClassLoader. The Java ClassLoader also includes three types. They are Bootstrap ClassLoader, Extensions ClassLoader, and App ClassLoader.

BootClassLoader

The Android system uses a BootClassLoader to preload common classes at startup. Unlike the Java BootClassLoader, it is not implemented by C/C++ code, but by Java. The code for BootClassLoade is shown below

// 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(a) {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader(a) {
        super(null);
    }

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

BootClassLoader is an inner class of ClassLoader and inherits from ClassLoader. BootClassLoader is a singleton class. Note that the access modifier of BootClassLoader is default and can only be accessed in the same package, so we cannot call it directly in the application.

PathClassLoader

Android uses PathClassLoader to load system classes and application classes. If you load non-system application classes, you will load the dex file under data/app/$Packagename and the APK file or JAR file containing dex. No matter what kind of file is loaded, the dex file will be loaded eventually. Here, for the convenience of understanding, we refer to the dex file and the APK file or JAR file containing dex as dex related files. PathClassLoader is not recommended for 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); }}Copy the code

PathClassLoader inherits from BaseDexClassLoader, so obviously the methods of PathClassLoader are all implemented in BaseDexClassLoader.

The PathClassLoader constructor takes three arguments:

  • DexPath: indicates the path set of the dex file and apK files or JAR files that contain dex. Multiple paths are separated by file separator. The default file separator is:.
  • LibrarySearchPath: Collection of paths containing C/C++ libraries. Multiple paths are separated by file delimiters and can be null
  • Parent: indicates the parent of the ClassLoader

DexClassLoader

The DexClassLoader can load the dex file and apK files or JAR files containing the dex. It can also be loaded from an SD card. This means that the DexClassLoader can load the dex files when the application is not installed. As such, it is the basis for hot fixes and plugins.

public class DexClassLoader extends BaseDexClassLoader {
    
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent); }}Copy the code

The DexClassLoader constructor takes one more parameter optimizedDirectory than the PathClassLoader constructor. What does the parameter optimizedDirectory 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 an ODEX file. After that, when the application is run, it is only necessary to load the optimized ODEX file. The optimizedDirectory parameter represents the path where the ODEX file is stored. This path must be an internal storage path. PathClassLoader has no parameter optimizedDirectory because PathClassLoader already defaults the parameter optimizedDirectory to /data/dalvik-cache. The DexClassLoader also inherits from BaseDexClassLoader, and the methods are implemented in BaseDexClassLoader.

The procedure for creating the above ClassLoader in Android is not the focus of this article because it involves the Zygote process.

ClassLoader inheritance relationship

  • ClassLoaderIs an abstract class that definesClassLoaderThe main function of.BootClassLoaderIs its inner class.
  • SecureClassLoaderClasses andJDK8In theSecureClassLoaderThe code for the class is the same, it inherits from the abstract classClassLoader.SecureClassLoaderIs notClassLoaderThe implementation class is extended insteadClassLoaderClass has been enhanced with the addition of privilegesClassLoaderSafety.
  • URLClassLoaderClasses andJDK8In theURLClassLoaderClass is the same, it inherits fromSecureClassLoaderIs used to load classes and resources from JAR files and folders via URl paths.
  • BaseDexClassLoaderInherited fromClassLoaderIs an abstract classClassLoaderThe concrete implementation class,PathClassLoaderandDexClassLoaderInherit it.

Let’s look at the types of classloaders you need to run an Android application

class MainActivity : AppCompatActivity() {

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

        var classLoader = this.classLoader

        // Print the ClassLoader inheritance
        while(classLoader ! =null) {
            Log.d("MainActivity", classLoader.toString())
            classLoader = classLoader.parent
        }
    }
}
Copy the code

Print out the classloader for MainActivity, and print the parent of the current classloader until there is no parent to terminate the loop. 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.hotfi xdemo-2/lib/arm64, /oem/lib64, /system/lib64, /vendor/lib64]]] com.zhgqthomas.github.hotfixdemo D/MainActivity: java.lang.BootClassLoader@4d7e926Copy the code

You can see that there are two kinds of loaders, one is PathClassLoader and the other is BootClassLoader. DexPathList contains many paths, including/data/app/com zhgqthomas. Making. Hotfixdemo – 2 / base. Apk is sample application installed on a mobile phone location.

Parent delegation mode

The parent delegate mode is that the classloader first determines whether the Class is loaded. If it is not, it does not look for the Class itself, but delegates it to the parent loader. In this way, it recurses until it delegates to the top-level BootstrapClassLoader. If the BootstrapClassLoader finds the Class, the BootstrapClassLoader returns it. If the BootstrapClassLoader does not find the Class, the BootstrapClassLoader searches for the Class. If the BootstrapClassLoader does not find the Class, the BootstrapClassLoader searches for the Class. This is the implementation logic of the ClassLoader in the JDK. The Android ClassLoader has a difference in the logical handling of the findBootstrapClassOrNull method.

// ClassLoader.java

    protectedClass<? > 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) {
                        // Delegate the lookup to the parent loader
                        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;
    }
Copy the code

The above code is easy to understand. It first looks for whether the loading class has been loaded, returns directly if not, and otherwise delegate the search to the parent loader until there is no parent loader and then calls the findBootstrapClassOrNull method.

Let’s take a look at how findBootstrapClassOrNull is implemented in JDK and Android

// JDK ClassLoader.java

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

        return findBootstrapClass(name);
    }

Copy the code

FindBootstrapClassOrNull in the JDK will eventually be passed to the BootstrapClassLoader, which is implemented by C++, to find the Class file. So findBootstrapClass is a native method

// JDK ClassLoader.java

    private nativeClass<? > findBootstrapClass(String name);Copy the code

FindBootstrapClassOrNull is implemented differently in Android than in the JDK

// Android 
    privateClass<? > findBootstrapClassOrNull(String name) {return null;
    }
Copy the code

Android does not need to use the BootstrapClassLoader, so this method returns null

It is the parent delegate mode adopted by the Class loader to find the Class, so you can use reflection to modify the order in which the Class loader loads dex related files, so as to achieve the purpose of hotfix

Class loading process

It can be seen from the above analysis

  • PathClassLoaderYou can load the dex file in the Android system
  • DexClassLoaderYou can load any directorydex/zip/apk/jarFile, but specifyoptimizedDirectory.

These two classes only inherit from BaseDexClassLoader, and the implementation is still done by BaseDexClassLoader.

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); }... }Copy the code

From the BaseDexClassLoader constructor, the most important thing to do is to initialize the pathList, or the DexPathList class, which is used to manage dex files

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

    @Override
    protectedClass<? > findClass(String name)throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions); // Give the search logic to 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;
    }
Copy the code

The most important part of the BaseDexClassLoader is the findClass method, which is used to load the corresponding class file in the dex file. And ultimately it’s the DexPathList class that handles the implementation of 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). */
    privateElement[] 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); . }}Copy the code

Looking at the code for the core constructor of DexPathList, we know that the DexPathList class stores the dex path through elements, and loads the dex file through the makeDexElements function, and returns the Element collection

// 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)) { // Check whether it is a dex file
                  // 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 { // If it is apk, jar, zip, etc
                  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);
                  }

                    // Wrap the dex file or compressed file as an Element object and add it to the Element collection
                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = newElement(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;
    }
Copy the code

In general, the constructor for DexPathList wraps the dex related file (perhaps dex, apk, JAR, zip, these types were defined at the beginning) into an Element object and then adds it to the Element collection

In fact, Android class loaders, whether PathClassLoader or DexClassLoader, only recognize the dex file, and loadDexFile is the core method to load the dex file. Dex can be extracted from JAR, APK, and ZIP

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

    publicClass<? > findClass(String name, List<Throwable> suppressed) {for(Element element : dexElements) { Class<? > clazz = element.findClass(name, definingContext, suppressed);if(clazz ! =null) {
                returnclazz; }}if(dexElementsSuppressedExceptions ! =null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
Copy the code

DexElements are already initialized in the DexPathList constructor, so this method makes sense. It simply iterates through the Element array and returns the same class as the name if it is not found, or null if it is not

Hot repair implementation

Running an Android application uses a PathClassLoader, That is, BaseDexClassLoader, and dex related files in APK are stored in the dexElements attribute of pathList object of BaseDexClassLoader.

The principle of hot fix is to put the dex file that has been fixed into the head of the dexElements set. In this way, the traversal will first traverse the dex and find the fixed class. Because of the parent delegate mode of the classloader, – Classes with bugs in the old dex will not get a chance to play. This makes it possible to fix existing bug classes without releasing a new version

Manually implement the hotfix function

Based on the above hot-repair principles, the corresponding ideas can be summarized as follows

  1. createBaseDexClassLoaderA subclass ofDexClassLoaderloader
  2. Load the fixed class.dex (the fix package downloaded from the server)
  3. Will be self-owned and systematicdexElementsMerge and set freedexElementspriority
  4. Assigned to the system by means of reflectionpathList

Hotfix Demo is recommended

See this project on Github

reference

  • Manually implement Android hotfixes
  • (2) Android ClassLoader