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)類加載器(也稱應用類加載器) ,下面分別介紹。

Java類加載器

BootstrapClassLoader(啟動類加載器)

啟動類加載器主要加載的是JVM自身需要的類,這個類加載使用 C++語言實現 (這里僅限于Hotspot,也就是JDK1.5之后默認的虛擬機,有很多其他的虛擬機是用Java語言實現的)的,是虛擬機自身的一部分,它負責 將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中 ,注意必由于虛擬機是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機識別,即使把jar包丟到lib目錄下也是沒有作用的(出于安全考慮,Bootstrap啟動類加載器只加載包名為 java、javax、sun 等開頭的類)。

ExtensionClassLoader(擴展類加載器)

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

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

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

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

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

雙親委派模式是在Java 1.2后引入的,類加載器間的關系如下

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

注意:雙親委派模式中的父子關系并非通常所說的類繼承關系,而是采用 組合關系 來復用父類加載器的相關代碼(“父類加載器”不能理解為“父類,加載器”,而應該理解為“父,類加載器”,)

  • 啟動類加載器,由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;
    }
}
復制代碼

輸出結果:

自定義類加載器的父加載器: [email protected]
系統默認的AppClassLoader: [email protected]
AppClassLoader的父類加載器: [email protected]
ExtClassLoader的父類加載器: null
復制代碼

工作原理

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

源碼

  • 相關類結構圖
Android類加載器與Java類加載器的對比

有幾個注意點可以幫助你閱讀源碼:

  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
[email protected]
復制代碼

注意:實現自定義類加載器也可以通過繼承 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的關系

Android類加載器與Java類加載器的對比
  • 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文件加載、類的查找加載的序列圖,如下所示:

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

從上圖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;
        }
    復制代碼

原文 

https://juejin.im/post/5d68f7a46fb9a06b0202d804

本站部分文章源于互聯網,本著傳播知識、有益學習和研究的目的進行的轉載,為網友免費提供。如有著作權人或出版方提出異議,本站將立即刪除。如果您對文章轉載有任何疑問請告之我們,以便我們及時糾正。

PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

轉載請注明原文出處:Harries Blog? » Android類加載器與Java類加載器的對比

贊 (0)
分享到:更多 ()

評論 1

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
  1. repostone非技術的路過。回復
手机彩票计划软件超稳