Android App熱更新中的外掛化(ClassLoader、DexLoader)(1)

desaco發表於2016-02-01

PathClassLoader在熱更新的作用?

Android ClassLoader流程解讀並簡單方式實現熱更新- https://www.jianshu.com/p/2f4939320eb1

> Android 動態升級
 1.Android 外掛化 —— 指將一個程式劃分為不同的部分,比如一般 App 的皮膚樣式就可以看成一個外掛;
 2.Android 元件化 —— 這個概念實際跟上面相差不那麼明顯,元件和外掛較大的區別就是:元件是指通用及複用性較高的構件,比如圖片快取就可以看成一個元件被多個 App 共用;
 3.Android動態載入 — 這個實際是更高層次的概念,也有叫法是熱載入或 Android 動態部署,指容器(App)在運⾏狀態下動態載入某個模組,從而新增功能或改變某⼀部分行為. 

-- 1.DexClassLoader類 ,可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk。

    2.PathClassLoader類,只能載入已經安裝到Android系統中的apk檔案。

Android動態載入jar、apk的實現- https://blog.csdn.net/bboyfeiyu/article/details/11710497

-- Android 熱補丁動態修復框架小結- http://blog.csdn.net/lmj623565791/article/details/49883661/
  熱修復入門:Android 中的 ClassLoader。ClassLoader 是個抽象類,其具體實現的子類有 BaseDexClassLoader 和SecureClassLoader。
  SecureClassLoader 的子類是 URLClassLoader,其只能用來載入 jar 檔案,這在 Android 的 Dalvik/ART 上沒法使用的。
  BaseDexClassLoader 的子類是 PathClassLoader和 DexClassLoader。
> Dalvik,ART 

 JVM 及 Dalvik ART對類唯一的識別是 ClassLoader id + PackageName + ClassName。
  Android 也有自己的 ClassLoader,分為 dalvik.system.DexClassLoader 和 dalvik.system.PathClassLoader。  
  關於動態載入apk,理論上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
 DexClassLoader :可以載入外部的 apk、jar 或 dex檔案,並且會在指定的outpath 路徑存放其 dex 檔案
 PathClassLoader:可以載入/data/app目錄下的apk,這也意味著,它只支援直接操作dex檔案或只能載入已經安裝的apk,不能直接從 zip 包中得到 dex
 URLClassLoader :可以載入java中的jar,但是由於dalvik不能直接識別jar,所以此方法在android中無法使用,儘管還有這個類 
   
  在Android系統中,一個App的所有程式碼都在一個Dex檔案裡面。Dex是一個類似Jar的儲存了多有Java編譯位元組碼的歸檔檔案。PathClassLoader不能主動從zip包中釋放出dex,因此只支援直接操作dex格式檔案,或者已經安裝的apk(因為已經安裝的apk在cache中存在快取的dex檔案)。而DexClassLoader可以支援.apk、.jar和.dex檔案,並且會在指定的outpath路徑釋放出dex檔案。

