Android插件化基礎1-----加載SD上APK

Android插件化基礎的主要內容包括

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

本文涉及的內容如下:

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

一、Java類加載介紹

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

(一)什么是ClassLoader:

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

(二)ClassLoader的作用:

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

(三)ClassLoader類的結構分析

為了更好的理解類加載機制,我們來深入研究下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 是一個負責加載classes的對象,ClassLoader類是一個抽象類,需要給出類的二進制名稱,ClassLoader嘗試定位或者產生一個class數據,一個典型的策略是把二進制名字轉換成文件名然后到文件系統中找到該文件
以下是ClassLoader常用到的幾個方法及其重載方法:

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

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

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

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

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

1原理介紹:

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

class_image.png

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

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

二.android apk安裝簡述

(一)android 打包簡述

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


image.png

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

PS:

  • 1 odex優化有什么用:
    ODEX的用途是分離程序資源和可執行文件,達到快速軟件加載速度和開機速度的目的。
  • 2 棒棒糖與ART帶來的問題
    很多人會有疑問,Android5.0開始,默認已經使用ART,棄用Dalvik了,應用程序會在安裝時被編譯成OAT文件,(ART上運行的格式)ODEX還有什么用那?
    這里我們引用google的權威回答:
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轉換成OAT的這個過程是在用戶安裝程序或者刷入ROM,OTA更新后首次啟動時執行的,按照google的說法,相比做過ODEX優化,未做過優化的DEX轉成成OAT要花費更長的時間,比如2-3倍。比如安裝一個odex優化過的程序假設需要1秒鐘,未做過優化的程序就需要2-3秒。由此次可見,雖然dalvik被棄用了,但是ODEX優化在Android棒棒糖上依舊擁有顯著的優化效果。首先ODEX優化不僅僅只是針對應用程序,還會對內核鏡像,jar庫文件等進行優化。其次,資源和可執行文件分離帶來的性能提升無論是運行在ART還是Dalvik,都有效。

(二)android 安裝

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

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

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

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

三 Demo演示 :

(一)先看下demo目錄

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

看下 AppTest 里面的目錄結構

顯示.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>

AppTest 被打包成apk的時候,我們要在 dexclassloaderapp 里面獲取這些數據 。現在開始打包apk,名字為"AppTest-release.apk",然后把這個apk放到sd上。
現在run dexclassloaderapp 項目會出現下面的顯示

device-1.png

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

類加載.gif

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

先看下 dexclassLoaderapp 是如何實現在家外部APK class的
具體實現是在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()) {
            // 如果沒有讀權限,確定你在 AndroidManifest 中是否聲明了讀寫權限
            // 如果是6.0以上手機要查看手機的權限管理,你的這個app是否具有讀寫權限
            Log.d("LGC", "apkFile.canRead()= " + apkFile.canRead());
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    pd.dismiss();
                    Toast.makeText(MainActivity.this, "沒有讀寫權限", Toast.LENGTH_LONG);

                }
            });
            return;
        }


        // getCodeCacheDir() 方法在 API 21 才能使用,實際測試替換成 getExternalCacheDir() 等也是可以的
        // 只要有讀寫權限的路徑均可
        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這個類,簡化了『通過一個類的全限定名獲取描述次類的二進制字節流』這個過程;我們只需要告訴DexClassLoader一個dex文件或者apk文件的路徑就能完成類的加載。
詳細敘述請看下面

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

(一)android 類加載設計圖

結構圖.jpg

(二)android類加載 類圖

image1.png

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

classloader.jpg

這里面有兩個重要的類
PathClassLoader和DexClassLoader他們分別繼承BaseDexClassLoader,那他們的區別是什么?那么看下他們的構造函數

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

他們都是調用父類BaseDexClassLoader的構造函數
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); 
}

這四個參數分別的含義:

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

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

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

通過上面的BaseDexClassLoader的構造函數,咱們知道了BaseDexClassLoader構造的時候創建了一個DexPathList類的對象
那咱們就繼續跟蹤看下DexPathList這個的類的構造函數

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

代碼里面調用了makeDexElements()這個方法,其中一個參數是調用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這個方法里面調用splitPaths()方法,而splitPaths方法調用了splitAndAdd()方法,通過代碼查看,大概能明白,這個一系列的方法主要作用是過濾,過濾掉不可讀的file和不存在的file,即剩下的都是canRead且是exists的,然后吧這些files add進一個ArrayList<File>,然把這個這個ArrayList<File>作為參數,調用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結尾的直接調用loadDexFile方法,如果是.apk或者.zip或者.jar結尾的用這個File去構造一個ZipFile對象,然后還是把這個ZipFile作為參數調用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();
    }

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

上述僅僅是構造DexClassLoader流程,下面咱們看下具體導入類的流程

loadClass()方法,由于DexClassLoader類本身就一個構造函數,所以知道這個方法是父類的方法,那么找下DexClassLoader的父類BaseDexClassLoader.java,結果發現BaseDexClassLoader.java也沒有這個方法,所以應該在BaseDexClassLoader.java的父類里面,那么繼續尋找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);
       }

看上面的源代碼發現是先調用findLoadedClass(name) 看注釋應該可以理解為檢查下是否已經加載了,如果已經加載了,則直接返回Class,如果沒有加載,則看看有沒有父類加載器,如果有父類加載器,則調用父類加載器的loadClass()方法,如果沒有父類加載器即根類加載器,通過根加載器加載(想下是不是上面說的雙親委托模型)。如果都沒有,則通過findClass()方法查找,那么進入findClass()進去看看,通過上面源代碼發現ClassLoader是個空方法,而DexClassLoader大家也知道,就一個構造函數,所以可以確定這個方法的具體實現在BaseClassLoader里面,那么咱們現在進去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()方法來獲取類的,那么咱們繼續進去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;
    }

最后調用兩個DexFile的loadClassBinaryName來導入類的,現在進入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()方法里面調用了defineClass()方法,而defineClass里面又調用natvie方法defineClassNative()在C層去加載類,由于涉及到底層的業務,由于涉及到比較大的內容,這里就不過多的敘述了,后續有時間單獨再出一個C層的分析文章。defineClassNative大家可以先理解為通過底層去加載類,如果有這個類,就加載出來,至此整個流程已經完全跑完。不知道大家理解沒有。

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

(四)android 類加載時序圖

時序圖.png
loadClass.png

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

五總結

DexClassLoader和PathClassLoader都屬于雙親委托模型的類加載器。也就是說,它們在加載一個類之前,會去檢查自己及自己以上的類加載器是否已經加載過這個類,如果加載過,就會直接將之返回,而不會重復加載
PathClassLoader是通過構造函數new DexFile(path)來產生DexFile對象的;而DexClassLoader則是通過靜態方法loadDex(path,outpath,0)得到DexFile對象。這兩者的區別在于DexClassLoader需要提供一個可寫的outputpath路徑,用來釋放apk包或者jar包中的dex文件。換個說法來說,就是PathClassLoader不能從zip包中釋放dex,因此只支持直接操作Dex格式的文件,或者已經安裝apk(因為已經安裝的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.lxweimin.com/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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容