Android類加載器與Java類加載器的對比

目錄結構

什麼是類加載器?
Java類加載器
    BootstrapClassLoader(啓動類加載器)
    ExtensionClassLoader(擴展類加載器)
    ApplicaitonClassLoader(也叫SystemClassLoader,應用程序類加載器)
    Java類加載器---雙親委派模型
    工做原理
    源碼
    ClassLoader中幾個比較重要的方法
    雙親委派模式做用
    自定義Java類加載器
android類加載器
    JVM與android虛擬機區別
    android類加載機制與源碼解析
    android類加載機制總結
    使用DexClassLoader動態加載dex的簡單例子
android類加載器與java類加載器異同
參考資料
複製代碼

什麼是類加載器?

類加載器(Class Loader):顧名思義,指的是能夠加載類的工具。前面JVM類加載機制——類的生命週期已經介紹過類加載的5個過程,即:加載、驗證、準備、解析、初始化,而類加載器的任務是根據一個類的全限定名來讀取此類的二進制字節流到JVM中,而後轉換爲一個與目標類對應的java.lang.Class對象實例,在虛擬機提供了3種類加載器,啓動(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器),下面分別介紹。php

Java類加載器

BootstrapClassLoader(啓動類加載器)

啓動類加載器主要加載的是JVM自身須要的類,這個類加載使用 C++語言實現 (這裏僅限於Hotspot,也就是JDK1.5以後默認的虛擬機,有不少其餘的虛擬機是用Java語言實現的)的,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必因爲虛擬機是按照文件名識別加載jar包的,如rt.jar,若是文件名不被虛擬機識別,即便把jar包丟到lib目錄下也是沒有做用的(出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類)。html

ExtensionClassLoader(擴展類加載器)

擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類由Java語言實現的,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類,開發者能夠直接使用標準擴展類加載器。java

ApplicaitonClassLoader(也叫SystemClassLoader,應用程序類加載器)

也稱應用程序加載器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader。它負責加載系統類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是咱們常常用到的classpath路徑,開發者能夠直接使用系統類加載器,通常狀況下該類加載是程序中默認的類加載器,經過ClassLoader#getSystemClassLoader() 方法能夠獲取到該類加載器。android

  在Java的平常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,須要注意的是,Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象,並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面咱們進一步瞭解它。c++

Java類加載器---雙親委派模型

雙親委派模式是在Java 1.2後引入的,類加載器間的關係以下shell

注意:雙親委派模式中的父子關係並不是一般所說的類繼承關係,而是採用組合關係來複用父類加載器的相關代碼(「父類加載器」不能理解爲「父類,加載器」,而應該理解爲「父,類加載器」,)bootstrap

  • 啓動類加載器,由C++實現,沒有父類。數組

  • 拓展類加載器(ExtClassLoader),由Java語言實現,父類加載器爲null緩存

  • 系統類加載器(AppClassLoader),由Java語言實現,父類加載器爲ExtClassLoader安全

  • 自定義類加載器,父類加載器爲AppClassLoader。

下面咱們經過程序來驗證上述闡述的觀點:

public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {
       
			 FileClassLoader loader1 = new FileClassLoader("xxx/path");
			
			  System.out.println("自定義類加載器的父加載器: "+loader1.getParent());
			  System.out.println("系統默認的AppClassLoader: "+ClassLoader.getSystemClassLoader());
			  System.out.println("AppClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent());
			  System.out.println("ExtClassLoader的父類加載器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
    }
}

class FileClassLoader extends ClassLoader{
    private String rootDir;
    
    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    // 編寫獲取類的字節碼並建立class對象的邏輯
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //...省略邏輯代碼
        return null;
    }
    //編寫讀取字節流的方法
    private byte[] getClassData(String className) {
        // 讀取類文件的字節
        //省略代碼....
        return null;
    }
}
複製代碼

輸出結果:

