Android 之ClassLoader 全解析

类加载器

前言

Android的Dalvik/ART虚拟机如同标准JAVA的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此,我们可以利用这一点,在程序运行时手动加载Class,从而达到代码动态加载可执行文件的目的。Dalvik/ART虚拟机虽然与JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的,也就是说在Android中同样可以采用类似的动态加载插件的功能.

ClassLoader分类

  • 下面我们简单写一个Demo来看下ClassLoader有哪些 源码如下:

     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如下:

      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
    
  • 由上知 类加载器有如下几种

    • 1.BootClassLoader

      可以看出View 和Context 都是由该ClassLoader 加载的,系统类加载器

      由源码得知 该类也是继承了ClassLoader

    • 2.默认类加载器是PathClassLoader,同时可以看到加载的apk路径,libPath(一般包括/vendor/lib和/system/lib)

    • 3.系统类加载器 注意区分系统类的加载器

      系统类加载器其实还是PathClassLoader,只是加载的apk路径不是/data/app/xxx.apk了,而是系统apk的路径:/system/app/xxx.apk

    • 4.从上面看出,系统加载器和默认加载器的委派关系:其基类都是BootClassLoader

ClassLoader源码解析

  • UML 图

    ClassLoader_UML

  • ClassLoader.java 源码

    从源码可以看出ClassLoader主要就是传入一个父构造器,而且一般父构造器不能为空,不像java虚拟机里父构造器为空时默认的父构造器为Bootstrap ClassLoader。Android中默认无父构造器传入的情况下,默认父构造器为一个PathClassLoader且此PathClassLoader父构造器为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 ClassLoader进行加载,加载不成功才在自己的findClass中进行加载。

    这和java虚拟机中常见的双亲委派模型一致的,这种模型并不是一个强制性的约束模型,比如你可以继承ClassLoader复写loadCalss方法来破坏这种模型,只不过双亲委派模是一种被推荐的实现类加载器的方式,而且jdk1.2以后已经不提倡用户在覆盖loadClass方法,而应该把自己的类加载逻辑写到findClass中。

    • BootClassLoader

      和java虚拟机中不同的是BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。

  • 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: 我这将一些乱七八糟的注释已经去掉了,这样看起来比较明显一些。其实就是对器基类BaseDexClassLoader进行一个简单的封装,真正的处理逻辑就是在BaseDexClassLoader里面

  • BaseDexClassLoader.java

    上面可以看到 DexClassLoader 和 PathClassLoader 都是对该类进行了一个封装

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

    可以看到 该类 实现了ClassLoader 并且重写了findClass() 方法, 先解释一下,构造器里面四个参数的含义

    • dexPath,指目标类所在的APK或jar文件的路径,类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径.如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)获得。上面"支持加载APK、DEX和JAR,也可以从SD卡进行加载"指的就是这个路径,最终做的是将dexPath路径上的文件ODEX优化到内部位置optimizedDirectory,然后,再进行加载的。

    • File optimizedDirectory,由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。这也是对apk中dex根据平台进行ODEX优化的过程。其实APK是一个程序压缩包,里面包含dex文件,ODEX优化就是把包里面的执行程序提取出来,就变成ODEX文件,因为你提取出来了,系统第一次启动的时候就不用去解压程序压缩包的程序,少了一个解压的过程。这样的话系统启动就加快了。为什么说是第一次呢?是因为DEX版本的也只有第一次会解压执行程序到 /data/dalvik-cache(针对PathClassLoader)或者optimizedDirectory(针对DexClassLoader)目录,之后也是直接读取目录下的的dex文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际生成的ODEX还有一定的优化作用。ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径。

    • libraryPath,指目标类中所使用的C/C++库存放的路径

    • ClassLoader parent,是指该装载器的父装载器,一般为当前执行类的装载器,例如在Android中以context.getClassLoader()作为父装载器。

由此可见,DexClassLoader可以加载任何路径的apk/dex/jar(DexPathList中体现) PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。这个也是PathClassLoader作为默认的类加载器的原因,因为一般程序都是安装了,在打开,这时候PathClassLoader就去加载指定的apk(解压成dex,然后在优化成odex)就可以了。

  • DexPathList.java

    该类有一个Element[] 数组,该类存放所有的dex文件 主要代码如下:

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

    这可以看到当optimizedDirectory==null时,去实例化一个DexFile,也就是说通过PathClassLoader去加载时最终去new 一个DexFile(),而当使用DexClassLoader 时,会去调用DexFile的一个static 方法去加载.

  • 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;
    

可以看到静态方法去loadDex时候,也是去new 一个DexFile

加载类的过程

我们现在梳理一下整个过程

  • 1.DexClassLoader|PathClassLoader 去加载类的时候首先会去找ClassLoader 的loadClass() 方法
  • 2.当检查到该类没有加载之后,该方法调用了findClass()方法,而BaseDexClassLoader重载了这个方法
  • 3.该方法又调用了DexPathList的findClass() 方法,在该反复里面去遍历整个Elements 数组,拿到DexFile对象调用loadClassBinaryName,得到Class 实例
  • 4.最终调用的是native层 defineClassNative()

Note: 以上源码在Android 5.1中

猜你喜欢

转载自my.oschina.net/u/1433837/blog/1186926