Android ClassLoader 源碼閱讀

Java 中的 ClassLoader 回顧

  • Bootstrap ClassLoader :加載虛擬機指定的 class 文件
  • Extension ClassLoader:加載虛擬機指定的 class 文件
  • App ClassLoader :加載應用中的class 文件
  • Custom ClassLoader :通過自定義 ClassLoader 加載自己指定的 clas

​ 類加載過程:加載,驗證,準備,解析,初始化

Android 中的 ClassLoader 作用詳解

Android 中 ClassLoader 的種類

  • PathClassLoader:加載我們已經安裝到系統中的 apk 中的class 文件
  • DexClassLoader:加載指定目錄中的字節碼文件
  • BootClassLoader:主要用來加載 framework 層的字節碼文件,繼承自 ClassLoader
  • BaseDexClassLoader:是一個父類,前兩個 ClassLoader 都是這個類的子類

我們可以通過一段代碼來查找一個最簡單的程序中用到了那幾個 ClassLoader

ClassLoader loader = getClassLoader();
        if (loader != null) {
            Log.e(TAG, "ClassLoader: " + loader.toString());
            //獲取父 ClassLoader
            while (loader.getParent() != null) {
                loader = loader.getParent();
                Log.e(TAG, "Parent ClassLoader: " + loader.toString());
            }
        }
2019-11-12 10:21:10.306 4046-4046/? E/MainActivity: ClassLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.testdemo.www.classloader-mNctun4pxQMM04Hsv0AU2A==/base.apk"],nativeLibraryDirectories=[/data/app/com.testdemo.www.classloader-mNctun4pxQMM04Hsv0AU2A==/lib/arm64, /system/lib64, /system/vendor/lib64]]]
2019-11-12 10:21:10.306 4046-4046/? E/MainActivity: Parent ClassLoader: java.lang.BootClassLoader@eea1b40

​ 從上面我們可以看出一共使用了 PathClassLoader 和 BootClassLoader。這兩個也是我們程序運行必須要有的兩個 ClassLoader

Android 中ClassLoader 的特點

​ 使用雙親代理模型的特定來加載 class

​ 雙親代理模型的特點:在加載一個 class 的時候會詢問當前的 ClassLoader 是否加載過該 calss ,如果已經加載過,就直接返回。如果沒有加載過,就會尋找當前 ClassLoader 的 Parent 進行加載,相應的 parent 也會判斷是否加載過該 calss,如果加載過則進行返回,否則繼續向上尋找。如果整個繼承路中都沒有加載過該 class, 則就會從最高層開始調用findClass從指定的路徑進行加載,如果路徑中沒有,則由子類繼續執行此過程。這樣的好處就是,一個 class 只要被加載過一次,那麼在系統以後的整個生命週期中都不會加載這個 class,大大提高了加載類的效率

​ 類加載的共享功能:如果 class 被加載過,後面如果需要用到這個 class,則不會在進行加載,而是可以直接進行使用。

​ 類加載的隔離功能:不同繼承路線上的 ClassLoader 加載的肯定不是同一個類。好處是,避免用戶自己去些一些代碼去冒充核心的類庫來訪問我們核心類庫中的成員變量。舉個例子:我們系統的類會在初始化的時候加載,比如 String等。這些類是在應用程序啓動之前被我們系統加載好的。如果自定義一個String 類來將 String替換掉的話會存在很大的安全問題,現在他會使用自定義的 ClassLoader 來加載自定義的 String。但是他不會成功。 具體就是因爲針對java.*開頭的類,jvm的實現中已經保證了必須由bootstrp來加載。

​ 驗證多個類是同一個類的條件

  • 相同的 ClassName
  • 相同的 packageName
  • 被相同的 ClassLoader 加載

ClassLoader 源碼講解

看一下重點方法

public Class<?> loadClass(String name) throws ClassNotFoundException {
       return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 首先檢查類是否被加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                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
                }
				// 如果沒有沒有加載,就調用 findClass
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
 }
//查找類,這個方法沒有做任何實現,很明顯是要子類來實現的
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

下面我們看一下它的實現類

//這個 ClassLoader 用於加載 jar 包 或者 apk 中的 class 文件,可以加載並沒有安裝到系統的應用中的類
//所以才說 DexClassLoader 纔是動態加載的核心
public class DexClassLoader extends BaseDexClassLoader {
	
    //dex 文件路徑
    //解壓的dex文件存儲路徑,這個路徑必須是一個內部存儲路徑,一般情況下使用當前應用程序的私有路徑:/data/data/<Package Name>/...
    //包含 C/C++ 庫的路徑集合,多個路徑用文件分隔符分隔分割,可以爲null。
    //父加載器
    public DexClassLoader(String dexPath, String optimizedDirectory,
           String librarySearchPath, ClassLoader parent) {
        // 可看到 optimizedDirectory 參數並沒有被使用,這個參數在 26中被廢棄了,
        super(dexPath, null, librarySearchPath, parent);
    }
}

DexClassLoader 繼承自 BaseDexClassLoader ,方法實現都在 BaseDexClassLoader 中

public class PathClassLoader extends BaseDexClassLoader {
  // dex路徑,
  // 父加載器  
  public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
  }   
  
  public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
  }  
}    

既然 optimizedDirectory 參數並沒有被使用,這個參數在 26中被廢棄了,那這樣 DexClassLoader 和 PathClassLoader 好像就沒有多大區別的吧(個人感覺)

PathClassLoader 繼承自 BaseDexClassLoader ,方法的實現都在 BaseDexClassLoader 中。

這兩個類其實沒有任何的作用,唯一一個的作用就是: BaseDexClassLoader DexClassLoader可以加載dex、apk、jar、zip等格式的插件, PathClassLoader 只能加載已安裝的APK ,當然這是針對 26 以前而言的,至於爲什麼這樣說呢,等一會我們就會看到。

他們正在的行爲都是在父類 BaseDexClassLoader 中完成的。接着,我們就看一下 BaseDexClassLoader 是如何來查找 dex 的


public class BaseDexClassLoader extends ClassLoader {
     // 重要成員變量,
	 private final DexPathList pathList;
    
     public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent, boolean isTrusted) {
        super(parent);
         //初始化 pathList ,傳入當前對象,dex 文件路徑
         //從構造參數中可以看到 optimizedDirectory 並沒有被使用,這個參數在 API 26以後被棄用了
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

        if (reporter != null) {
            reportClassLoaderChain();
        }
    }
    
    
    //核心方法 
	@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;
    }    
}

​ findClass 中的第三行,調用了 pathList 的 findClass,傳入了要查找的類名,也就是說當前的方法也不是真正的查找方法,只是一箇中轉而已。

​ 下面我們來看一下 pathList 的 findClass 是如何查找的


final class DexPathList {
	
    //後綴名
	private static final String DEX_SUFFIX = ".dex";
	//從構造方法中傳入的 ClassLoader
    private final ClassLoader definingContext;
	// DexPathList 的內部類,等一下在說他
    private Element[] dexElements;
    
    //構造方法
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
		......
            
        //初始化    
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //1, 將dexPath保存爲BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
		
    }
}
//切割路徑
private static List<File> splitDexPath(String path) {
        return splitPaths(path, false);
}

​ 首先是 DEX_SUFFIX ,這是一個常量,保存的就是後綴名,然後接着是 dexElements

​ 我們首先看一個靜態內部類, Element 就是 dexElements 對象數組存儲的具體靜態內部類 。

static class Element {
        private final File path;
        private final DexFile dexFile;
        private ClassPathURLStreamHandler urlHandler;
        private boolean initialized;
        
        public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
                this.path = dir;
                this.dexFile = null;
                this.path = zip;
                this.dexFile = dexFile;
            }
        } 
}

