Android插件化基礎(chǔ)1-----加載SD上APK

Android插件化基礎(chǔ)的主要內(nèi)容包括

本文是第一篇文章,主要是講解如何加載SD卡上的apk中的class

本文涉及的內(nèi)容如下:

  • 1.java的類加載與雙親委托
  • 2.android apk安裝簡述
  • 3.demo演示
  • 4.demo背后的故事----android的類加載流程 (重點)
  • 5.總結(jié)
  • 6.github地址

一、Java類加載介紹

先來復(fù)習(xí)下Java類加載的事情,對Java類加載很熟悉的朋友可以直接略過第一部分,直接從第二部分開始

(一)什么是ClassLoader:

我們寫完一個java程序后,通過編譯,形成若干個.class文件,而這些若干個.class文件組織成一個完成的java程序,當(dāng)程序運行時,都會調(diào)用一個入口函數(shù)來調(diào)用系統(tǒng)的相關(guān)功能,而這些功能都被封裝在不同的class文件當(dāng)中,所以經(jīng)常要從一個class文件調(diào)用到另外一個class文件的某個方法,如果另外一個class不存在,則會引發(fā)系統(tǒng)異常。而在程序啟動的時候,不會一次性加載程序所有的class文件,而是根據(jù)程序的需要,通過java類加載機制(ClassLoader)來動態(tài)加載某個class文件到內(nèi)存中,從而只有class被記載到內(nèi)存之后,才能被其他class所引用。所以ClassLoader就是用來動態(tài)加載class文件到內(nèi)存當(dāng)中用到的

(二)ClassLoader的作用:

  • 1 負責(zé)將Class加載到JVM中
  • 2 審查每個類由誰加載(父類優(yōu)先的等級加載機制)
  • 3 將Class字節(jié)碼重新解析成JVM統(tǒng)一要求的對象格式

(三)ClassLoader類的結(jié)構(gòu)分析

為了更好的理解類加載機制,我們來深入研究下ClassLoader和他的方法

ClassLoader 類是一個抽象類

public abstract class ClassLoader

/**  * A classloader isan object thatisresponsible forloading classes. The
* classClassLoader isan abstract class.  Given thebinary nameofa 
*class, a classloader should attempt to* locate orgenerate data that 
*constitutes a definition fortheclass.  A 
* typical strategy istotransform thenameintoa filenameandthenreada 
* "class file"ofthatnamefroma filesystem. **/

大致的意思是: ClassLoader 是一個負責(zé)加載classes的對象,ClassLoader類是一個抽象類,需要給出類的二進制名稱,ClassLoader嘗試定位或者產(chǎn)生一個class數(shù)據(jù),一個典型的策略是把二進制名字轉(zhuǎn)換成文件名然后到文件系統(tǒng)中找到該文件
以下是ClassLoader常用到的幾個方法及其重載方法:

  • 1 ClassLoader
  • 2 defineClass(byte[] ,int ,int )把字節(jié)數(shù)組b中的內(nèi)容轉(zhuǎn)換成Java類,返回* 的結(jié)果是java.lang.Class類的實例,這個方法被聲明為final的
  • 3 findClass(String name)查找名稱為name類,返回結(jié)果java.lang.Class類的實例
  • 4 loadClass(String name) 加載名稱為name的類,返回的結(jié)果是java.lang.Class類的實例
  • 5 resolveClass(Class<?>) 鏈接指定Java類

其中defineClass方法用來將byte字節(jié)流解析成JVM能夠識別的Class對象,有了這個方法意味著我們不僅僅可以通過class文件實例化對象,還可以通過其他方式實例化對象,如果我們通過網(wǎng)絡(luò)接受到一個類的字節(jié)碼,拿到這個字節(jié)碼流直接創(chuàng)建類的Class對象形式實例化對象。如果直接調(diào)用這個方法生成類的Class對象,這個對象Class對象還沒有resolve,這個resolve將會在這個對象真正實例化時才進行