自定義類加載器的父加載器: sun.misc.Launcher$AppClassLoader@18b4aac2
系統默認的AppClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
AppClassLoader的父類加載器: sun.misc.Launcher$ExtClassLoader@6bc7c054
ExtClassLoader的父類加載器: null
複製代碼

工做原理

若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行,若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式(接下來的源碼能夠看出這個流程),即每一個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子本身想辦法去完成,這不就是傳說中的實力坑爹?

源碼

  • 相關類結構圖

有幾個注意點能夠幫助你閱讀源碼:

  1. 拓展類加載器ExtClassLoader和系統類加載器AppClassLoader,這兩個類都繼承自URLClassLoader,是sun.misc.Launcher的靜態內部類。
  2. sun.misc.Launcher主要被系統用於啓動主應用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher建立的
  3. 頂層的類加載器是ClassLoader類,它是一個抽象類,其後全部的類加載器都繼承自ClassLoader(不包括啓動類加載器)
ClassLoader中幾個比較重要的方法
  • loadClass(String)

該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2以後再也不建議用戶重寫但用戶能夠直接調用該方法,loadClass()方法是ClassLoader類本身實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼以下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數表明是否生成class對象的同時進行解析相關操做。

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先從緩存查找該class對象,找到就不用從新加載
          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
                  // 若是都沒有找到,則經過自定義實現的findClass去查找並加載
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否須要在加載時進行解析
              resolveClass(c);
          }
          return c;
      }
  }

複製代碼

從loadClass實現也能夠知道若是不想從新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載本身指定的類,那麼咱們能夠直接使用this.getClass().getClassLoder.loadClass("className"),這樣就能夠直接調用ClassLoaderloadClass方法獲取到class對象

  • findClass(String)

在JDK1.2以前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的源碼可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗後,則會調用本身的findClass()方法來完成類加載,這樣就能夠保證自定義的類加載器也符合雙親委託模式。須要注意的是ClassLoader類中並無實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法一般是和defineClass方法一塊兒使用的(稍後會分析),ClassLoader類中findClass()方法源碼以下:

//直接拋出異常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}
複製代碼
  • defineClass(String name,byte[] b, int off, int len)
    • name:classname;b:字節碼的byte字節流;off:開始解析的索引;len:解析的字符長度

defineClass()方法是用來將byte字節流解析成JVM可以識別的Class對象(ClassLoader中已實現該方法邏輯),經過這個方法不只可以經過class文件實例化class對象,也能夠經過其餘方式實例化class對象,如經過網絡接收一個類的字節碼,而後轉換爲byte字節流建立對應的Class對象,defineClass()方法一般與findClass()方法一塊兒使用,通常狀況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼後轉換成流,而後調用defineClass()方法生成類的Class對象,簡單例子以下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
	  // 獲取類的字節數組
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
	      //使用defineClass生成class對象
          return defineClass(name, classData, 0, classData.length);
      }
  }
複製代碼

須要注意的是,若是直接調用defineClass()方法生成類的Class對象,這個類的Class對象並無解析(也能夠理解爲連接階段,畢竟解析是連接的最後一步),其解析操做須要等待初始化階段進行。

  • resolveClass(Class≺?≻ c)

解析類。前面咱們說連接階段主要是對字節碼進行驗證,爲類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換爲直接引用。(類的生命週期詳解)

雙親委派模式做用

  • 共享功能,一些Framework層級的類一旦被頂層的ClassLoader加載過就緩存在內存裏面,之後任何地方用到都不須要從新加載。
  • 隔離功能,保證java/Android核心類庫的純淨和安全,防止惡意加載。( 好比string類,避免用戶本身寫代碼冒充核心類庫)

自定義Java類加載器

從上面源碼的分析,能夠知道:實現自定義類加載器須要繼承ClassLoader,若是想保證自定義的類加載器符合雙親委派機制,則覆寫findClass方法;若是想打破雙親委派機制,則覆寫loadClass方法。

編寫自定義類加載器的意義何在呢?

  • 當class文件不在ClassPath路徑下,默認系統類加載器沒法找到該class文件,在這種狀況下咱們須要實現一個自定義的ClassLoader來加載特定路徑下的class文件生成class對象。

  • 當一個class文件是經過網絡傳輸而且可能會進行相應的加密操做時,須要先對class文件進行相應的解密後再加載到JVM內存中,這種狀況下也須要編寫自定義的ClassLoader並實現相應的邏輯。

  • 當須要實現熱部署功能時(一個class文件經過不一樣的類加載器產生不一樣class對象從而實現熱部署功能),須要實現自定義ClassLoader的邏輯。

這裏咱們實現一個不破壞雙親委託機制的類加載器,以下步驟:

  1. 自定義一個People.java類作例子,該類寫在記事本里,在用javac命令行編譯成class文件,放在d盤根目錄下
public class People {
	private String name;
 
	public People() {}
 
	public People(String name) {
		this.name = name;
	}
 
	public String getName() {
		return name;
	}
 
	public void setName(String name) {
		this.name = name;
	}
 
	public String toString() {
		return "I am a people, my name is " + name;
	}
 
複製代碼
  1. 自定義類加載器
package cn.eft.llj.jvm.customLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
 
public class MyClassLoader extends ClassLoader {
    public MyClassLoader() {
        
    }
    
    public MyClassLoader(ClassLoader parent) {
        super(parent);
    }
    
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
    	File file = new File("D:\\Person.class");//讀取咱們剛放的class文件
        try{
            byte[] bytes = getClassBytes(file);
            //defineClass方法能夠把二進制流字節組成的文件轉換爲一個java.lang.Class
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }
    
    private byte[] getClassBytes(File file) throws Exception
    {
        // 這裏要讀入.class的字節,所以要使用字節流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        
        while (true){
            int i = fc.read(by);
            if (i == 0 || i == -1)
            break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
}
複製代碼
  1. 測試類加載
public class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException {
        MyClassLoader mcl = new MyClassLoader();
        Class<?> clazz = Class.forName("Person", true, mcl);
        Object obj = clazz.newInstance();
    
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());//打印出咱們的自定義類加載器
    
    }
}
複製代碼

運行結果:

I am a person, my name is null
cn.eft.llj.jvm.customLoader.MyClassLoader@6bc7c054
複製代碼

注意:實現自定義類加載器也能夠經過繼承URLClassLoader,可是這種方式不知爲什麼沒法實現,網上不少示例代碼,要麼實現有問題,要麼運行會報錯,如有知道的同窗麻煩評論區跟我說下。

android類加載器

JVM與android虛擬機區別

JVM是Java Virtual Machine,而DVM就是Dalvik Virtual Machine,是安卓中使用的虛擬機,全部安卓程序都運行在安卓系統進程裏,每一個進程對應着一個Dalvik虛擬機實例。他們都提供了對象生命週期管理、堆棧管理、線程管理、安全和異常管理以及垃圾回收等重要功能,各自擁有一套完整的指令系統,如下簡要對比兩種虛擬機的不一樣。 dalvik與jvm的不一樣:

  1. jvm:執行的是class文件;dalvik:執行的是dex
  2. 類加載系統與jvm區別較大
  3. jvm只能存在一個;dalvik能夠同時存在多個dvm
  4. dalvk基於寄存器,jvm基於棧(內存)
Dalvik執行的是dex字節碼,運行時動態地將執行頻率很高的dex字節碼翻譯成本地機器碼;

android5.0後虛擬機由dalvik替換爲ART(Android Runtime),在安裝應用的時候,dex中的字節碼將被編譯成本地機器碼,以後每次打開應用,執行的都是本地機器碼。

ART對比dalvik:
1、DVM使用JIT將字節碼轉換成機器碼,效率低
2、ART採用AOT預編譯技術,執行效率更快
3、ART會佔用更多的安裝時間和存儲空間
4、預編譯減小了 CPU 的使用頻率,下降了能耗

JIT(Just In Time,即時編譯技術)
AOT(Ahead Of Time,預編譯技術)
複製代碼

詳細瞭解可參考這兒JAVA虛擬機與Android虛擬機的區別

android類加載機制與源碼解析

Android的Dalvik/ART虛擬機雖然與標準Java的JVM虛擬機不同,ClassLoader具體的加載細節不同,可是工做機制是相似的, (從android sdk的ClassLoader#loadClass和jdk的ClassLoader#loadClass源碼能夠看出都使用了雙親委託機制)

下面直接來看看ClassLoader的關係

  • BootClassLoader

BootClassLoader實例在Android系統啓動的時候被建立,用於加載一些Android系統框架的類,其中就包括APP用到的一些系統類。(與Java中的BootstrapClassLoader不一樣,它並非由C/C++代碼實現,而是由Java實現的,BootClassLoader是ClassLoader的內部類)

  • BaseDexClassLoader,咱們先來看看基類BaseDexClassLoader的構造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
複製代碼

BaseDexClassLoader構造方法的四個參數的含義以下:

  • dexPath :指目標類所在的apk或jar文件的路徑,好比像這樣的:"/data/app/com.yeoggc.myapplication-1/base.apk"。若是要包含多個路徑,路徑之間用冒號分隔。

  • optimizedDirectory:類加載器把dexPath路徑上的文件,進行ODEX優化到內部存儲路徑,該路徑就是由optimizedDirectory指定的。若是爲null,那麼就採用默認的系統路徑。(不能是任意目錄,它必須是程序所屬的目錄才行,好比:data/data/包名/xxx)

    dex和odex區別:
    其實一個APK是一個程序壓縮包,裏面有個執行程序包含dex文件,ODEX優化就是把包裏面的執行程序提取出來,就變成ODEX文件。由於你提取出來了,系統第一次啓動的時候就不用去解壓程序壓縮包,少了一個解壓的過程。這樣的話系統啓動就加快了。爲何說是第一次呢?是由於DEX版本的也只有第一次會解壓執行程序到 /data/dalvik-cache(針對PathClassLoader)或者optimizedDirectory(針對DexClassLoader)目錄,以後也是直接讀取目錄下的的dex文件,因此第二次啓動就和正常的差很少了。固然這只是簡單的理解,實際生成的ODEX還有必定的優化做用。ClassLoader只能加載內部存儲路徑中的dex文件,因此這個路徑必須爲內部路徑。
    複製代碼
  • libraryPath:指目標類中所使用到的C/C++庫存放的路徑。

  • parent:是指該類加載器的父類加載器,通常爲當前執行類的裝載器

它派生出兩個子類加載器:

  • PathClassLoader: 主要用於系統和app的類加載器,其中optimizedDirectory爲null, 採用默認目錄/data/dalvik-cache/
  • DexClassLoader: 能夠從包含classes.dex的jar或者apk中,加載類的類加載器, 可用於執行動態加載, 但必須是app私有可寫目錄來緩存odex文件. 可以加載系統沒有安裝的apk或者jar文件, 所以不少熱修復和插件化方案都是採用DexClassLoader;

DexClassLoader與PathClassLoader都繼承於BaseDexClassLoader,這兩個類只是提供了本身的構造函數,沒有額外的實現,咱們對比下它們的構造函數的區別。

  • PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
    
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
複製代碼
  • DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
    
   public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}
複製代碼

能夠發現這兩個類的構造函數最大的差異就是DexClassLoader提供了optimizedDirectory,而PathClassLoader則沒有,optimizedDirectory正是用來存放odex文件的地方,因此能夠利用DexClassLoader實現動態加載。

除了這兩個子類之外,還有兩個類:

  • DexPathList:就跟它的名字那樣,該類主要用來查找Dex、SO庫的路徑,並這些路徑總體呈一個數組。
  • DexFile:用來描述Dex文件,Dex的加載以及Class的查找都是由該類調用它的native方法完成的。(DexFile爲DexPathList的內部類Element的成員屬性)

Dex的加載以及Class額查找都是由DexFile調用它的native方法完成的,咱們來看看它的實現。 咱們來看看Dex文件加載、類的查找加載的序列圖,以下所示:

從上圖Dex加載的流程能夠看出,optimizedDirectory決定了調用哪個DexFile的構造函數。

若是optimizedDirectory爲空,這個時候實際上是PathClassLoader,則調用:

DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
        throws IOException {
    this(file.getPath(), loader, elements);
}
複製代碼

若是optimizedDirectory不爲空,這個時候實際上是DexClassLoader,則調用:

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);
    mFileName = sourceName;
    //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}
複製代碼

因此你能夠看到DexClassLoader在加載Dex文件的時候比PathClassLoader多了一個openDexFile()方法,該方法調用的是native方法openDexFileNative()方法。

這個方法並非真的打開Dex文件,而是將Dex文件以一種mmap的方式映射到虛擬機進程的地址空間中去,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的映射關係後,虛擬機進程就能夠採用指針的方式讀寫操做這一段內存,而系統會自動回寫髒頁面到對應的文件磁盤上,即完成了對文件的操做而沒必要再調用read,write等系統調用函數。 關於mmap,它是一種頗有用的文件讀寫方式,限於篇幅這裏再也不展開,更多關於mmap的內容能夠參見文章:認真分析mmap:是什麼 爲何 怎麼用

android類加載機制總結

Android虛擬機有兩個主要的類加載器DexClassLoader與PathClassLoader,它們都繼承於BaseDexClassLoader,它們內部都維護了一個DexPathList的對象,DexPathList主要用來存放指明包含dex文件、native庫和優化odex目錄。 Dex文件採用DexFile這個類來描述,Dex的加載以及類的查找都是經過DexFile調用它的native方法來完成的。

使用DexClassLoader動態加載dex的簡單例子

前面咱們說過DexClassLoader能夠加載外部獲取的dex/jar/apk文件來實現熱修復或插件化,接下來先演示一個簡單的動態加載dex示例,該示例實現了:從 SD 卡中動態加載一個包含 class.dex 的 jarr文件,加載其中的類,並調用其方法。(關於熱修復要比這個複雜一些,下次再詳解)

主要步驟以下:

  1. 將修復的代碼打成jar包 新建兩個java類ISayHello.java 和 SayHello.java
package com.jaeger;

public interface ISayHello {

    String say();
}

複製代碼
package com.jaeger;
public class SayHello implements ISayHello {
    @Override
    public String say() {
        return "這是新的文字";
    }
}
複製代碼

這裏咱們經過android studio的gradle腳本打出jar包:sayHello.jar

task sayHelloJar(type: Jar) {
    dependsOn 'build'
    from ("${buildDir}/intermediates/classes/debug/")
    include "com/jaeger/**"
    exclude "com/jaeger/testclassloader/**" //這個包下放MainActivity,不一塊兒打包,排除掉
    archiveName "sayHello.jar"
    destinationDir new File("${buildDir}/outputs/libs")//打好的jar包會在這個目錄
}
複製代碼
  1. 將jar包打成dex包

將上面的sayHello.jar放在sdk的dx 工具所在的目錄下(android 23及以上dx工具的目錄在:sdk\build-tools\sdk版本\下,更早以前的版本可能在sdk\platform-tools目錄下),打開命令行工具,進入該目錄,而後執行以下

D:\Android\sdk\build-tools\28.0.3>dx --dex --output=sayHello_dex.jar sayHello.jar
複製代碼

這樣,就打出了sayHello_dex.jar的dex包了

  1. dex包sayHello_dex.jar直接拖到手機存儲根目錄下

  2. 使用DexClassLoader加載dex中的類,並調用相關方法

