Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android中ClassLoader源碼解析之真的是你認為的ClassLoader

Android中ClassLoader源碼解析之真的是你認為的ClassLoader

編輯:關於Android編程

1.前言

首先,閱讀本文章之前,需要了解java中的ClassLoader的基本原理,包括java中的三級ClassLoader機制以及ClassLoader的委托機制,否則下面的內容會不知道在講什麼。雖然Android中的ClassLoader也是遵循其委托機制,但是他沒有遵循java的三級ClassLoader機制,而是自己造了一個,修改了java系統的代碼,如果將兩者混淆的話,在Android中使用ClassLoader的時候,你可能會遇到想不通的問題。(因為作者曾經就踩過坑。。。。。。)   首先,我們看下面一段代碼,在Application的onCreate()中添加如下代碼:
PathClassLoader classLoader = (PathClassLoader) getApplicationContext().getClassLoader();
Log.d("mytest", "classLoader : " + classLoader + "\n" +
            "parent : " + classLoader.getParent() + "\n" +
            "grandParent : " + classLoader.getParent().getParent() + "\n" +
            "system classloader : " + ClassLoader.getSystemClassLoader() + "\n" +
            "system parent : " + ClassLoader.getSystemClassLoader().getParent());
  代碼的執行結果,打印內容如下:
 classLoader : dalvik.system.PathClassLoader[dexPath=/data/app/com.gavin.demo2application-1.apk,libraryPath=/data/app-lib/com.gavin.demo2application-1]
 parent : java.lang.BootClassLoader@41099128
 grandParent : null
 system classloader : dalvik.system.PathClassLoader[dexPath=.,libraryPath=null]
 system parent : java.lang.BootClassLoader@41099128
  看到上面的打印的內容,我們了解到在Android的項目中使用的ClassLoader是其自定義的PathClassLoader,最重要的一點是打印的dexPath,這個決定了在項目代碼中要加載的類的位置(後面詳細講解)。 第二點奇怪的地方就是它的parent是 BootClassLoader,這個又是什麼東西,後面詳細講解。 第三點調用ClassLoader.getSystemClassLoader()返回的是PathClassLoader,並且dexPath為. ,我們了解的java中的ClassLoader.getSystemClassLoader()返回的是加載classpath裡面的class的ClassLoader,也就是java中的第三級ClassLoader,它是調用sun.misc.Launcher的getClassLoader()方法獲取的,詳細解析請自行查閱。 第四點它的parent也是BootClassLoader,看來這個必須要分析一下下,畢竟出鏡率這麼高。

2.Context.getClassLoader()返回的是PathClassLoader

首先,Android中可以使用的CLassLoader有PathClassLoader和DexClassLoader,PathClassLoader只能加載dex文件,我們安裝apk之後會在/data/dalvik-cache目錄下生產一個名為data@[email protected]@classes.dex的 ODEX 文件,而PathClassLoader要加載apk的時候會到這個文件夾下找對應的dex文件。(ODEX文件就是經過優化的dex文件,詳細自行查閱),同時也是我們自己編寫的項目中使用的ClassLoader。而DexClassLoader可以加載apk,dex,jar文件,就是被用來實現動態加載機制,加載一個外部的apk文件,實現完全解耦的模塊式開發,現在的開源框架有DL(使用代理的方式,其實加載的不是插件中的類)和DroinPlugin(hook掉AMS和PMS實現);現在比較火的熱修改也是和其有關系,比如AndFix(它是在運行時將java方法修改成native方法,然後修改調用這個方法的指針,指向修復的方法),nuwa(也就是qq空間實現基於dex分包,修改CLassLoader中的dexElements中的dex順序實現)以及最新的美團的Robust(基於Android Studio的instance run的原理,為每個類創建代理類)。 關於上面所提的動態加載框架,熱修復框架等等都會在後續的文章中進行分析。   上面說了那麼多,現在開始分析PathClassLoader和DexCLassLoader的源碼實現,他們都是繼承BaseDexClassLoader,所有的實現都瘋轉在了這個類裡面,先來看看PathClassLoader和DexClassLoader的源碼. PathClassLoader的代碼:
public class PathClassLoader extends BaseDexClassLoader {

37    public PathClassLoader(String dexPath, ClassLoader parent) {
38        super(dexPath, null, null, parent);
39    }
40

63    public PathClassLoader(String dexPath, String libraryPath,
64            ClassLoader parent) {
65        super(dexPath, null, libraryPath, parent);
66    }
67}
68
  DexClassLoader源碼如下:
public class DexClassLoader extends BaseDexClassLoader {

55    public DexClassLoader(String dexPath, String optimizedDirectory,
56            String libraryPath, ClassLoader parent) {
57        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
58    }
59}
可以看出他們僅僅是重寫了構造函數,所以所有的實現都是在BaseDexClassLoader裡面。
參數:dexPath:要加載的apk或者jar文件的路徑,optimizedDirectory:從apk中解析出dex文件存儲的路徑,libraryPath:apk文件中類要使用的c/c++代碼,parent:父裝載器,也就是真正loadclass的裝載器。   下面重點來了,分析BaseDexClassLoader的源碼,首先構造函數如下:
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
super()走的是ClassLoader的設置parent的ClassLoader的方式,重點是下面的PathList,他存儲的是dex的集合,因為apk是可以dex分包,它裡面含有一個DexElement的集合,每一個Element就對應一個dex文件。   DexPathList的構造函數的核心代碼如下:
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
makeDexElements()就是解析dex文件成對應的DexElement,代碼如下:
private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
                                            ArrayList suppressedExceptions) {
        ArrayList elements = new ArrayList();
209        /*
210         * Open all files and load the (direct or contained) dex files
211         * up front.
212         */
213        for (File file : files) {//遍歷所有的dex文件
214            File zip = null;
215            DexFile dex = null;//這是核心的類,處理將dex文件轉化成對應的DexFile對象
216            String name = file.getName();
217
218            if (name.endsWith(DEX_SUFFIX)) {//.dex結尾(針對PathClassLoader處理)
219                // Raw dex file (not inside a zip/jar).
220                try {
221                    dex = loadDexFile(file, optimizedDirectory);//核心方法,內部是調用的DexFile的loadDex()方法
222                } catch (IOException ex) {
223                    System.logE("Unable to load dex file: " + file, ex);
224                }
225            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
226                    || name.endsWith(ZIP_SUFFIX)) {//.dex .jar .apk 結尾 (針對DexClassLoader處理)
227                zip = file;
228
229                try {
230                    dex = loadDexFile(file, optimizedDirectory);
231                } catch (IOException suppressed) {
232                    /*
233                     * IOException might get thrown "legitimately" by the DexFile constructor if the
234                     * zip file turns out to be resource-only (that is, no classes.dex file in it).
235                     * Let dex == null and hang on to the exception to add to the tea-leaves for
236                     * when findClass returns null.
237                     */
238                    suppressedExceptions.add(suppressed);
239                }
240            } else if (file.isDirectory()) {
241                // We support directories for looking up resources.
242                // This is only useful for running libcore tests.
243                elements.add(new Element(file, true, null, null));//創建Element對象
244            } else {
245                System.logW("Unknown file type for: " + file);
246            }
247
248            if ((zip != null) || (dex != null)) {
249                elements.add(new Element(file, false, zip, dex));
250            }
251        }
252
253        return elements.toArray(new Element[elements.size()]);
254    }
上面的代碼是遍歷所有的dex文件,然後調用的DexFile的loadDex()方法,內部就是創建一個DexFile對象,這個構造函數中會調用openDexFile()解析dex文件,這是一個native()方法,使用c代碼實現的,這裡就不分析了,如果想要學習,推薦一篇博客:DexClassLoader源碼解析
關於DexFile,AndFix就是直接使用DexFile直接加載的dex文件,而沒有使用DexClassLoader,有興趣可以查看AndFix的源碼。  

3.Context的getClassLoader()為什麼返回是PathClassLoader,探索其中奧秘