(三)Java默認提供的三個ClassLoader

  • 1 BootStrap ClassLoader:稱為啟動類加載器,是java類加載層次中最高層次的類加載器,負責(zé)加載JDK中的核心類庫,如:rt.jar,resource.jar,charsets.jar等,可通過如下程序獲得該類加載器從哪些地方加載了相關(guān)jar或clas
  • 2 Extension ClassLoader:稱為擴展類加載器,負責(zé)加載java的擴展類苦苦,java虛擬機的實現(xiàn)會提供一個擴展目錄,該類加載器在此目錄里面查找并加載java類。默認加載JAVA_HOME/jre/lib/ext/目錄下的所有jar
  • 3 App ClassLoader:稱為系統(tǒng)類加載器,負責(zé)加載應(yīng)用程序classpath目錄下所有jar和class文件,一般來說,Java應(yīng)用的類都是由它們來完成加載的??梢酝ㄟ^ClassLoader.getSystemClassLoader()來獲取它。
    除了系統(tǒng)提供的類加載器以外,開發(fā)人員也可以通過集成java.lang.ClassLoader類的方式實現(xiàn)自己的類加載器,以滿足一些特殊的需求。

(四)ClassLoader加載類的原理----雙親委托模型:

1原理介紹:

ClassLoader使用的是雙親委托模型來搜索類的,每個ClassLoader實例都有一個父類加載器的引用(不是繼承關(guān)系,是一個包含的關(guān)系),虛擬機內(nèi)置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但是可以作用于其他ClassLoader實例的父類加載器。當(dāng)一個ClassLoader實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務(wù)委托給它的父類加載器,這個過程是由上到下依次檢查的,首先由最頂層的類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒有加載到,則把任務(wù)轉(zhuǎn)交給Extension ClassLoader試圖加載,如果也沒加載到,則轉(zhuǎn)交給app ClassLoader進行加載。如果他沒有家在得到的話,則返回給委托的發(fā)起者,由它到制定的文件系統(tǒng)或者網(wǎng)絡(luò)等URL加載該類,如果他們都沒有加載這個類,則票拋出ClassNotFoundException異常,否則將這個找到的類生成一個類的定義,并將它加載到內(nèi)存當(dāng)中,最后返回這個類在內(nèi)存中的Class實例對象。如下圖

class_image.png

2為什么要使用雙親委托這種模型?:

因為這樣可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時候,就沒有必要ClassLoader再加載一次了,考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態(tài)替代java核心api中定義的類型,這樣會存在非常大安全隱患,而雙親委托可以避免這種情況,因為String已經(jīng)在啟動的時候就被引導(dǎo)類加載器(Bootstrap ClassLoader)加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非你改變JDK中的ClassLoader的搜索類默認算法。

二.android apk安裝簡述

(一)android 打包簡述

Android應(yīng)用打包成apk時,class文件會被打包成一個或者多個dex文件,將一個apk文件后綴改成.zip格式后解壓;里面有class.dex文件,由于android64K方法數(shù)的問題,使用MultiDex就會生成多個dex文件。如下圖


image.png

當(dāng)Android 系統(tǒng)安裝包安裝一個應(yīng)用的時候,會針對不同的平臺對Dex進行優(yōu)化,這個過程由一個專門的工具來處理叫DexOpt。DexOpt是第一次加載Dex文件的時候執(zhí)行,該過程會生成一個ODEX文件,即Optimised Dex,執(zhí)行ODEX的效率會比直接執(zhí)行Dex文件的效率要高很多,加快App的啟動和響應(yīng)。

PS:

  • 1 odex優(yōu)化有什么用:
    ODEX的用途是分離程序資源和可執(zhí)行文件,達到快速軟件加載速度和開機速度的目的。
  • 2 棒棒糖與ART帶來的問題
    很多人會有疑問,Android5.0開始,默認已經(jīng)使用ART,棄用Dalvik了,應(yīng)用程序會在安裝時被編譯成OAT文件,(ART上運行的格式)ODEX還有什么用那?
    這里我們引用google的權(quán)威回答:
Dex file compilation uses a tool called dex2oat and takes more time than 
dexopt. The increase in time varies, but 2-3x increases in compile time 
are not unusual. For example, apps that typically take a second to install 
using dexopt might take 2-3 seconds.

這里解釋下:DEX轉(zhuǎn)換成OAT的這個過程是在用戶安裝程序或者刷入ROM,OTA更新后首次啟動時執(zhí)行的,按照google的說法,相比做過ODEX優(yōu)化,未做過優(yōu)化的DEX轉(zhuǎn)成成OAT要花費更長的時間,比如2-3倍。比如安裝一個odex優(yōu)化過的程序假設(shè)需要1秒鐘,未做過優(yōu)化的程序就需要2-3秒。由此次可見,雖然dalvik被棄用了,但是ODEX優(yōu)化在Android棒棒糖上依舊擁有顯著的優(yōu)化效果。首先ODEX優(yōu)化不僅僅只是針對應(yīng)用程序,還會對內(nèi)核鏡像,jar庫文件等進行優(yōu)化。其次,資源和可執(zhí)行文件分離帶來的性能提升無論是運行在ART還是Dalvik,都有效。

(二)android 安裝

下載好的Android apk, 在安裝過程中,其中文件內(nèi)容是這樣處理的:

  • 1 先把apk拷貝到/data/app下, 沒錯,就是完整的apk, 例如
    com.test.demo-2.apk
  • 2 解壓apk,把其中的classes.dex 拷貝到/data/dalvik-cache, 其命名規(guī)則是 apk路徑+classes.dex, 如: data/app/com.test.demo2.apk/classes.dex
  • 3 在/data/data下創(chuàng)建對應(yīng)的目錄,用于存儲程序的數(shù)據(jù),例如cache, database等, 目錄名稱與包名相同, 如com.test.demo.

要注意的是, 安裝過程并沒有把資源文件, assets目錄下文件拷貝出來,他們還在apk包里面呆著,所以,當(dāng)應(yīng)用要訪問資源的時候,其實是從apk包里讀取出來的。其過程是,首先加載apk里的resources(這個文件是存儲資源Id與值的映射文件),根據(jù)資源id讀取加載相應(yīng)的資源。

由于本文主要是講解android類加載,android apk安裝過程就不詳細描述了

三 Demo演示 :

(一)先看下demo目錄

項目.jpeg
  • 1 其中 dexclassloaderapp 是用來演示的運行的app
  • 2 AppTest 是用來打包成apk的項目

看下 AppTest 里面的目錄結(jié)構(gòu)

顯示.jpeg

分別看下 IDexTest,IDexTestImpl和string.xml

public interface IDexTest {

    String  getText();
}
public class IDexTestImpl implements IDexTest {
    @Override
    public String getText() {
        return "我是SD卡上的APK";
    }
}
<resources>
    <string name="app_name">AppTest</string>
    <string name="showtext">我是SD上的字符串</string>
</resources>

當(dāng)AppTest 被打包成apk的時候,我們要在 dexclassloaderapp 里面獲取這些數(shù)據(jù) ?,F(xiàn)在開始打包apk,名字為"AppTest-release.apk",然后把這個apk放到sd上。
現(xiàn)在run dexclassloaderapp 項目會出現(xiàn)下面的顯示

device-1.png

點擊 測試加載類 上面的textview會有原來的"類信息!"轉(zhuǎn)變"我是SD卡上的APK",證明已經(jīng)成功加載到SD卡上的apk
(ps:6.0手機注意權(quán)限,有的手機沒有開通權(quán)限會報找不到類)

類加載.gif

四demo背后的故事----android的類加載流程:

先看下 dexclassLoaderapp 是如何實現(xiàn)在家外部APK class的
具體實現(xiàn)是在load方法里面

    private void load() {
        // 獲取到包含 class.dex 的 jar 包文件
        final File apkFile =
                new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "apptest-release.apk");

        if (!apkFile.exists()) {

            Log.e("LGC", "文件不存在");
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    pd.dismiss();
                    Toast.makeText(MainActivity.this, "文件不存在", Toast.LENGTH_LONG);

                }
            });
            return;
        }

        if (!apkFile.canRead()) {
            // 如果沒有讀權(quán)限,確定你在 AndroidManifest 中是否聲明了讀寫權(quán)限
            // 如果是6.0以上手機要查看手機的權(quán)限管理,你的這個app是否具有讀寫權(quán)限
            Log.d("LGC", "apkFile.canRead()= " + apkFile.canRead());
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    pd.dismiss();
                    Toast.makeText(MainActivity.this, "沒有讀寫權(quán)限", Toast.LENGTH_LONG);

                }
            });
            return;
        }


        // getCodeCacheDir() 方法在 API 21 才能使用,實際測試替換成 getExternalCacheDir() 等也是可以的
        // 只要有讀寫權(quán)限的路徑均可
        Log.i("LGC", "getExternalCacheDir().getAbsolutePath()=" + getExternalCacheDir().getAbsolutePath());
        Log.i("LGC", "apkFile.getAbsolutePath()=" + apkFile.getAbsolutePath());

        try {
            DexFile dx = DexFile.loadDex(apkFile.getAbsolutePath(), File.createTempFile("opt", "dex", getApplicationContext().getCacheDir()).getPath(), 0);

            // Print all classes in the DexFile
            for (Enumeration<String> classNames = dx.entries(); classNames.hasMoreElements(); ) {
                String className = classNames.nextElement();
                if (className.equals("com.yibao.test.IDexTestImpl")) {
                    Log.d("LGC", "#########################################################" + className);
                    Log.d("LGC", className);
                    Log.d("LGC", "#########################################################" + className);


                }
                Log.d("LGC", "Analyzing dex content, fonud class: " + className);
            }
        } catch (IOException e) {
            Log.d("LGC", "Error opening " + apkFile.getAbsolutePath(), e);
        }
        DexClassLoader dexClassLoader =
                new DexClassLoader(apkFile.getAbsolutePath(), getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
        try {
            // 加載 com.test.IDexTestImpl 類
            Class clazz = dexClassLoader.loadClass("com.test.IDexTestImpl");

            Object dexTest = clazz.newInstance();

            Method getText = clazz.getMethod("getText");

            final String result = getText.invoke(dexTest).toString();
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    pd.dismiss();
                    if (!TextUtils.isEmpty(result)) {
                        tv.setText(result);
                    }

                }
            });
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }

大體的流程是先new 一個DexFile,然后loadClass就獲取了這個SD卡上的apk資源了,為什么可以這樣那?

因為Android Framework提供了DexClassLoader這個類,簡化了『通過一個類的全限定名獲取描述次類的二進制字節(jié)流』這個過程;我們只需要告訴DexClassLoader一個dex文件或者apk文件的路徑就能完成類的加載。
詳細敘述請看下面

Android的Dalvik/ART虛擬機如果標(biāo)準(zhǔn)Java的JVM虛擬機一樣,在運行程序時首先需要將對應(yīng)的類加載到內(nèi)存中。因此我們可以利用這一點,在程序運行時手動加載Class,從而達到代碼動態(tài)加載可執(zhí)行文建的目的。Android的Dalvik/ART虛擬機雖然與標(biāo)準(zhǔn)java的JVM不一樣,所以ClassLoader具體的加載細節(jié)不一樣,但是工作機制是類似的,也就是說在Android中同樣可以采用類似動態(tài)加載插件的功能,只是在Android應(yīng)用中動態(tài)加載一個插件的工作要比java復(fù)雜的多。

(一)android 類加載設(shè)計圖

結(jié)構(gòu)圖.jpg

