前言
關于插件,已經在各大平臺上出現過很多,eclipse 插件、chrome 插件、3dmax 插件,所有這些插件大概都為了在一個主程序中實現比較通用的功能,把業務相關或者讓可以讓用戶自定義擴展的功能不附加在主程序中,主程序可在運行時安裝和卸載。
在 android 如何實現插件也已經被廣泛傳播,實現的原理都是實現一套插件接口,把插件實現編成 apk 或者 dex,然后在運行時使用 DexClassLoader 動態加載進來,這里分享一下 DexClassLoader 加載原理和分析在實現插件時不同操作造成錯誤的原因。
插件 Sample
先來回顧一下如何在 Android 平臺下做插件吧,首先定義一個插件接口 IPlugin(其實不使用接口也可以,在加載類的時候直接使用反射調用相關類,但寫代碼來比較蛋疼):
? ?
1 2 3 4 5 |
public interface IPlugin { public String getName(); public String getVersion(); public void show(); } |
1 2 3 4 5 |
public interface IPlugin { public String getName(); public String getVersion(); public void show(); } |
1 2 3 4 5 |
public abstract class AbsPlugin { public abstract String getName(); public abstract String getVersion(); public abstract void show(); } |
寫好這個接口后,導出這個 IPlugin生成 jar 包,這個相當于 SDK了,然后新建一個工程并,這個工程以引用方式(即 eclipse 中 external?library)引用這個包后,實現這個接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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"); } } |
編譯這個工程并生成 apk 或者導出實現類生成 dex?, 這時就做好了我們的插件實體,最后在我們的主工程里把插件接口的 jar(即插件 SDK)放在 lib 目錄下在 apk 編譯時打包進來,同時用下面的代碼在需要的時候加載進來調用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
try { ClassLoader classLoader= context.getClassLoader() ; DexClassLoader localDexClass Loader = new DexClassLoader("/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 } |
原理剖析
好,這樣我們就實現了一個簡單的插件, 現在來問兩個問題:
1. 為什么插件 SDK要放在 lib 目錄? 放在 lib 目錄和非 lib 目錄以 external 方式引用的區別是什么?
2. 為什么插件 SDK只能導出接口,在插件工程里要以 external 方式引用又不是放在 lib 目錄了?
在回答這兩個問題之前,我們來做下實驗:
1. 主工程不把插件 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
......
2. 在插件工程里把 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
3. 在插件工程把 SDK放到 lib 目錄下,加載的 classloader 改為:
?ClassLoader?classLoader=?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?應用開發人員編寫的。系統提供的類加載器主要有下面三個:
引導類加載器(bootstrap?class?loader):它用來加載?Java?的核心庫,是用原生代碼來實現的,并不繼承自?java.lang.ClassLoader。
擴展類加載器(extensions?class?loader):它用來加載?Java?的擴展庫。Java?虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄里面查找并加載?Java?類。
系統類加載器(system?class?loader):它根據?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 那么:
ClassLoader?classLoader=?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 是在不同的類加載器加載的,不能進行類型轉換了。
總結與思考:
好了,到現在為止,如何實現插件還有使用不同方式編譯加載出現的錯誤原因我們也知道了也就是 JAVA是用類加載器來加載類的,加載類時會先使用父的加載器加載后再從使用自己的父加載器去加載,同一個類不同的加載器加載的類也被認為不同的類。
下面探討另一個問題,有沒有可能在主程序里不打包插件 SDK也可以實現動態加載但現在的邏輯代碼不變?還有上面的例子做成插件的是普通的類,Activity能不能也一樣可以做到,升級插件了怎么辦?答案是兩個都可以實現,原理也是使用DexClassLoader,篇幅和利益關系這里不做介紹。
李白 2017 年 3 月 30 日
找外包,想要穩定靠譜、費用還低的外包商?難!
怕被坑?上空心 www.kxhtml.com 一家 100 元/頁的軟件開發云平臺!
在招人,海招海篩、培訓,到頭來上手還是慢!
用結果打臉!上空心 www.kxhtml.com 一家先看開發結果后付費的平臺!
想創業,有 idea? 到處找 CTO? 技術難關攻不破?
立即上線!上空心 www.kxhtml.com 一家開發神速火箭般輸出頁面的平臺!
LJ 2016 年 9 月 21 日
這時使用 PathClassLoader 在主工程查找時找到 AbsPlugin 和 IPlguin,用 DexClassLoader 加載 Impl 時因為也會加載 AbsPlugin 和 IPlugin, 但這時使用 DexClassLoader 在 plugin.apk 也找到了,因此出現兩個相同類的但是由不同的類加載器加載的 。 主程序是 PathClassLoader,但是里面調用一直都用 DexClassLoader,這樣加載的兩個類一直都不同嗎?那與插件何關呢?
LJ 2016 年 9 月 21 日
那我在主工程中使用 PathClassLoader 去加載可以嗎?
LJ 2016 年 9 月 21 日
主程序里不打包插件 SDK 實現動態加載,那就需要在插件在 libs 里面包含 SDK,然后調用代碼改成:Method pluginShow = mLoaderClass.getMethod(“show”, null); pluginShow.invoke(testBActivityObject, null);
zyb 2016 年 3 月 10 日
第二個問題最終是怎么解決的呢?
不只是看客 2016 年 3 月 16 日
把插件中與宿主相同的 jar 給去掉。 比如你的插件和宿主都引用了 suport-v7 包的話,就會出這個錯誤。
bobo 2016 年 1 月 14 日
尼瑪!有時講些不講些的廢文!直接給 demo 吧!
王莉莉 2016 年 1 月 1 日
真的很有用,謝謝啦!
王莉莉 2016 年 1 月 1 日
不錯!值得學習,博主繼續
了解一下插件 2015 年 12 月 25 日
這錯誤也太多了吧
busyboy 2015 年 4 月 3 日
Direct-Load-apk 是一個為 Android 研制的強大插件化系統,支持無約束啟動一個 apk
評術 – Android上APP實現動態打補丁的探索 2015 年 1 月 12 日
[…] 第一個想法是既然 Android 可以做到動態加載類,那能否通過 DexClassLoader 來加載補丁中與要替換的類名一致的 Class 呢?經過試驗發現這條路行不通。原因是即便兩個類的包名類名方法名等等完全一樣,但卻是兩個不同的 ClassLoader 加載的,虛擬機也會把他們當做兩個不同的類,運行時會報 java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation 的錯誤。具體可以參看一下這篇講 Android 插件原理的文章。http://www.5418yb.com/2014/04/android-cha-jian-yuan-li-pou-xi/ […]
哈里樂呵 2014 年 11 月 24 日
最近也在研究 android 插件化,現在遇到的問題類似樓主提到的問題 2,如果主工程和插件工程必須包含公共依賴包(jar),這個問題要如何解決
Cesare 2014 年 11 月 20 日
很不錯哦 學習了
雪玉龍 2014 年 8 月 5 日
我怎么總是不行啊,求調教啊。。。。有 demo 給看看么,,,,求
beer 2014 年 5 月 23 日
不需要設置 sharedUserId 嗎,這么都沒有提到。
beer 2014 年 5 月 24 日
是不用的,測試通過,多謝了.
cpy 2014 年 8 月 20 日
求發一個 demo,lib 下放 jar 包把我弄暈了。cpyacpy@163.com 謝謝
iptton 2014 年 4 月 18 日
先頂后看~