然後我們看一下1, makeDexElements ,這裏纔是真正的實現

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;
     	//遍歷所有的 file 文件
      for (File file : files) {
          //判斷是否爲 目錄
          if (file.isDirectory()) {
              //如果是 目錄,則繼續進行遞歸
              elements[elementsPos++] = new Element(file);
           //是否爲 文件   
          } else if (file.isFile()) {
              String name = file.getName();
              DexFile dex = null;
              //是否爲 .dex 文件
              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      //如果是 dex 文件,則加載這個文件
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          //將dex 文件保存,注意第二個參數傳的底 null 
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  //如果不是目錄且不是 .dex 結尾的,那麼他可能是 jar。加載這個文件
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);          
                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      // 保存dex 文件 和 當前的 file,這種情況下可能是 jar,具體可以看這個構造方法
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
             
          }
      }
     ......
      return elements;
}

​ 上面對 file 文件進行了遍歷,1,首先判斷了是否爲目錄,如果是,則將目錄進行保存。2,判斷如果是 dex 文件 則 如果是則通過 loadDexFile 加載,3,如果不是目錄且不是 .dex 結尾的,則繼續加載這個文件,接着判斷這個文件是否爲 dex,如果不是,保存 file ,如果是 保存 dex 和 file 。

​ 下面 看一下 loadDexFile 方法:

   private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
}

​ 如果 optimizedDirectory 爲 null ,說明該 file 就是 dex 文件,那麼直接創建 DexFile 對象即可。

所以說 makeDexElements 方法就是 通過傳入的 files 獲取真正的 dex 文件。makeDexElements 方法執行完後,幹了什麼呢?我們接着往下看:

從 BaseDexClassLoader 的構造方法中創建了 DexPathList對象。 在 DexPathList 的構造方法中 通過調用 makeDexElements 方法獲取到 dex 文件,並轉化成了 Element 數組,在 BaseDexClassLoader 的 findClass 中 調用了 pathList.findClass ,並且傳入了要查找的 class 名字,那麼我們來看一下 DexPathList 的 findClass 方法:

//name:要加載的類的名字
public Class<?> findClass(String name, List<Throwable> suppressed) {
    //遍歷 Element 數組
        for (Element element : dexElements) {
           // 調用 element 的 findClass 獲取class 對象
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            //不爲空直接返回
            if (clazz != null) {
                return clazz;
            }
        }
		......
        return null;
}

這裏最終會調用到底層的一個方法:

private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)

這個方法我們看不了,但是可以推斷一下:dex 文件中包含了整個項目的 class 文件,所以在這裏,他應該是通過我們傳入的 name,在 dex 文件中查找相關的數據,然後拼裝成一個 class 最後返回給我們。

optimizedDirectory 不爲空的情況:通過查看了 DexClassLoader 和 PathClassLoader 後我們知道 optimizedDirectory 這個參數傳的是 null。所以上面的 else 是不會執行的。但是我們還是看一下這裏吧:

 if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
            
         //如果不爲 null    
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }

​ DexFile.LoadDex

static DexFile loadDex(String sourcePathName, String outputPathName,
        int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {

        return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
    }



private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
            DexPathList.Element[] elements) 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, loader, elements);
        mInternalCookie = mCookie;
        mFileName = sourceName;
        //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}

可以看到這裏最終調用的是 openDexFile:

 private static Object openDexFile(String sourceName, String outputName, int flags,
            ClassLoader loader, DexPathList.Element[] elements) throws IOException {
        // Use absolute paths to enable the use of relative paths when testing on host.
        return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                 (outputName == null)
                                     ? null
                                     : new File(outputName).getAbsolutePath(),
                                 flags,
                                 loader,
                                 elements);
 }
  public static native boolean isDexOptNeeded(String fileName)
            throws FileNotFoundException, IOException;

​ 到這裏就結束了,當前有興趣的看以看一下這篇文章,對 DexClassLoader 和 PathClassLoader 講的比較詳細,


參考:慕課網視頻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章