(二)android類加載 類圖

image1.png

SecureClassLoader的子類是URLClassLoader,其只能用來加載jar文件,在android的Dal/ART上是沒法使用的,這里就不過多的介紹了!

classloader.jpg

這里面有兩個重要的類
PathClassLoader和DexClassLoader他們分別繼承BaseDexClassLoader,那他們的區(qū)別是什么?那么看下他們的構(gòu)造函數(shù)

PathDexClassLoader

public  class PathClassLoader{

    public  PathClassLoader(String dexPath, ClassLoader parent) {
      super(dexPath, null, null, parent);
   }

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

DexClassLoader

public class  DexClassLoader   extends BaseDexClassLoader {

       public   DexClassLoader (String dexPath , String  optimizedDirectory,String libraryPath, ClassLoader parent) {
          super(dexPath, new File(optimizedDirectory),  libraryPath,  parent);
  }
}

他們都是調(diào)用父類BaseDexClassLoader的構(gòu)造函數(shù)
BaseDexClassLoader

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent); this.originalPath = dexPath;
        this.originalLibraryPath = libraryPath; 
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); 
}

這四個參數(shù)分別的含義:

  • 1 String dexPath
    包含class.dex的apk、jar文件路徑,多個用文件分隔符(默認是:)分隔
  • 2 String optimizedDirectory
    用來緩存優(yōu)化的dex文件的路徑,即從apk或者jar文件中提取出來的dex文件。該路徑不可以為空,且應(yīng)該是應(yīng)用私有,有讀寫權(quán)限(實際上也可以使用外部存儲空間,但是這樣的話,有代碼注入風(fēng)險),可以通過方式來穿件一個這樣的路徑.
  • 3 String libraryPath
    存儲C/C++庫文件的路徑集
  • 4 ClassLoader parent
    父類加載器,遵從雙親委托模型
很明顯,對比PathClassLoader只能加載已經(jīng)安裝應(yīng)用的dex或者Apk文件,DexClassLoader則沒有這個限制,可以從SD卡上加載包含class.dex的jar和.apk文件,也是插件化和熱修復(fù)的基礎(chǔ),在不需要安裝應(yīng)用的情況下,完成需要使用的dex的加載。DexClassLoader 的源碼里面只有一個構(gòu)造函數(shù),也是遵從雙親委托模型

簡單介紹了PathClassLoader和DexClassLoader,但這兩者都是對BaseDexClassLoader的一層簡單的封裝,真正的實現(xiàn)都在BaseClassLoader內(nèi),那么咱們看下BaseClassLoader內(nèi)的具體實現(xiàn)

(三)android 類加載類從類的角度來先看流程

通過上面的BaseDexClassLoader的構(gòu)造函數(shù),咱們知道了BaseDexClassLoader構(gòu)造的時候創(chuàng)建了一個DexPathList類的對象
那咱們就繼續(xù)跟蹤看下DexPathList這個的類的構(gòu)造函數(shù)

DexPathList


    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;
        this.dexElements =
                makeDexElements(splitDexPath(dexPath), optimizedDirectory);
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }

代碼里面調(diào)用了makeDexElements()這個方法,其中一個參數(shù)是調(diào)用splitLibraryPath()方法的返回值。所以先看下splitLibraryPath()方法

    private static ArrayList<File> splitDexPath(String path) {
        return splitPaths(path, null, false);
    }

    private static ArrayList<File> splitPaths(String path1, String path2, boolean wantDirectories) {
        ArrayList<File> result = new ArrayList<File>();
        splitAndAdd(path1, wantDirectories, result);
        splitAndAdd(path2, wantDirectories, result);
        return result;
    }

    private static void splitAndAdd(String path, boolean wantDirectories,
                                    ArrayList<File> resultList) {
        if (path == null) {
            return;
        }

        String[] strings = path.split(Pattern.quote(File.pathSeparator));

        for (String s : strings) {
            File file = new File(s);

            if (!(file.exists() && file.canRead())) {
                continue;
            }
            
                       /*
             * Note: There are other entities in filesystems than
             * regular files and directories.
             */
            if (wantDirectories) {
                if (!file.isDirectory()) {
                    continue;
                }
            } else {
                if (!file.isFile()) {
                    continue;
                }
            }

            resultList.add(file);
        }
    }

splitDexPath這個方法里面調(diào)用splitPaths()方法,而splitPaths方法調(diào)用了splitAndAdd()方法,通過代碼查看,大概能明白,這個一系列的方法主要作用是過濾,過濾掉不可讀的file和不存在的file,即剩下的都是canRead且是exists的,然后吧這些files add進一個ArrayList<File>,然把這個這個ArrayList<File>作為參數(shù),調(diào)用makeDexElements這個方法,那么咱么一起看下這個方法

    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";
    //上面是支持的后綴,由于在下面這個方法用到了,我就放到到這里

 private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();

        for (File file : files) {
            ZipFile zip = null;
            DexFile dex = null;
            String name = file.getName();

            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 if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                try {
                    zip = new ZipFile(file);
                } catch (IOException ex) {
                    System.logE("Unable to open zip file: " + file, ex);
                }
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ignored) {

                }
            } else {
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }

        return elements.toArray(new Element[elements.size()]);
    }

上面的方法大概的意思是,遍歷剛上傳入的ArrayListM<File>,如果是.dex結(jié)尾的直接調(diào)用loadDexFile方法,如果是.apk或者.zip或者.jar結(jié)尾的用這個File去構(gòu)造一個ZipFile對象,然后還是把這個ZipFile作為參數(shù)調(diào)用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);
        }
    }


    private static String optimizedPathFor(File path,
                                           File optimizedDirectory) {

        String fileName = path.getName();
        if (!fileName.endsWith(DEX_SUFFIX)) {
            int lastDot = fileName.lastIndexOf(".");
            if (lastDot < 0) {
                fileName += DEX_SUFFIX;
            } else {
                StringBuilder sb = new StringBuilder(lastDot + 4);
                sb.append(fileName, 0, lastDot);
                sb.append(DEX_SUFFIX);
                fileName = sb.toString();
            }
        }

        File result = new File(optimizedDirectory, fileName);
        return result.getPath();
    }

上面代碼有個設(shè)置如果optimizedDirectory==null(PS:PathClassLoader其中的optimizedDirectory就是null)則直接new一個DexFile,如果不為空則調(diào)用optimizedPathFor方法,optimizedPathFor就是把復(fù)制一份file放到
optimizedDirectory目錄下,最后把這個文件返回回去。
得到這個DexFile以后,用這個DexFile構(gòu)造一個Element對象
在makeDexElements的for循環(huán)里面依照上面的方法獲取一組DexFile,然后用這一組DexFile去組成Element數(shù)組對象放到內(nèi)存中。

上述僅僅是構(gòu)造DexClassLoader流程,下面咱們看下具體導(dǎo)入類的流程

loadClass()方法,由于DexClassLoader類本身就一個構(gòu)造函數(shù),所以知道這個方法是父類的方法,那么找下DexClassLoader的父類BaseDexClassLoader.java,結(jié)果發(fā)現(xiàn)BaseDexClassLoader.java也沒有這個方法,所以應(yīng)該在BaseDexClassLoader.java的父類里面,那么繼續(xù)尋找BaseDexClassLoader.java的父類ClassLoader里面有這個方法

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

     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                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
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                }
            }
            return c;
      }

       protected Class<?> findClass(String name) throws ClassNotFoundException {
             throw new ClassNotFoundException(name);
       }