> Android 外掛化
  對於虛擬機器來說,其實所有的程式碼都是在執行時被載入進來的。而不同於C語言還存在著靜態連結。虛擬機器在所有Java程式碼執行之前被啟動,然後開始把位元組碼載入到環境中執行,我們可以理解成所有的程式碼都是動態載入到虛擬機器裡的。
  在目前的軟硬體環境下,Native App與Web App在使用者體驗上有著明顯的優勢,但在實際專案中有些會因為業務的頻繁變更而頻繁的升級客戶端,造成較差的使用者體驗,而這也恰恰是Web App的優勢。
 關於外掛,已經在各大平臺上出現過很多,eclipse外掛、chrome外掛、3dmax外掛,所有這些外掛大概都為了在一個主程式中實現比較通用的功能,把業務相關或者讓可以讓使用者自定義擴充套件的功能不附加在主程式中,主程式可在執行時安裝和解除安裝。

   在android如何實現外掛也已經被廣泛傳播,實現的原理都是實現一套外掛介面,把外掛實現編成apk或者dex,然後在執行時使用DexClassLoader動態載入進來,這裡分享一下DexClassLoader載入原理和分析在實現外掛時不同操作造成錯誤的原因。

  DexClassLoader是一個可以從包含classes.dex實體的.jar或.apk檔案中載入classes的類載入器。可以用於實現dex的動態載入、程式碼熱更新等等。能夠載入自定義的jar/apk/dex;
  PathClassLoader提供一個簡單的ClassLoader實現,可以操作在本地檔案系統的檔案列表或目錄中的classes,但不可以從網路中載入classes。只能載入系統中已經安裝過的apk;
  所以Android系統預設的類載入器為PathClassLoader,而DexClassLoader可以像JVM的ClassLoader一樣提供動態載入。
  很多部落格裡說PathClassLoader只能載入已安裝的apk的dex,其實這說的應該是在dalvik虛擬機器上,在art虛擬機器上PathClassLoader可以載入未安裝的apk的dex(在art平臺上已驗證)。

  Dalvik採用的是JIT技術,位元組碼都需要通過即時編譯器(just in time ,JIT)轉換為機器碼,這會拖慢應用的執行效率,而ART採用Ahead-of-time(AOT)技術,應用在第一次安裝的時候,位元組碼就會預先編譯成機器碼,這個過程叫做預編譯。ART同時也改善了效能、垃圾回收(Garbage Collection)、應用程式除錯以及效能分析。但是請注意,執行時記憶體佔用空間較少同樣意味著編譯二進位制需要更高的儲存。 

  Android中類載入器有BootClassLoader,URLClassLoader,PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最終繼承自java.lang.ClassLoader。
 自定義ClassLoader和雙親委派機制,JVM中的類的載入機制。

> Android 外掛化 Sample
1.先來回顧一下如何在Android平臺下做外掛吧,首先定義一個外掛介面IPlugin(其實不使用介面也可以,在載入類的時候直接使用反射呼叫相關類,但寫程式碼來比較蛋疼):
public interface IPlugin {
  public String getName();
  public String getVersion();
  public void show();
}

public abstract class AbsPlugin {
  public abstract String getName();
  public abstract String getVersion();
  public abstract void show();
}

2.寫好這個介面後,匯出這個IPlugin生成jar包,這個相當於SDK了,然後新建一個工程並,這個工程以引用方式(即eclipse中externallibrary)引用這個包後,實現這個介面:
public class PluginImp extends AbsPlugin {
  public String getName() {
    return "PluginImp";
  }

  public String getVersion() {
    return "1.0";
  }

  public void show() {
    android.util.Log.("PluginImp", "ha ha I'm pluginimp");
  }
}

3.編譯這個工程並生成apk或者匯出實現類生成dex,這時就做好了我們的外掛實體,最後在我們的主工程裡把外掛介面的jar(即外掛SDK)放在lib目錄下在apk編譯時打包進來,同時用下面的程式碼在需要的時候載入進來呼叫:
try {
  ClassLoader classLoader= context.getClassLoader() ;
  DexClassLoader localDexClass Loader = newDexClassLoader("/sdcard/plugin.apk", dexoutputpath, null ,classLoader) ;
  //load class
  Class localClass = localDexClassLoader.loadClass("org.cmdmac.plugin.PluginImpl");

  //construct instance
  Constructor localConstructor = localClass.getConstructor(new Class[] {});
  Object instance = localConstructor.newInstance(new Object[] {});

  //call method
  IPlugin plugin = (IPlugin)instance;
  plugin.show ();
 } catch (Excpetion e) {
  //To do something
}

-- 原理剖析,這樣我們就實現了一個簡單的外掛,現在來問兩個問題:
 a.為什麼外掛SDK要放在lib目錄?放在lib目錄和非lib目錄以external方式引用的區別是什麼?
 b.為什麼外掛SDK只能匯出介面,在外掛工程裡要以external方式引用又不是放在lib目錄了?

-- 在回答這兩個問題之前,我們來做下實驗:
  a.主工程不把外掛sdk放在lib目錄下,而是以external方式引用,外掛SDK和外掛工程引用的方式不變。這時在執行時會產生如下錯誤:
java.lang.ClassNotFoundException: PluginImpl
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:61)
at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
at org.cmdmac.host.MainActivity.onCreate(MainActivity.java:23)
at android.app.Activity.performCreate(Activity.java:5084)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
at
……

  b.在外掛工程裡把SDK放到lib目錄下,主工程引用方式不變,會出現下面的錯誤
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
dalvik.system.DexFile.defineClass(Native Method)
dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211)
dalvik.system.DexPathList.findClass(DexPathList.java:315)
dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.j

  c.在外掛工程把SDK放到lib目錄下,載入的classloader改為:
ClassLoaderclassLoader=ClassLoader.getSystemClassLoader();

-- 會出現下面的錯誤:
java.lang.ClassCastException: org.cmdmac.plugin.PluginImp cannot be cast to org.cmdmac.pluginsdk.AbsPlugin
com.example.org.cmdmac.host.test.MainActivity.onCreate(MainActivity.java:30)
android.app.Activity.performCreate(Activity.java:5084)
android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1079)
com.lbe.security.service.core.client.internal.InstrumentationDelegate.callActivityOnCreate(InstrumentationDelegate.java:76)
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2044)

這些錯誤是怎麼來的?解析答案得從JAVA類載入原理出發:
Java的類載入器一般為URLClassLoader,在Android裡是不能用的,取而代之的是DexClassLoader和PathClassLoader。
Java中的類載入器大致可以分成兩類,一類是系統提供的,另外一類則是由Java應用開發人員編寫的。系統提供的類載入器主要有下面三個:
  a.引導類載入器(bootstrapclassloader):它用來載入Java的核心庫,是用原生程式碼來實現的,並不繼承自java.lang.ClassLoader。
  b.擴充套件類載入器(extensionsclassloader):它用來載入Java的擴充套件庫。Java虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入Java類。
  c.系統類載入器(systemclassloader):它根據Java應用的類路徑(CLASSPATH)來載入Java類。一般來說,Java應用的類都是由它來完成載入的。可以通過ClassLoader.getSystemClassLoader()來獲取它。

  類載入器在嘗試自己去查詢某個類的位元組程式碼並定義它時,會先代理給其父類載入器,由父類載入器先去嘗試載入這個類,依次類推。在介紹代理模式背後的動機之前,首先需要說明一下Java虛擬機器是如何判定兩個Java類是相同的。Java虛擬機器不僅要看類的全名是否相同,還要看載入此類的類載入器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。比如一個Java類com.example.Sample,編譯之後生成了位元組程式碼檔案Sample.class。兩個不同的類載入器ClassLoaderA和ClassLoaderB分別讀取了這個Sample.class檔案,並定義出兩個java.lang.Class類的例項來表示這個類。這兩個例項是不相同的。對於Java虛擬機器來說,它們是不同的類。試圖對這兩個類的物件進行相互賦值,會丟擲執行時異常ClassCastException。

-- 由java類載入器原理可以得到如下答案:
關於第一個錯誤:
  Android預設的類載入器是PathClassLoader那麼:ClassLoaderclassLoader=context.getClassLoader();
  這個得到的結果就是PathClassLoader,它載入了一個apk或者dex裡的所有類,當以exteral方式引用時,由於生成的主工程的apk是沒有把介面類打包進來的,這時使用PathClassLoader去載入時也是沒有載入到Impl的,由於PathClassLoader是父載入器,它找不到就會使用類載入器本身(即DexClassLoader)去查詢,他去查詢時發現需要引用AbsPlugin和IPlugin,這時再去找了一圈,也是沒有找到,因此出現ClasNotFound錯誤。

關於第二個錯誤:
  第二個錯誤是由於主工程和外掛都包含和外掛的介面,這時使用PathClassLoader在主工程查詢時找到AbsPlugin和IPlguin,用DexClassLoader載入Impl時因為也會載入AbsPlugin和IPlugin,但這時使用DexClassLoader在plugin.apk也找到了,因此出現兩個相同類的但是由不同的類載入器載入的,就出現了這個錯誤,這個錯誤型別出錯的程式碼可以檢視Resolve.cpp的dvmResolveClass函式。

關於第三個錯誤:
  這個錯誤是在型別轉換的時候出現,原因也是兩個不同的基類,但原因不同,是因為使用SystemClassLoader載入時只能在plugin.apk裡找到,但在進行型別轉換時查詢AbsPlugin和IPlugin是在主工程中查詢的,這時的情況下,主工程的AbsPlugin和Impl繼承的AbsPlugin是在不同的類載入器載入的,不能進行型別轉換了。
 

相關文章