我們都知道getApplicationContext()的返回的實現類是ContextImpl,下面來看看ContextImpl的getClassLoader()的代碼實現:
public ClassLoader getClassLoader() {
        return mPackageInfo != null ?
                mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
}
我們看到如果mPackageInfo不為null,就調用它的getClassLoader()方法,否則調用ClassLoader.getSystemClassLoader(),這裡我們看到了ClassLoader的getSystemClassLoader()方法,但是這裡還不是重點,重點是mPackageInfo這個對象,這是什麼呢,它是一個LoadedAPK對象。它又是什麼呢? 官方文檔說明如下:
Local state maintained about a currently loaded .apk.
LoaderAPK對象是apk在內存中的表示。通過這個LoaderApk對象可以拿到apk中代碼和資源,甚至裡面Activity和Service等信息。 那麼它又是哪裡創建的,又是什麼時候創建的呢,如果你了解Activity的啟動過程,你就明白了(如果想了解,推薦老羅的文章),這裡就不贅述了。在ActivityThread裡面有一個mPackages的map類型的成員變量,根據鍵值(packageName)存儲對應的LoadedApk對象。啟動Activity的時候要調用LoadedApk的getClassLoader(),來加載對應的Activity class文件,然後通過反射創建這個activity的實例;那麼獲取這個對象,會先去mPackages中去查找有沒有緩存,如果沒有就創建一個新的LoadedAPK對象。 下面代碼截取自ActivityThread中啟動Activity的過程中創建LoadedApk的代碼:
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
        CompatibilityInfo compatInfo) {
    return getPackageInfo(ai, compatInfo, null, false, true, false);
}
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
        // 獲取userid信息
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
    // 嘗試獲取緩存信息
        WeakReference ref;
        if (differentUser) {
            // Caching not supported across users
            ref = null;
        } else if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
                // 緩存沒有命中,直接new
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

        // 省略。。更新緩存
        return packageInfo;
    }
}
下面看上面使用到的LoadedApk的構造函數,其實LoadedApk還有一個構造函數,在ContextImpl創建自己的實例的同時創建其LoadedApk的成員變量的時候使用了。
108    public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
109            CompatibilityInfo compatInfo,
110            ActivityThread mainThread, ClassLoader baseLoader,
111            boolean securityViolation, boolean includeCode) {
	   .......(省略)
126
127        if (mAppDir == null) {
128            if (ActivityThread.mSystemContext == null) {//這個context很重要,一個ActivityThread只有這一個,是靜態全局的
129                ActivityThread.mSystemContext =
130                    ContextImpl.createSystemContext(mainThread);
131                ActivityThread.mSystemContext.getResources().updateConfiguration(
132                         mainThread.getConfiguration(),
133                         mainThread.getDisplayMetricsLocked(compatInfo, false),
134                         compatInfo);
135                //Slog.i(TAG, "Created system resources "
136                //        + mSystemContext.getResources() + ": "
137                //        + mSystemContext.getResources().getConfiguration());
138            }
139            mClassLoader = ActivityThread.mSystemContext.getClassLoader();//這個ClassLoader就是最後返回的那個ClassLoader
140            mResources = ActivityThread.mSystemContext.getResources();
141        }
142    }
143
看到這裡,我們只要最終這個CLassLoader的來源是從Context那裡後去的,也就是ActivityThread的mSystemContext裡面的ClassLoader,我們來看一下這個mSystemContext的創建過程,代碼如下:
1458    static ContextImpl createSystemContext(ActivityThread mainThread) {
1459        ContextImpl context = new ContextImpl();
1460        context.init(Resources.getSystem(), mainThread);//這個init操作也沒有創建他裡面LoadedApk成員變量
1461        return context;
1462    }
所以最終調用的代碼,就是最開始的ContextImpl的getClassLoader()方法,並且mPackageInfo(LoadedAPK對象)為null,所以最終調用的是ClassLoader.getSystemClassLoader(),所以最終結論就是系統ClassLoader是通過ClassLoader.getSystemClassLoader()創建。  

4.揭開ClassLoader.getSystemClassLoader()在Android中的神秘面紗

  其代碼如下:
