Android ClassLoader 解析

類加載器

前言

Android的Dalvik/ART虛擬機如同標準JAVA的JVM虛擬機同樣,在運行程序時首先須要將對應的類加載到內存中。所以,咱們能夠利用這一點,在程序運行時手動加載Class,從而達到代碼動態加載可執行文件的目的。Dalvik/ART虛擬機雖然與JVM虛擬機不同,ClassLoader具體的加載細節不同,可是工做機制是相似的,也就是說在Android中一樣能夠採用相似的動態加載插件的功能.java

ClassLoader分類

  • 下面咱們簡單寫一個Demo來看下ClassLoader有哪些 源碼以下:android

    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以下:c++

    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
  • 由上知 類加載器有以下幾種git

    • 1.BootClassLoader數組

      能夠看出View 和Context 都是由該ClassLoader 加載的,系統類加載器cookie

      由源碼得知 該類也是繼承了ClassLoaderapp

    • 2.默認類加載器是PathClassLoader,同時能夠看到加載的apk路徑,libPath(通常包括/vendor/lib和/system/lib)ide

    • 3.系統類加載器 注意區分系統類的加載器優化

      系統類加載器其實仍是PathClassLoader,只是加載的apk路徑不是/data/app/xxx.apk了,而是系統apk的路徑:/system/app/xxx.apkui

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

相關文章
相關標籤/搜索