Android插件化開(kāi)發(fā)動(dòng)態(tài)加載基礎(chǔ)之ClassLoader工作機(jī)制

      網(wǎng)友投稿 944 2022-05-30

      類(lèi)加載器ClassLoader

      早期使用過(guò)Eclipse等Java編寫(xiě)的軟件的同學(xué)可能比較熟悉,Eclipse可以加載許多第三方的插件(或者叫擴(kuò)展),這就是動(dòng)態(tài)加載。這些插件大多是一些Jar包,而使用插件其實(shí)就是動(dòng)態(tài)加載Jar包里的Class進(jìn)行工作。這其實(shí)非常好理解,Java代碼都是寫(xiě)在Class里面的,程序運(yùn)行在虛擬機(jī)上時(shí),虛擬機(jī)需要把需要的Class加載進(jìn)來(lái)才能創(chuàng)建實(shí)例對(duì)象并工作,而完成這一個(gè)加載工作的角色就是ClassLoader。

      對(duì)于Java程序來(lái)說(shuō),編寫(xiě)程序就是編寫(xiě)類(lèi),運(yùn)行程序也就是運(yùn)行類(lèi)(編譯得到的class文件),其中起到關(guān)鍵作用的就是類(lèi)加載器ClassLoader。

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

      有幾個(gè)ClassLoader實(shí)例?

      動(dòng)態(tài)加載的基礎(chǔ)是ClassLoader,從名字也可以看出,ClassLoader就是專(zhuān)門(mén)用來(lái)處理類(lèi)加載工作的,所以這貨也叫類(lèi)加載器,而且一個(gè)運(yùn)行中的APP 不僅只有一個(gè)類(lèi)加載器。

      其實(shí),在Android系統(tǒng)啟動(dòng)的時(shí)候會(huì)創(chuàng)建一個(gè)Boot類(lèi)型的ClassLoader實(shí)例,用于加載一些系統(tǒng)Framework層級(jí)需要的類(lèi),我們的Android應(yīng)用里也需要用到一些系統(tǒng)的類(lèi),所以APP啟動(dòng)的時(shí)候也會(huì)把這個(gè)Boot類(lèi)型的ClassLoader傳進(jìn)來(lái)。

      此外,APP也有自己的類(lèi),這些類(lèi)保存在APK的dex文件里面,所以APP啟動(dòng)的時(shí)候,也會(huì)創(chuàng)建一個(gè)自己的ClassLoader實(shí)例,用于加載自己dex文件中的類(lèi)。下面我們?cè)陧?xiàng)目里驗(yàn)證看看

      @Override

      protected void onCreate(Bundle savedInstanceState) {

      super.onCreate(savedInstanceState);

      ClassLoader classLoader = getClassLoader();

      if (classLoader != null){

      Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());

      while (classLoader.getParent()!=null){

      classLoader = classLoader.getParent();

      Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());

      }

      }

      }

      輸出結(jié)果為

      [onCreate] classLoader 1 : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/me.kaede.anroidclassloadersample-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]

      [onCreate] classLoader 2 : java.lang.BootClassLoader@14af4e32

      可以看見(jiàn)有2個(gè)Classloader實(shí)例,一個(gè)是BootClassLoader(系統(tǒng)啟動(dòng)的時(shí)候創(chuàng)建的),另一個(gè)是PathClassLoader(應(yīng)用啟動(dòng)時(shí)創(chuàng)建的,用于加載“/data/app/me.kaede.anroidclassloadersample-1/base.apk”里面的類(lèi))。由此也可以看出,一個(gè)運(yùn)行的Android應(yīng)用至少有2個(gè)ClassLoader。

      創(chuàng)建自己ClassLoader實(shí)例

      動(dòng)態(tài)加載外部的dex文件的時(shí)候,我們也可以使用自己創(chuàng)建的ClassLoader實(shí)例來(lái)加載dex里面的Class,不過(guò)ClassLoader的創(chuàng)建方式有點(diǎn)特殊,我們先看看它的構(gòu)造方法

      /*

      * constructor for the BootClassLoader which needs parent to be null.

      */

      ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {

      if (parentLoader == null && !nullAllowed) {

      throw new NullPointerException("parentLoader == null && !nullAllowed");

      }

      parent = parentLoader;

      }

      創(chuàng)建一個(gè)ClassLoader實(shí)例的時(shí)候,需要使用一個(gè)現(xiàn)有的ClassLoader實(shí)例作為新創(chuàng)建的實(shí)例的Parent。這樣一來(lái),一個(gè)Android應(yīng)用,甚至整個(gè)Android系統(tǒng)里所有的ClassLoader實(shí)例都會(huì)被一棵樹(shù)關(guān)聯(lián)起來(lái),這也是ClassLoader的 雙親代理模型(Parent-Delegation Model)的特點(diǎn)。

      ClassLoader雙親代理模型加載類(lèi)的特點(diǎn)和作用

      JVM中ClassLoader通過(guò)defineClass方法加載jar里面的Class,而Android中這個(gè)方法被棄用了。

      @Deprecated

      protected final Class defineClass(byte[] classRep, int offset, int length)

      throws ClassFormatError {

      throw new UnsupportedOperationException("can't load this type of class file");

      }

      取而代之的是loadClass方法

      public Class loadClass(String className) throws ClassNotFoundException {

      return loadClass(className, false);

      }

      protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {

      Class clazz = findLoadedClass(className);

      if (clazz == null) {

      ClassNotFoundException suppressed = null;

      try {

      clazz = parent.loadClass(className, false);

      } catch (ClassNotFoundException e) {

      suppressed = e;

      }

      if (clazz == null) {

      try {

      clazz = findClass(className);

      } catch (ClassNotFoundException e) {

      e.addSuppressed(suppressed);

      throw e;

      }

      }

      }

      return clazz;

      }

      特點(diǎn)

      從源碼中我們也可以看出,loadClass方法在加載一個(gè)類(lèi)的實(shí)例的時(shí)候,

      會(huì)先查詢(xún)當(dāng)前ClassLoader實(shí)例是否加載過(guò)此類(lèi),有就返回;

      如果沒(méi)有。查詢(xún)Parent是否已經(jīng)加載過(guò)此類(lèi),如果已經(jīng)加載過(guò),就直接返回Parent加載的類(lèi);

      如果繼承路線上的ClassLoader都沒(méi)有加載,才由Child執(zhí)行類(lèi)的加載工作;

      這樣做有個(gè)明顯的特點(diǎn),如果一個(gè)類(lèi)被位于樹(shù)根的ClassLoader加載過(guò),那么在以后整個(gè)系統(tǒng)的生命周期內(nèi),這個(gè)類(lèi)永遠(yuǎn)不會(huì)被重新加載。

      作用

      首先是共享功能,一些Framework層級(jí)的類(lèi)一旦被頂層的ClassLoader加載過(guò)就緩存在內(nèi)存里面,以后任何地方用到都不需要重新加載。

      除此之外還有隔離功能,不同繼承路線上的ClassLoader加載的類(lèi)肯定不是同一個(gè)類(lèi),這樣的限制避免了用戶(hù)自己的代碼冒充核心類(lèi)庫(kù)的類(lèi)訪問(wèn)核心類(lèi)庫(kù)包可見(jiàn)成員的情況。這也好理解,一些系統(tǒng)層級(jí)的類(lèi)會(huì)在系統(tǒng)初始化的時(shí)候被加載,比如java.lang.String,如果在一個(gè)應(yīng)用里面能夠簡(jiǎn)單地用自定義的String類(lèi)把這個(gè)系統(tǒng)的String類(lèi)給替換掉,那將會(huì)有嚴(yán)重的安全問(wèn)題。

      使用ClassLoader一些需要注意的問(wèn)題

      我們都知道,我們可以通過(guò)動(dòng)態(tài)加載獲得新的類(lèi),從而升級(jí)一些代碼邏輯,這里有幾個(gè)問(wèn)題要注意一下。

      如果你希望通過(guò)動(dòng)態(tài)加載的方式,加載一個(gè)新版本的dex文件,使用里面的新類(lèi)替換原有的舊類(lèi),從而修復(fù)原有類(lèi)的BUG,那么你必須保證在加載新類(lèi)的時(shí)候,舊類(lèi)還沒(méi)有被加載,因?yàn)槿绻呀?jīng)加載過(guò)舊類(lèi),那么ClassLoader會(huì)一直優(yōu)先使用舊類(lèi)。

      如果舊類(lèi)總是優(yōu)先于新類(lèi)被加載,我們也可以使用一個(gè)與加載舊類(lèi)的ClassLoader沒(méi)有樹(shù)的繼承關(guān)系的另一個(gè)ClassLoader來(lái)加載新類(lèi),因?yàn)镃lassLoader只會(huì)檢查其Parent有沒(méi)有加載過(guò)當(dāng)前要加載的類(lèi),如果兩個(gè)ClassLoader沒(méi)有繼承關(guān)系,那么舊類(lèi)和新類(lèi)都能被加載。

      不過(guò)這樣一來(lái)又有另一個(gè)問(wèn)題了,在Java中,只有當(dāng)兩個(gè)實(shí)例的類(lèi)名、包名以及加載其的ClassLoader都相同,才會(huì)被認(rèn)為是同一種類(lèi)型。上面分別加載的新類(lèi)和舊類(lèi),雖然包名和類(lèi)名都完全一樣,但是由于加載的ClassLoader不同,所以并不是同一種類(lèi)型,在實(shí)際使用中可能會(huì)出現(xiàn)類(lèi)型不符異常。

      同一個(gè)Class = 相同的 ClassName + PackageName + ClassLoader

      以上問(wèn)題在采用動(dòng)態(tài)加載功能的開(kāi)發(fā)中容易出現(xiàn),請(qǐng)注意。

      DexClassLoader 和 PathClassLoader

      在Android中,ClassLoader是一個(gè)抽象類(lèi),實(shí)際開(kāi)發(fā)過(guò)程中,我們一般是使用其具體的子類(lèi)DexClassLoader、PathClassLoader這些類(lèi)加載器來(lái)加載類(lèi)的,它們的不同之處是:

      DexClassLoader可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk;

      PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過(guò)的apk;

      類(lèi)加載器的初始化

      平時(shí)開(kāi)發(fā)的時(shí)候,使用DexClassLoader就夠用了,但是我們不妨挖一下這兩者具體細(xì)節(jié)上的區(qū)別。

      // DexClassLoader.java

      public class DexClassLoader extends BaseDexClassLoader {

      public DexClassLoader(String dexPath, String optimizedDirectory,

      String libraryPath, ClassLoader parent) {

      super(dexPath, new File(optimizedDirectory), libraryPath, parent);

      }

      }

      // PathClassLoader.java

      Android插件化開(kāi)發(fā)之動(dòng)態(tài)加載基礎(chǔ)之ClassLoader工作機(jī)制

      public class PathClassLoader extends BaseDexClassLoader {

      public PathClassLoader(String dexPath, ClassLoader parent) {

      super(dexPath, null, null, parent);

      }

      public PathClassLoader(String dexPath, String libraryPath,

      ClassLoader parent) {

      super(dexPath, null, libraryPath, parent);

      }

      }

      這兩者只是簡(jiǎn)單的對(duì)BaseDexClassLoader做了一下封裝,具體的實(shí)現(xiàn)還是在父類(lèi)里。不過(guò)這里也可以看出,PathClassLoader的optimizedDirectory只能是null,進(jìn)去BaseDexClassLoader看看這個(gè)參數(shù)是干什么的

      public BaseDexClassLoader(String dexPath, File optimizedDirectory,

      String libraryPath, ClassLoader parent) {

      super(parent);

      this.originalPath = dexPath;

      this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

      }

      這里創(chuàng)建了一個(gè)DexPathList實(shí)例,進(jìn)去看看

      public DexPathList(ClassLoader definingContext, String dexPath,

      String libraryPath, File optimizedDirectory) {

      ……

      this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);

      }

      private static Element[] makeDexElements(ArrayList files,

      File optimizedDirectory) {

      ArrayList elements = new ArrayList();

      for (File file : files) {

      ZipFile zip = null;

      DexFile dex = null;

      String name = file.getName();

      if (name.endsWith(DEX_SUFFIX)) {

      dex = loadDexFile(file, optimizedDirectory);

      } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)

      || name.endsWith(ZIP_SUFFIX)) {

      zip = new ZipFile(file);

      }

      ……

      if ((zip != null) || (dex != null)) {

      elements.add(new Element(file, zip, dex));

      }

      }

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

      }

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

      }

      }

      /**

      * Converts a dex/jar file path and an output directory to an

      * output file path for an associated optimized dex file.

      */

      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是用來(lái)緩存我們需要加載的dex文件的,并創(chuàng)建一個(gè)DexFile對(duì)象,如果它為null,那么會(huì)直接使用dex文件原有的路徑來(lái)創(chuàng)建DexFile對(duì)象。

      optimizedDirectory必須是一個(gè)內(nèi)部存儲(chǔ)路徑,還記得我們之前說(shuō)過(guò)的,無(wú)論哪種動(dòng)態(tài)加載,加載的可執(zhí)行文件一定要存放在內(nèi)部存儲(chǔ)。DexClassLoader可以指定自己的optimizedDirectory,所以它可以加載外部的dex,因?yàn)檫@個(gè)dex會(huì)被復(fù)制到內(nèi)部路徑的optimizedDirectory;而PathClassLoader沒(méi)有optimizedDirectory,所以它只能加載內(nèi)部的dex,這些大都是存在系統(tǒng)中已經(jīng)安裝過(guò)的apk里面的。

      加載類(lèi)的過(guò)程

      上面還只是創(chuàng)建了類(lèi)加載器的實(shí)例,其中創(chuàng)建了一個(gè)DexFile實(shí)例,用來(lái)保存dex文件,我們猜想這個(gè)實(shí)例就是用來(lái)加載類(lèi)的。

      Android中,ClassLoader用loadClass方法來(lái)加載我們需要的類(lèi)

      public Class loadClass(String className) throws ClassNotFoundException {

      return loadClass(className, false);

      }

      protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException {

      Class clazz = findLoadedClass(className);

      if (clazz == null) {

      ClassNotFoundException suppressed = null;

      try {

      clazz = parent.loadClass(className, false);

      } catch (ClassNotFoundException e) {

      suppressed = e;

      }

      if (clazz == null) {

      try {

      clazz = findClass(className);

      } catch (ClassNotFoundException e) {

      e.addSuppressed(suppressed);

      throw e;

      }

      }

      }

      return clazz;

      }

      loadClass方法調(diào)用了findClass方法,而B(niǎo)aseDexClassLoader重載了這個(gè)方法,得到BaseDexClassLoader看看

      @Override

      protected Class findClass(String name) throws ClassNotFoundException {

      Class clazz = pathList.findClass(name);

      if (clazz == null) {

      throw new ClassNotFoundException(name);

      }

      return clazz;

      }

      結(jié)果還是調(diào)用了DexPathList的findClass

      public Class findClass(String name) {

      for (Element element : dexElements) {

      DexFile dex = element.dexFile;

      if (dex != null) {

      Class clazz = dex.loadClassBinaryName(name, definingContext);

      if (clazz != null) {

      return clazz;

      }

      }

      }

      return null;

      }

      這里遍歷了之前所有的DexFile實(shí)例,其實(shí)也就是遍歷了所有加載過(guò)的dex文件,再調(diào)用loadClassBinaryName方法一個(gè)個(gè)嘗試能不能加載想要的類(lèi),真是簡(jiǎn)單粗暴

      public Class loadClassBinaryName(String name, ClassLoader loader) {

      return defineClass(name, loader, mCookie);

      }

      private native static Class defineClass(String name, ClassLoader loader, int cookie);

      看到這里想必大家都明白了,loadClassBinaryName中調(diào)用了Native方法defineClass加載類(lèi)。

      至此,ClassLoader的創(chuàng)建和加載類(lèi)的過(guò)程的完成了。有趣的是,標(biāo)準(zhǔn)JVM中,ClassLoader是用defineClass加載類(lèi)的,而Android中defineClass被棄用了,改用了loadClass方法,而且加載類(lèi)的過(guò)程也挪到了DexFile中,在DexFile中加載類(lèi)的具體方法也叫defineClass,不知道是Google故意寫(xiě)成這樣的還是巧合。

      自定義ClassLoader

      平時(shí)進(jìn)行動(dòng)態(tài)加載開(kāi)發(fā)的時(shí)候,使用DexClassLoader就夠了。但我們也可以創(chuàng)建自己的類(lèi)去繼承ClassLoader,需要注意的是loadClass方法并不是final類(lèi)型的,所以我們可以重載loadClass方法并改寫(xiě)類(lèi)的加載邏輯。

      通過(guò)前面我們分析知道,ClassLoader雙親代理的實(shí)現(xiàn)很大一部分就是在loadClass方法里,我們可以通過(guò)重寫(xiě)loadClass方法避開(kāi)雙親代理的框架,這樣一來(lái)就可以在重新加載已經(jīng)加載過(guò)的類(lèi),也可以在加載類(lèi)的時(shí)候注入一些代碼。這是一種Hack的開(kāi)發(fā)方式,采用這種開(kāi)發(fā)方式的程序穩(wěn)定性可能比較差,但是卻可以實(shí)現(xiàn)一些“黑科技”的功能。

      Android程序比起一般Java程序在使用動(dòng)態(tài)加載時(shí)麻煩在哪里

      通過(guò)上面的分析,我們知道使用ClassLoader動(dòng)態(tài)加載一個(gè)外部的類(lèi)是非常容易的事情,所以很容易就能實(shí)現(xiàn)動(dòng)態(tài)加載新的可執(zhí)行代碼的功能,但是比起一般的Java程序,在Android程序中使用動(dòng)態(tài)加載主要有兩個(gè)麻煩的問(wèn)題:

      Android中許多組件類(lèi)(如Activity、Service等)是需要在Manifest文件里面注冊(cè)后才能工作的(系統(tǒng)會(huì)檢查該組件有沒(méi)有注冊(cè)),所以即使動(dòng)態(tài)加載了一個(gè)新的組件類(lèi)進(jìn)來(lái),沒(méi)有注冊(cè)的話還是無(wú)法工作;

      Res資源是Android開(kāi)發(fā)中經(jīng)常用到的,而Android是把這些資源用對(duì)應(yīng)的R.id注冊(cè)好,運(yùn)行時(shí)通過(guò)這些ID從Resource實(shí)例中獲取對(duì)應(yīng)的資源。如果是運(yùn)行時(shí)動(dòng)態(tài)加載進(jìn)來(lái)的新類(lèi),那類(lèi)里面用到R.id的地方將會(huì)拋出找不到資源或者用錯(cuò)資源的異常,因?yàn)樾骂?lèi)的資源ID根本和現(xiàn)有的Resource實(shí)例中保存的資源ID對(duì)不上;

      說(shuō)到底,拋開(kāi)虛擬機(jī)的差別不說(shuō),一個(gè)Android程序和標(biāo)準(zhǔn)的Java程序最大的區(qū)別就在于他們的上下文環(huán)境(Context)不同。Android中,這個(gè)環(huán)境可以給程序提供組件需要用到的功能,也可以提供一些主題、Res等資源,其實(shí)上面說(shuō)到的兩個(gè)問(wèn)題都可以統(tǒng)一說(shuō)是這個(gè)環(huán)境的問(wèn)題,而現(xiàn)在的各種Android動(dòng)態(tài)加載框架中,核心要解決的東西也正是“如何給外部的新類(lèi)提供上下文環(huán)境”的問(wèn)題。

      Android Java

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:華為新一代企業(yè)數(shù)字化協(xié)作,有哪些黑科技?
      下一篇:華為網(wǎng)絡(luò)配置(VRRP)
      相關(guān)文章
      亚洲国产成人精品女人久久久| 国产亚洲综合久久| 国产gv天堂亚洲国产gv刚刚碰| 亚洲成A人片在线观看无码3D| 国产精品久久亚洲一区二区| 亚洲AV成人一区二区三区观看 | 中文字幕亚洲一区| 亚洲国产精品综合久久一线| 亚洲成A人片在线观看中文| 亚洲av麻豆aⅴ无码电影| 婷婷综合缴情亚洲狠狠尤物| 亚洲Av无码乱码在线znlu| 亚洲国产综合精品一区在线播放| 亚洲国产综合精品一区在线播放| 亚洲片一区二区三区| 中文字幕中韩乱码亚洲大片| 自拍偷自拍亚洲精品情侣| 亚洲中文字幕无码一久久区| 亚洲国产一成人久久精品| 亚洲成AV人在线观看天堂无码| 久久国产亚洲精品麻豆| 亚洲AV无码久久精品狠狠爱浪潮| 亚洲日本一区二区| 91亚洲国产成人久久精品| 日韩亚洲不卡在线视频中文字幕在线观看| 久久精品国产亚洲AV忘忧草18| 亚洲中文字幕无码mv| 久久无码av亚洲精品色午夜| 亚洲国产成人精品无码久久久久久综合 | 亚洲中文字幕在线无码一区二区| 亚洲人成网站在线观看播放动漫 | 亚洲国产综合第一精品小说| 久久久国产亚洲精品| 韩国亚洲伊人久久综合影院| 亚洲日韩在线观看免费视频| 亚洲精品国产精品乱码不卡√| 亚洲人成在线电影| 亚洲中文字幕无码mv| 亚洲人成网站在线观看青青| 国产AV无码专区亚洲精品| 亚洲欧洲精品久久|