public static ClassLoader getSystemClassLoader() {
     return SystemClassLoader.loader;
}
static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }
對於最有一個核心方法createSystemClassLoader(),官方說明如下:
Create the system class loader. Note this is NOT the bootstrap class
loader (which is managed by the VM). We use a null value for the parent
to indicate that the bootstrap loader is our parent.
創建系統的ClassLoader。注釋:這不是bootstrap ClassLoader(被虛擬機管理的ClassLoader)。我們使用null作為我們系統ClassLoader的parent來表明bootstrap就是我們的系統ClassLoader的parent。這裡也就是充分說明了Android系統不是使用的java原生的bootstrap來加載,而是使用自己的創建的套機制。取代bootstrap的是用null(bootstrap不是一個ClassLoader對象,所以他的子級ClassLoader調用getParent()返回的是null),而取代java中第二級的ClassLoader使用Android中創建的最基層的BootClassLoader(也就是上面的PathClassLoader的parent)。 這個BootClassLoader是單例的,所以全局只有一個,我們也可以得出,系統所有執行裝載類的操作,都會執行到這個對象。代碼如下:
public BootClassLoader() {
     super(null, true);//他的parent為null(模擬它的parent是bootstrap)
}
  還有我們看到上面PathClassLoader構造函數中傳遞的第一個參數classPath,這個就是我們之前打印出來的dexPath。為什麼系統的ClassLoader.getSystemClassLoader()返回的PathClassLoader 的dexPath指向的是當前apk的安裝路徑;但是我們自己創建的返回的PathClassLoader的dexPath卻是. 我們發現是由上面的System.getProperty("java.class.path",".");決定的,看到get操作,相對的也就會有set操作,那麼這個值是什麼時候設置的呢。這我們就要追溯到Android framwork中System中的initSystemProperty()方法中,調用VMRuntime.getRuntime().classPath()的值,這個值應該就是當前apk的安裝路徑。 代碼如下: 路徑:/libcore/luni/src/main/java/java/lang/System.java(4.0.4)
private static void initSystemProperties() {
        VMRuntime runtime = VMRuntime.getRuntime();
        Properties p = new Properties();

        String projectUrl = "http://www.android.com/";
        String projectName = "The Android Project";

        p.put("java.boot.class.path", runtime.bootClassPath());
        p.put("java.class.path", runtime.classPath());
}
但是,為什麼我們自己調用,獲取這個值就是. 我的猜想就是 系統創建完對應的ClassLoader之後,就將這個值修改成了.(當然這僅僅是我個人的猜想,希望了解的大神能幫我解答一下,因為我實在是找不到原因,困惑好久了)。  

5.總結

1.Android系統最頂級的ClassLoader是BootClassLoader(替代java中第二級的ext ClassLoader),而用來加載系統的類是使用的以這兒BootClassLoader為parent的PathClassLoader(替代java中第三級加載classpath的ClassLoader)。   2.Android系統的PathClassLoader的dexPath(要加載類的路徑),指向當前apk安裝的路徑,然後使用DexFile來解析對應的dex文件,裝載裡面的class   3.DexClassLoader,主要用於加載外部插件,也就是可以直接加載一個apk文件,現在的插件化動態加載機制,熱修復等都要使用到它的特性,當然直接使用裡面的DexFile直接加載dex文件也是可以(AndFix就是這樣做的)。   4.自己遇到的坑:我之前在學習動態化的時候,看完原理之後,自己創建一個DexClassLoader ,它的parent我設置的是系統PathClassLoader(也就是context.getClassLoader()),但是load外部apk中的類的時候,報錯:當前apk的安裝路徑下(也就是這個apk文件對應生成的ODEX文件)找不到對應的類,也就是ClassNotFoundException,但是自己不明白為什麼我用DexClassLoader加載的外部apk,但是它卻到系統中去尋找。現在明白了因為我傳遞的是PathClassLoader的dexPath是指向系統的apk的路徑的,自然會到那裡去找。但是如果傳遞的是ClassLoader.getSystemClassLoader(),因為dexPath是. ,是Directory,就不會創建DexFile,也就不會走DexFile的loadDexFile()方法,而是直接調用它的parent也就是BootClassLoader的findClass()來裝載當前類。   文章終於寫完了,鄙人愚鈍,水平有限,文章難免有錯,歡迎指出。
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved