Full analysis of Android's ClassLoader

class loader

foreword

Android's Dalvik/ART virtual machine, like the standard JAVA JVM virtual machine, first needs to load the corresponding class into the memory when running the program. Therefore, we can take advantage of this to manually load the Class when the program is running, so as to achieve the purpose of dynamically loading the executable file by the code. Although the Dalvik/ART virtual machine is different from the JVM virtual machine and the specific loading details of the ClassLoader are different, the working mechanism is similar, which means that a similar function of dynamically loading plug-ins can also be used in Android.

ClassLoader classification

  • Let's simply write a Demo to see what source code of ClassLoader is as follows:

     package org.professor.classloaderdemo;
    
     import android.content.Context;
     import android.support.v7.app.AppCompatActivity;
     import android.os.Bundle;
     import android.util.Log;
     import android.widget.ImageView;
    
     public class MainActivity extends AppCompatActivity {
    
         @ Override 
         protected void onCreate(Bundle savedInstanceState) {
             super.onCreate(savedInstanceState);
             setContentView(R.layout.activity_main);
    
             Log.i("TAG", "Context的类加载加载器:" + Context.class.getClassLoader()); 
             Log.i("TAG", "ImageView的类加载器:" + ImageView.class.getClassLoader());  
             Log.i("TAG", "应用程序默认加载器:" + getClassLoader()); 
             Log.i("TAG", "系统类加载器:" + ClassLoader.getSystemClassLoader());		
             Log.i("TAG", "系统类加载器和Context的类加载器是否相等:" +
                     (Context.class.getClassLoader() == ClassLoader.getSystemClassLoader())); //false		
             Log.i("TAG", "系统类加载器和应用程序默认加载器是否相等:" +
                     (getClassLoader() == ClassLoader.getSystemClassLoader())); 		
             Log.i("TAG", "打印应用程序默认加载器的委派机制:");
             ClassLoader classLoader = getClassLoader();
             while (classLoader != null) {
                 Log.i("TAG", "类加载器:" + classLoader); 
                 classLoader = classLoader.getParent();
             }		
             Log.i("TAG", "打印系统加载器的委派机制:");
             classLoader = ClassLoader.getSystemClassLoader();
             while (classLoader != null) {
                 Log.i("TAG", "类加载器:" + classLoader);
                 classLoader = classLoader.getParent();
             }
    
         }
    
  • LOGTAG_INFO is as follows:

      08-26 16:08:45.688 12403-12403/? I/TAG: Context的类加载加载器:java.lang.BootClassLoader@b039c3e
      08-26 16:08:45.688 12403-12403/? I/TAG: ImageView的类加载器:java.lang.BootClassLoader@b039c3e
      08-26 16:08:45.688 12403-12403/? I/TAG: 应用程序默认加载器:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.lq.classloaderdemo-1/base.apk"],nativeLibraryDirectories=[/data/app/com.lq.classloaderdemo-1/lib/arm64, /vendor/lib64, /system/lib64]]]
      08-26 16:08:45.688 12403-12403/? I/TAG: 系统类加载器:dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
      08-26 16:08:45.688 12403-12403/? I/TAG: 系统类加载器和Context的类加载器是否相等:false
      08-26 16:08:45.688 12403-12403/? I/TAG: 系统类加载器和应用程序默认加载器是否相等:false
      08-26 16:08:45.688 12403-12403/? I/TAG: 打印应用程序默认加载器的委派机制:
      08-26 16:08:45.688 12403-12403/? I/TAG: 类加载器:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.lq.classloaderdemo-1/base.apk"],nativeLibraryDirectories=[/data/app/com.lq.classloaderdemo-1/lib/arm64, /vendor/lib64, /system/lib64]]]
      08-26 16:08:45.688 12403-12403/? I/TAG: 类加载器:java.lang.BootClassLoader@b039c3e
      08-26 16:08:45.688 12403-12403/? I/TAG: 打印系统加载器的委派机制:
      08-26 16:08:45.688 12403-12403/? I/TAG: 类加载器:dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
      08-26 16:08:45.688 12403-12403/? I/TAG: 类加载器:java.lang.BootClassLoader@b039c3e
    
  • Known from the above class loaders are as follows

    • 1.BootClassLoader

      It can be seen that View and Context are loaded by the ClassLoader, the system class loader

      It is known from the source code that this class also inherits ClassLoader

    • 2. The default class loader is PathClassLoader, and you can see the loaded apk path, libPath (generally including /vendor/lib and /system/lib)

    • 3. System class loader Pay attention to distinguish the loader of the system class

      The system class loader is actually PathClassLoader, but the path of the loaded apk is not /data/app/xxx.apk, but the path of the system apk: /system/app/xxx.apk

    • 4. As can be seen from the above, the delegation relationship between the system loader and the default loader: its base class is BootClassLoader

ClassLoader source code analysis

  • UML

    ClassLoader_UML

  • ClassLoader.java source code

    It can be seen from the source code that ClassLoader mainly passes in a parent constructor, and generally the parent constructor cannot be empty, unlike the default parent constructor in the Java virtual machine when the parent constructor is empty is Bootstrap ClassLoader. If no parent constructor is passed in by default in Android, the default parent constructor is a PathClassLoader and the PathClassLoader parent constructor is BootClassLoader.

     public abstract class ClassLoader {
         static private class SystemClassLoader {
             public static ClassLoader loader = ClassLoader.createSystemClassLoader();
         }
         private ClassLoader parent;
    
         private static ClassLoader createSystemClassLoader() {
             String classPath = System.getProperty("java.class.path", "."); 
             return new PathClassLoader(classPath, BootClassLoader.getInstance()); //getSystemClassLoader加载器是pathclassloader,它的parent是BootClassLoader,但是DexPathList[[directory "."]..
         }
    
         public static ClassLoader getSystemClassLoader() {
             return SystemClassLoader.loader; //返回系统默认类加载器
         }
    
     	protected ClassLoader() {
     		this(getSystemClassLoader(), false);
         }
    
         ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
             if (parentLoader == null && !nullAllowed) {
                 throw new NullPointerException("parentLoader == null && !nullAllowed");
             }
             parent = parentLoader;
         }
         //自定义classloader需要重载该方法
         protected Class<?> findClass(String className) throws ClassNotFoundException {
             throw new ClassNotFoundException(className);
         }
         protected final Class<?> findLoadedClass(String className) {
             ClassLoader loader;
             if (this == BootClassLoader.getInstance()) //如果该classloader是BootClassLoader类型
                 loader = null;
             else
                 loader = this;
             return VMClassLoader.findLoadedClass(loader, className); //调用本地c/c++方法
         }
         //可以看到android系统其实也实现了双亲委托模型,只是跟java的双亲委托模型有点不同而已
         protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
             Class<?> clazz = findLoadedClass(className); //检查是否已经加载过
             if (clazz == null) {
                 ClassNotFoundException suppressed = null;
                 try {
                     clazz = parent.loadClass(className, false); //使用parent去查找
                 } catch (ClassNotFoundException e) {
                     suppressed = e;
                 }
    
                 if (clazz == null) {
                     try {
                         clazz = findClass(className); //调用findclass
                     } catch (ClassNotFoundException e) {
                         e.addSuppressed(suppressed);
                         throw e;
                     }
                 }
             }
             return clazz;
         }
         //可以看到loadClass的resolve参数是没用的
         protected final void resolveClass(Class<?> clazz) {
         }
     }
     //BootClassLoader单例模型
     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, true);
         }
         @Override
         protected Class<?> findClass(String name) throws ClassNotFoundException {
             return Class.classForName(name, false, null);
         }
     }
    

    NOTE **: Parent delegation model**

    • When loading a class, first determine whether the class has been loaded before, and if so, return directly;
    • If not, first try to let the parent ClassLoader to load, if the load is unsuccessful, it will be loaded in its own findClass.

    This is consistent with the common parent delegation model in the Java virtual machine. This model is not a mandatory constraint model. For example, you can inherit ClassLoader and override the loadCalss method to destroy this model, but the parent delegation model is a recommended model. The way to implement the class loader, and after jdk1.2, users are not recommended to override the loadClass method, but should write their own class loading logic into findClass.

    • BootClassLoader

      The difference from the java virtual machine is that BootClassLoader is an inner class of ClassLoader, which is implemented by java code instead of C++. It is the final parent of all ClassLoaders on the Android platform. This inner class is visible in the package, so we can't use it.

  • DexClassLoader&PathClassLoader

    • DexClassLoader.java

        public class DexClassLoader extends BaseDexClassLoader {
      
            public DexClassLoader(String dexPath, String optimizedDirectory,
                    String libraryPath, ClassLoader parent) {
                super(dexPath, new File(optimizedDirectory), libraryPath, parent);
            }
        }
      
    • PathClassLoader.java

        public class PathClassLoader extends BaseDexClassLoader {
      
            public PathClassLoader(String dexPath, ClassLoader parent) {
                super(dexPath, null, null, parent);
            }
      
            public PathClassLoader(String dexPath, String libraryPath,
                    ClassLoader parent) {
                super(dexPath, null, libraryPath, parent);
            }
        }
      

    NOTE: I have removed some messy comments so that it looks more obvious. In fact, it is a simple encapsulation of the base class BaseDexClassLoader. The real processing logic is in BaseDexClassLoader.

  • BaseDexClassLoader.java

    As you can see above, DexClassLoader and PathClassLoader both encapsulate this class

      private final DexPathList pathList;
      /**
       * Constructs an instance.
       *
       * @param dexPath the list of jar/apk files containing classes and
       * resources, delimited by {@code File.pathSeparator}, which
       * defaults to {@code ":"} on Android
       * @param optimizedDirectory directory where optimized dex files
       * should be written; may be {@code null}
       * @param libraryPath the list of directories containing native
       * libraries, delimited by {@code File.pathSeparator}; may be
       * {@code null}
       * @param parent the parent class loader
       */
      public BaseDexClassLoader(String dexPath, File optimizedDirectory,
          String libraryPath, ClassLoader parent) {
          super(parent);
          this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
      }
    
      @Override
      protected Class<?> findClass(String name) throws ClassNotFoundException {
          List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
          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;
      }
    

    You can see that this class implements ClassLoader and rewrites the findClass() method. Let me explain the meaning of the four parameters in the constructor.

    • dexPath, refers to the path of the APK or jar file where the target class is located. The class loader will look for the specified target class from this path. The class must be the full path of the APK or jar. If multiple paths are to be included, the paths must be Separate using a specific separator, which can be obtained using System.getProperty("path.separtor"). The above "supports loading APK, DEX and JAR, and can also be loaded from SD card" refers to this path. What is finally done is to optimize the file ODEX on the dexPath path to the internal location optimizedDirectory, and then load it.

    • File optimizedDirectory, since the dex file is included in the APK or Jar file, the dex file needs to be decompressed from the APK or Jar file before loading the target class. This parameter is to formulate the path where the decompressed dex file is stored. This is also the process of ODEX optimization of the dex in the apk according to the platform. In fact, APK is a program compression package, which contains dex files. ODEX optimization is to extract the executable program in the package and turn it into an ODEX file. Because you extract it, you do not need to decompress the program compression when the system is first started. The program of the package, one less decompression process. This will speed up the system startup. Why is it the first time? It is because the DEX version only decompresses the executable program to the /data/dalvik-cache (for PathClassLoader) or optimizedDirectory (for DexClassLoader) directory for the first time, and then directly reads the dex file in the directory, so the second startup It's almost like normal. Of course, this is just a simple understanding, and the actually generated ODEX still has a certain optimization effect. ClassLoader can only load dex files in the internal storage path, so this path must be an internal path.

    • libraryPath, refers to the path where the C/C++ library used in the target class is stored

    • ClassLoader parent refers to the parent loader of the loader, which is generally the loader of the currently executing class. For example, in Android, context.getClassLoader() is used as the parent loader.