記得先刪除項目中的SayHello.java,防止在加載dex包下的SayHello以前就先加載了項目中的SayHello

package com.jaeger.testclassloader;

import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.jaeger.ISayHello;
import com.jaeger.SayHello;

import dalvik.system.DexClassLoader;
import java.io.File;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "TestClassLoader";
    private TextView mTvInfo;
    private Button mBtnLoad;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvInfo = findViewById(R.id.tv_info);
        mBtnLoad = findViewById(R.id.btn_load);
        mBtnLoad.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 獲取到包含 class.dex 的 jar 包文件
                final File jarFile =
                    new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "sayHello_dex.jar");

                //6.0以上的自行手動申請權限,這裏重點不是這個就不寫了,或者能夠到設置界面直接開啓對應的權限
                Log.d(TAG, jarFile.length() + "");
                Log.d(TAG, jarFile.canRead() + "");

                if (!jarFile.exists()) {
                    Log.e(TAG, "sayHello_dex.jar not exists");
                    return;
                }

                // getCodeCacheDir() 方法在 API 21 才能使用,實際測試替換成 getExternalCacheDir() 等也是能夠的
                // 只要有讀寫權限的路徑都可
                DexClassLoader dexClassLoader =
                    new DexClassLoader(jarFile.getAbsolutePath(), getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
                try {
                    // 加載 SayHello 類
                    Class clazz = dexClassLoader.loadClass("com.jaeger.SayHello");
                    // 強轉成 ISayHello, 注意 ISayHello 的包名須要和 jar 包中的
                    ISayHello iSayHello = (ISayHello) clazz.newInstance();
                    mTvInfo.setText(iSayHello.say());
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

複製代碼

佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">

    <TextView android:id="@+id/tv_info" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="36dp" android:text="Hello World!"/>

    <Button android:id="@+id/btn_load" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="36dp" android:text="load dex form SD card"/>
</LinearLayout>

複製代碼

AndroidManifest.xml記得受權

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
複製代碼
  1. 運行項目,點擊按鈕,上面的文字被成功替換成了新的文字

android類加載器與java類加載器異同

根據前面的分析,咱們總結下,android與java在類加載上的異同

相同:

  • Android類加載器和Java的類加載器工做機制是相似的,使用雙親委託機制

不一樣:

  • 加載的字節碼不一樣 Android虛擬機運行的是dex字節碼,Java虛擬機運行的class字節碼。

  • 類加載器不一樣以及類加載器的類體系結構不一樣 如上面的類加載器結構圖

  • BootClassLoader和Java的BootStrapClassLoader區別:Android虛擬機中BootClassLoader是ClassLoader內部類,由java代碼實現而不是c++實現,是Android平臺上全部ClassLoader的最終parent,這個內部類是包內可見,因此咱們無法使用。 Java虛擬機中BootStrapClassLoader是由原生代碼(C++)編寫的,負責加載java核心類庫(例如rt.jar等)

  • ClassLoader類中的findBootstrapClassOrNull方法,android sdk直接返回null,jdk會去調用native方法findBootstrapClass,以下源碼

    • jdk8的findBootstrapClassOrNull方法:
    /** * Returns a class loaded by the bootstrap class loader; * or return null if not found. */
        private Class<?> findBootstrapClassOrNull(String name)
        {
            if (!checkName(name)) return null;
    
            return findBootstrapClass(name);
        }
    
        // return null if not found
        private native Class<?> findBootstrapClass(String name);
    
    複製代碼

    android sdk的findBootstrapClassOrNull方法:

    /** * Returns a class loaded by the bootstrap class loader; * or return null if not found. */
        private Class<?> findBootstrapClassOrNull(String name)
        {
            return null;
        }
    複製代碼

參考資料

深刻理解Java類加載器(ClassLoader)

Android虛擬機框架:類加載機制

熱修復入門

相關文章
相關標籤/搜索