看上面的源代碼發(fā)現(xiàn)是先調(diào)用findLoadedClass(name) 看注釋應(yīng)該可以理解為檢查下是否已經(jīng)加載了,如果已經(jīng)加載了,則直接返回Class,如果沒有加載,則看看有沒有父類加載器,如果有父類加載器,則調(diào)用父類加載器的loadClass()方法,如果沒有父類加載器即根類加載器,通過根加載器加載(想下是不是上面說的雙親委托模型)。如果都沒有,則通過findClass()方法查找,那么進入findClass()進去看看,通過上面源代碼發(fā)現(xiàn)ClassLoader是個空方法,而DexClassLoader大家也知道,就一個構(gòu)造函數(shù),所以可以確定這個方法的具體實現(xiàn)在BaseClassLoader里面,那么咱們現(xiàn)在進去BaseClassLoader里面看看

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

可以看到BaseDexClassLoader是通過pathList對象的findClass()方法來獲取類的,那么咱們繼續(xù)進去DexPathList.java的findClass方法里面去看看

    public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

最后調(diào)用兩個DexFile的loadClassBinaryName來導(dǎo)入類的,現(xiàn)在進入DexFIle.java中的loadClassBinaryName()方法中去看下

    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, int 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, int cookie)
            throws ClassNotFoundException, NoClassDefFoundError;

通過上面可以看到DexFile的loadClassBinaryName()方法里面調(diào)用了defineClass()方法,而defineClass里面又調(diào)用natvie方法defineClassNative()在C層去加載類,由于涉及到底層的業(yè)務(wù),由于涉及到比較大的內(nèi)容,這里就不過多的敘述了,后續(xù)有時間單獨再出一個C層的分析文章。defineClassNative大家可以先理解為通過底層去加載類,如果有這個類,就加載出來,至此整個流程已經(jīng)完全跑完。不知道大家理解沒有。

由上述可以歸結(jié)出android類 加載的時序圖,如下圖:
(第一次畫時序圖,畫的不好,大家將就的看下,(__) 嘻嘻……)

(四)android 類加載時序圖

時序圖.png
loadClass.png

可以看出BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile數(shù)組,由上面分析知道,dexPath傳入的原始(.apk,.zip,jar等)文件在optimizedDirectory文件夾生成相應(yīng)的優(yōu)化后的odex文件,dexElements數(shù)組就是這些odex文件的集合,如果不分包,一般這個數(shù)組只有一個Element元素,也就只有一個DexFile文件,而對于這個類加載,就是遍歷這個集合,通過DexFile去尋找。最終調(diào)用native方法的defineClass

五總結(jié)

DexClassLoader和PathClassLoader都屬于雙親委托模型的類加載器。也就是說,它們在加載一個類之前,會去檢查自己及自己以上的類加載器是否已經(jīng)加載過這個類,如果加載過,就會直接將之返回,而不會重復(fù)加載
PathClassLoader是通過構(gòu)造函數(shù)new DexFile(path)來產(chǎn)生DexFile對象的;而DexClassLoader則是通過靜態(tài)方法loadDex(path,outpath,0)得到DexFile對象。這兩者的區(qū)別在于DexClassLoader需要提供一個可寫的outputpath路徑,用來釋放apk包或者jar包中的dex文件。換個說法來說,就是PathClassLoader不能從zip包中釋放dex,因此只支持直接操作Dex格式的文件,或者已經(jīng)安裝apk(因為已經(jīng)安裝的apk在cache中存在緩存的dex文件)。而DexClassLoader可以支持apk.jar,dex文件,并且會制定的outpath路徑釋放dex文件

六github地址

項目地址

感謝
http://androidxref.com/6.0.0_r1/xref/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
http://gityuan.com/2017/03/19/android-classloader/
http://www.itdecent.cn/p/2c23a9e88e3d
http://www.blogjava.net/zh-weir/archive/2011/10/29/362294.html
https://segmentfault.com/a/1190000004062880
http://www.voidcn.com/blog/Mr_LiaBill/article/p-4979756.html
https://yq.aliyun.com/ziliao/160711?spm=5176.8246799.blogcont.19.IRwTYy
http://www.cnblogs.com/coding-way/p/5212208.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容