It can be seen that DexClassLoader can load apk/dex/jar of any path (reflected in DexPathList) PathClassLoader can only load apk in /data/app, that is, apk that has been installed in the phone. This is also the reason why PathClassLoader is used as the default class loader, because the general program is installed, and when it is opened, PathClassLoader loads the specified apk (decompressed into dex, and then optimized into odex).

  • DexPathList.java

    This class has an Element[] array, which stores all the dex files. The main code is as follows:

     public DexPathList(ClassLoader definingContext, String dexPath,
             String libraryPath, File optimizedDirectory) {
         if (definingContext == null) {
             throw new NullPointerException("definingContext == null");
         }
    
         if (dexPath == null) {
             throw new NullPointerException("dexPath == null");
         }
    
         if (optimizedDirectory != null) {
             if (!optimizedDirectory.exists())  {
                 throw new IllegalArgumentException(
                         "optimizedDirectory doesn't exist: "
                         + optimizedDirectory);
             }
    
             if (!(optimizedDirectory.canRead()
                             && optimizedDirectory.canWrite())) {
                 throw new IllegalArgumentException(
                         "optimizedDirectory not readable/writable: "
                         + optimizedDirectory);
             }
         }
    
         this.definingContext = definingContext;
         ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
         this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);
         if (suppressedExceptions.size() > 0) {
             this.dexElementsSuppressedExceptions =
                 suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
         } else {
             dexElementsSuppressedExceptions = null;
         }
         this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
     }
    
     //该构造器中会调用
    
     /**
      * Makes an array of dex/resource path elements, one per element of
      * the given array.
      */
     private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                              ArrayList<IOException> suppressedExceptions) {
         ArrayList<Element> elements = new ArrayList<Element>();
         /*
          * Open all files and load the (direct or contained) dex files
          * up front.
          */
         for (File file : files) {
             File zip = null;
             DexFile dex = null;
             String name = file.getName();
    
             if (file.isDirectory()) {
                 // We support directories for looking up resources.
                 // This is only useful for running libcore tests.
                 elements.add(new Element(file, true, null, null));
             } else if (file.isFile()){
                 if (name.endsWith(DEX_SUFFIX)) {
                     // Raw dex file (not inside a zip/jar).
                     try {
                         dex = loadDexFile(file, optimizedDirectory);
                     } catch (IOException ex) {
                         System.logE("Unable to load dex file: " + file, ex);
                     }
                 } else {
                     zip = file;
    
                     try {
                         dex = loadDexFile(file, optimizedDirectory);
                     } 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);
                     }
                 }
             } else {
                 System.logW("ClassLoader referenced unknown path: " + file);
             }
    
             if ((zip != null) || (dex != null)) {
                 elements.add(new Element(file, false, zip, dex));
             }
         }
    
         return elements.toArray(new Element[elements.size()]);
     }
    
     //然后去loadDexFile
    
     private static DexFile loadDexFile(File file, File optimizedDirectory)
         throws IOException {
         if (optimizedDirectory == null) {
             return new DexFile(file);
         } else {
             String optimizedPath = optimizedPathFor(file, optimizedDirectory);
             return DexFile.loadDex(file.getPath(), optimizedPath, 0);
         }
     }
    

    It can be seen that when optimizedDirectory==null, a DexFile is instantiated, that is to say, a new DexFile() is finally used when loading through PathClassLoader, and when DexClassLoader is used, a static method of DexFile is called to load.

  • DexFile.java

     public DexFile(String fileName) throws IOException {
         mCookie = openDexFile(fileName, null, 0);
         mFileName = fileName;
         guard.open("close");
         //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
     }
    
     private DexFile(String sourceName, String outputName, int flags) throws IOException {
         if (outputName != null) {
             try {
                 String parent = new File(outputName).getParent();
                 if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                     throw new IllegalArgumentException("Optimized data directory " + parent
                             + " is not owned by the current user. Shared storage cannot protect"
                             + " your application from code injection attacks.");
                 }
             } catch (ErrnoException ignored) {
                 // assume we'll fail with a more contextual error later
             }
         }
    
         mCookie = openDexFile(sourceName, outputName, flags);
         mFileName = sourceName;
         guard.open("close");
         //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
     }
    
     static public DexFile loadDex(String sourcePathName, String outputPathName,
         int flags) throws IOException {  
         return new DexFile(sourcePathName, outputPathName, flags);
     }
    
     public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
         return defineClass(name, loader, mCookie, suppressed);
     }
    
     private static Class defineClass(String name, ClassLoader loader, long cookie,
                                      List<Throwable> suppressed) {
         Class result = null;
         try {
             result = defineClassNative(name, loader, cookie);
         } catch (NoClassDefFoundError e) {
             if (suppressed != null) {
                 suppressed.add(e);
             }
         } catch (ClassNotFoundException e) {
             if (suppressed != null) {
                 suppressed.add(e);
             }
         }
         return result;
     }
    
     private static native Class defineClassNative(String name, ClassLoader loader, long cookie)
     throws ClassNotFoundException, NoClassDefFoundError;
    

You can see that when the static method goes to loadDex, it also goes to new a DexFile

The process of loading a class

Let's sort out the whole process now

  • 1. When DexClassLoader|PathClassLoader loads a class, it will first look for the loadClass() method of ClassLoader
  • 2. When it is checked that the class is not loaded, the method calls the findClass() method, which is overloaded by BaseDexClassLoader
  • 3. This method calls the findClass() method of DexPathList, traverses the entire Elements array in this iteration, gets the DexFile object and calls loadClassBinaryName to get the Class instance
  • 4. The final call is the native layer defineClassNative()

Note: The above source code is in Android 5.1

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325546207&siteId=291194637