過去的一兩年android插件化,熱修復(fù)等技術(shù)發(fā)展迅速,并且還在持續(xù)的探索中,也許插件化技術(shù)最終會在android工程中退出舞臺,但里面包含的技術(shù)是非常值得我們學(xué)習(xí)的。最近,會就android動態(tài)加載等技術(shù)進(jìn)行研究總結(jié)。
本篇文章作為插件化框架第一篇,首先分析android中的類加載器,并實(shí)現(xiàn)在android中動態(tài)加載一個外部apk中的類,從以下三部分進(jìn)行介紹。
一. java類加載器雙親委派機(jī)制
二. android中的類加載器介紹
三. android中動態(tài)加載實(shí)現(xiàn)類加載
一. java類加載器雙親委派機(jī)制
學(xué)過java的同學(xué)都知道類加載器是采用雙親委派機(jī)制來進(jìn)行類加載的。雙親委派機(jī)制從ClassLoader.java可以清晰的看出來。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);//判斷類是否已經(jīng)加載過
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);//父類加載器優(yōu)先加載
} 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
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//調(diào)用當(dāng)前類加載器的findClass方法進(jìn)行加載
// this is the defining class loader; record the stats
}
}
return c;
}
簡單來說,java的雙親委派機(jī)制分為三個過程,在ClassLoader的loadClass方法中會先判斷該類是否已經(jīng)加載,若加載了直接返回,若沒加載過則先調(diào)用父類加載器的loadClass方法進(jìn)行類加載,若父類加載器沒有找到,則會調(diào)用當(dāng)前正在查找的類加載器的findClass方法進(jìn)行加載。這里就涉及到類加載器的兩個很重要的方法loadClass和findClass。在自定義類加載器中會涉及到這兩個方法,具體二者有什么區(qū)別呢?
由上文的雙親委派機(jī)制的代碼可以看出來,如果想保證自定義的類加載器符合雙親委派機(jī)制,則覆寫findClass方法;如果想打破雙親委派機(jī)制,則覆寫loadClass方法。,雙親委派機(jī)制保證了同一個類不會被重復(fù)加載,但是某些情況下,是需要限定名相同的多個類被多個類加載器分別加載的,比如容器插件應(yīng)用場景。這時就可以在自定義類加載器時覆寫loadClass方法,擺脫雙親委派機(jī)制來直接加載。例如:
public class MyClassLoader extends DexClassLoader{
public MyClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, librarySearchPath, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if(xxx){//條件判斷是否自己加載
return this.loadClass(name);
}else{//雙親委派機(jī)制加載
return super.loadClass(name, resolve);
}
}
}
二. android中的類加載器介紹
android中的類加載器中主要包括三類BootClassLoader,PathClassLoader和DexClassLoader。
BootClassLoader主要用于加載系統(tǒng)的類,包括java和android系統(tǒng)的類庫。
PathClassLoader主要用于加載應(yīng)用內(nèi)中的類。路徑是固定的,只能加載
/data/app中的apk,無法指定解壓釋放dex的路徑。所以PathClassLoader是無法實(shí)現(xiàn)動態(tài)加載的。
DexClassLoader可以用于加載任意路徑的zip,jar或者apk文件??梢詫?shí)現(xiàn)動態(tài)加載。下面來具體看看應(yīng)用程序中的類加載器。
Log.i("ljj", "Context的類加載器:"+ Context.class.getClassLoader());
Log.i("ljj", "TextView的類加載器: "+ TextView.class.getClassLoader());
打印結(jié)果:
02-14 12:37:49.161 22341-22341/com.ljj.host I/ljj: Context的類加載器:java.lang.BootClassLoader@a645091
02-14 12:37:49.162 22341-22341/com.ljj.host I/ljj: TextView的類加載器: java.lang.BootClassLoader@a645091
可見系統(tǒng)的類都是由BootClassLoader加載完成。
Log.i("ljj", "classLoader:"+getClassLoader());
02-14 13:19:23.730 20518-20518/com.ljj.host I/ljj: classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.ljj.host-2/base.apk"],nativeLibraryDirectories=[/data/app/com.ljj.host-2/lib/arm64, /vendor/lib64, /system/lib64]]]
可見直接調(diào)用getClassLoader調(diào)用的是應(yīng)用的PathClassLoader,DexPathList為/data/app/com.ljj.host-2/base.apk。
除了BootClassLoader和應(yīng)用的PathClassLoader外,還有一個classLoader,比較難以理解,我們可以打印出來看看。
Log.i("ljj", "classLoader:"+ClassLoader.getSystemClassLoader());
02-14 13:32:01.747 4482-4482/com.ljj.host I/ljj: classLoader:dalvik.system.PathClassLoader[DexPathList[[directory "."],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
可見調(diào)用ClassLoader.getSystemClassLoader()得到的也是一個PathClassLoader,但是DexPathList為“.”。這就奇怪了,為什么路徑會為“.”,有必要查看一下源碼。
static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}
private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}
從源碼中可以看出,getSystemClassLoader()方法獲得的pathClassLoader的path是由classPath來指定的
String classPath = System.getProperty("java.class.path", ".");
打印發(fā)現(xiàn)輸出為".",沒有從源碼中找到對于"java.class.path"變量的賦值過程,希望了解的人可以指教一下。至于這個classLoader什么時候用,我的看法是當(dāng)我們自定義classLoader時,假設(shè)是一個插件工程,想與host工程不沖突,獨(dú)立運(yùn)行,關(guān)注插件工程中的類的加載,而不關(guān)注host工程中的類的加載造成的沖突,此時可以將自定義類加載器的parent指定為此classLoader。
至于PathClassLoader我們只要知道它的路徑是指定的,必須是已經(jīng)安裝的apk,應(yīng)用的classLoader默認(rèn)為PathClassLoader即可。下面我們將重點(diǎn)分析一下DexClassLoader。先從源碼的角度進(jìn)行簡單分析。
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
DexClassLoader的源碼很簡單,只包含一個構(gòu)造函數(shù),看來所有的工作都是在BaseDexClassLoader中完成的。這里再看BaseDexClassLoader前,先說一下DexClassLoader構(gòu)造函數(shù)的四個參數(shù)。
dexPath:是加載apk/dex/jar的路徑
optimizedDirectory:是dex的輸出路徑(因?yàn)榧虞dapk/jar的時候會解壓除dex文件,這個路徑就是保存dex文件的)
librarySearchPath:是加載的時候需要用到的lib庫,這個一般不用,可以傳入Null
parent:給DexClassLoader指定父加載器
下面繼續(xù)分析BaseClassLoader。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
可以看出,DexClassLoader會通過傳入的路徑構(gòu)造出一個DexPathList對象,作為pathList。從findClass方法可以看出來加載的類都是從pathList中查找。至于DexPathList對象的源碼就不往下具體分析了,簡單的理解就是將每個dex都構(gòu)建成Element元素,放入到dexElements數(shù)組中,多說一句,這個dexElements數(shù)組的用處很大,MultiDex方案以及由此衍生出的QQ空間熱更新方案都是通過改變dexElements數(shù)組的元素位置來實(shí)現(xiàn)的。感興趣的同學(xué)可以去學(xué)習(xí)一下。
三. android中動態(tài)加載實(shí)現(xiàn)類加載
類加載器知識介紹完畢后,我們來具體實(shí)現(xiàn)利用DexClassLoader來動態(tài)加載一個apk中的類。最簡單的實(shí)現(xiàn)方式,我們可以將一個java文件打包成jar或者轉(zhuǎn)化成dex后壓縮成apk,然后利用DexClassLoader加載后,反射調(diào)用里面的方法來驗(yàn)證效果,不過這個實(shí)例不太好說明問題,而且在項目中我們一般也不這樣用。
在項目中我們可能更多的是這樣使用,一個插件工程,一個宿主工程,二者間的聯(lián)系通過公共接口來完成。下面來具體操作以下。
第一步:將接口打包成jar
我們新建一個接口,打包出PayService.jar。
public interface IPay {
public void pay(int money);
public String getOrder(String userName);
public String getUserName();
}
我電腦里裝有eclipse,打jar包非常方便。如果沒有安裝eclipse,也可以用jar命令或者gradle任務(wù)來完成。
第二步:新建一個android工程,命名為PluginPro
將PayService.jar放入libs目錄,新建一個PayServiceImpl類實(shí)現(xiàn)IPay接口,為了方便起見,具體實(shí)現(xiàn)中只打印了log。
public class PayServiceImpl implements IPay {
@Override
public void pay(int money) {
Log.i("ljj", "pay: "+money+" 元");
}
@Override
public String getOrder(String s) {
return "0001";
}
@Override
public String getUserName() {
return "ljj";
}
}
以compile的形式打包進(jìn)apk。
compile fileTree(include: ['*.jar'], dir: 'libs')
第三步:新建一個android工程,命名為HostPro
將PluginPro工程生成的apk放入assets目錄下,至于為什么放入到assets目錄,目前插件框架中的插件都是放入到assets目錄上進(jìn)行打包的,這里雖然不存在插件框架,但是盡量模擬一下這種動態(tài)加載的場景。我們分為以下幾種情況進(jìn)行分析。
1. PayService.jar在PluginPro和HostPro均以compile的形式進(jìn)行依賴,DexClassLoader的parent設(shè)置為應(yīng)用默認(rèn)的PathClassLoader
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//創(chuàng)建apk加載路徑
String src=this.getFilesDir().getAbsolutePath()+ File.separator+"plugin.apk";
//作為odex的釋放路徑
String des= this.getFilesDir().getAbsolutePath()+ File.separator+"plugin"+File.separator;
try {
copyPlugin(src);//將assets下的插件apk拷貝到src路徑下
//創(chuàng)建DexClassLoader,parent指定為應(yīng)用默認(rèn)的PathClassLoader
DexClassLoader classLoader=new DexClassLoader(src,des,null,getClassLoader());
Class class1=classLoader.loadClass("com.ljj.plugin.serviceimpl.PayServiceImpl");
Object instance=class1.newInstance();
IPay payService=(IPay)instance;
String userName=payService.getUserName();
String order=payService.getOrder("ss");
payService.pay(10);
Log.i("ljj", "userName: "+userName);
Log.i("ljj", "order: "+order);
} catch (Exception e) {
e.printStackTrace();
}
}
private void copyPlugin(String path) throws Exception{
InputStream in=this.getAssets().open("app-release-unsigned.apk");
OutputStream os=new FileOutputStream(path);
byte[] temp=new byte[1024];
int len=-1;
while((len=in.read(temp))!=-1){
os.write(temp,0,len);
}
in.close();
os.flush();
os.close();
}
}
使用android5.0系統(tǒng)的手機(jī)進(jìn)行測試一把,結(jié)果如下,能夠正常運(yùn)行,正常調(diào)用到了插件apk中的函數(shù):
02-15 16:21:04.750 8875-8875/com.ljj.host I/art: Can not find class: Lcom/ljj/plugin/serviceimpl/PayServiceImpl;
02-15 16:21:04.750 8875-8875/com.ljj.host I/ljj: pay: 10 元
02-15 16:21:04.750 8875-8875/com.ljj.host I/ljj: userName: ljj
02-15 16:21:04.750 8875-8875/com.ljj.host I/ljj: order: 0001
保持代碼不變,又搞了個android4.2系統(tǒng)的手機(jī)測試一把,結(jié)果掛了:
02-15 08:50:32.346 3897-3897/? E/AndroidRuntime: FATAL EXCEPTION: main
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
at dalvik.system.DexFile.defineClass(Native Method)
at dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211)
at dalvik.system.DexPathList.findClass(DexPathList.java:313)
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:51)
at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
at com.ljj.host.MainActivity.onCreate(MainActivity.java:35)
我們來分析一下為什么出現(xiàn)這種結(jié)果。首先在插件apk和宿主apk中都包含了IPay接口,我們定義的DexClassLoader指定的parent為應(yīng)用默認(rèn)的PathClassLoader,兩個classLoader的DexPathList的路徑如下所示。之所以出現(xiàn)這種原因可能是art和Dalvik虛擬機(jī)的內(nèi)部加載細(xì)節(jié)的差異。下面針對兩種情況進(jìn)行分析。
02-15 09:27:28.020 6666-6666/? I/ljj: onCreate: dalvik.system.DexClassLoader[DexPathList[[zip file "/data/data/com.ljj.host/files/plugin.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
02-15 09:27:28.020 6666-6666/? I/ljj: onCreate: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.ljj.host-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.ljj.host-1, /vendor/lib, /system/lib]]]
在art虛擬機(jī)中,當(dāng)我們執(zhí)行l(wèi)oadClass("com.ljj.plugin.serviceimpl.PayServiceImpl");時,根據(jù)雙親委派機(jī)制,PathClassLoader會先在宿主apk中查找,此時肯定找不到"PayServiceImpl",接著由DexClassLoader從插件apk中查找PayServiceImpl,毫無疑問,可以查找PayServiceImpl,同時發(fā)現(xiàn)其實(shí)現(xiàn)了IPay接口,此時仍然會由PathClassLoader去宿主apk中查找,很明顯可以找到IPay接口,所以IPay接口是由應(yīng)用的PathClassLoader加載的。這樣就正常的完成了整個類加載過程。
在dalvik虛擬機(jī)中,虛擬機(jī)在首次加載dex的時候,會進(jìn)行dexopt過程,進(jìn)行預(yù)校驗(yàn)。具體來解釋下拋出此異常的原因。當(dāng)一個class文件和其直接引用的類在同一個dex中時,就會被打上CLASS_ISPREVERIFIED標(biāo)記,而如果加載過程中,發(fā)現(xiàn)該類和其引用又不是在同一個dex中加載的,此時就會拋出該異常,該異常是由Resolve.cpp的dvmResolveClass函數(shù)定義的。在本文的例子中,插件apk中,PayServiceImpl引用了IPay接口并且他們都在同一個插件dex中,所以PayServiceImpl會被標(biāo)記CLASS_ISPREVERIFIED,而IPay接口由雙親委派機(jī)制可以看出是由PathClassLoader在宿主apk中加載到的,此時虛擬機(jī)會認(rèn)為IPay接口不在插件dex中,與之前標(biāo)記的CLASS_ISPREVERIFIED沖突,從而拋出異常。很明顯,當(dāng)A類和其引用的B類如果不在同一個dex中,那么A類就不會被打上CLASS_ISPREVERIFIED標(biāo)記,那么我們插件采用provided方式,則虛擬機(jī)會發(fā)現(xiàn)插件中沒有IPay類,就不會對PayServiceImpl打標(biāo)記了,也就不會再拋異常了??蓞⒖嘉恼?a target="_blank">安卓App熱補(bǔ)丁動態(tài)修復(fù)技術(shù)介紹。這個問題就是QZone熱更新方案所遇到的坑。有興趣的可以了解下。
所以我們在插件開發(fā)中,盡量保證只在宿主(插件)中使用compile方式,而在插件可以(宿主)中使用provided進(jìn)行依賴,就可以完全避免java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation的產(chǎn)生。
下面我們使用分析好的方式繼續(xù)探索。
2. PayService.jar在PluginPro采用provided方式,在HostPro中以compile的形式進(jìn)行依賴,DexClassLoader的parent設(shè)置為系統(tǒng)默認(rèn)的PathClassLoader
代碼與上文中保持不變,可以正確的完成動態(tài)加載,但是我們嘗試著將parent設(shè)置為ClassLoader.getSystemClassLoader()
DexClassLoader classLoader=new DexClassLoader(src,des,null,ClassLoader.getSystemClassLoader());
再次運(yùn)行,會發(fā)現(xiàn)結(jié)果又報錯了。。。。
02-15 21:53:15.820 3070-3070/com.ljj.host W/System.err: java.lang.ClassNotFoundException: Didn't find class "com.ljj.plugin.serviceimpl.PayServiceImpl" on path: DexPathList[[zip file "/data/data/com.ljj.host/files/plugin.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]
02-15 21:53:15.820 3070-3070/com.ljj.host W/System.err: Caused by: java.lang.ClassNotFoundException: Didn't find class "com.ljj.test.interfaces.IPay" on path: DexPathList[[zip file "/data/data/com.ljj.host/files/plugin.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]
看log可以大致猜出來是由于沒有加載到IPay接口,導(dǎo)致PayServiceImpl失敗。宿主中明明有IPay接口,為什么會找不到呢,原因就在于我們的parent設(shè)置為了系統(tǒng)的pathClassLoader,前面分析了該類加載器的 DexPathList的路徑為".",也就是什么都加載不到,接著會轉(zhuǎn)由DexClassLoader去加載PayServiceImpl,要想成功加載PayServiceImpl,必須能夠加載到IPay接口,而插件是以provided的方式依賴的PayService.jar,所以DexClassLoader無法成功加載PayServiceImpl,也就出現(xiàn)了log中所示內(nèi)容。
3. PayService.jar在PluginPro和HostPro均以compile的形式進(jìn)行依賴,DexClassLoader的parent設(shè)置為系統(tǒng)默認(rèn)的PathClassLoader
02-15 22:16:35.750 19600-19600/com.ljj.host W/System.err: java.lang.ClassCastException: com.ljj.plugin.serviceimpl.PayServiceImpl cannot be cast to com.ljj.test.interfaces.IPay
02-15 22:16:35.750 19600-19600/com.ljj.host W/System.err: at com.ljj.host.MainActivity.onCreate(MainActivity.java:39)
02-15 22:16:35.750 19600-19600/com.ljj.host W/System.err: at android.app.Activity.performCreate(Activity.java:6013)
又出錯了,這次報了java.lang.ClassCastException,首先需要明白,Java虛擬機(jī)不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認(rèn)為兩個類是相同的。即便是同樣的字節(jié)代碼,被不同的類加載器加載之后所得到的類,也是不同的。當(dāng)我們調(diào)用loadClass的時候,很明顯,PayServiceImpl和其實(shí)現(xiàn)的接口IPay都是由DexClassLoader在插件apk中加載的,而執(zhí)行到IPay payService=(IPay)instance;時,宿主apk中的IPay是由應(yīng)用默認(rèn)的PathClassLoader加載的,二者并不是同一個類,所以強(qiáng)轉(zhuǎn)時會報錯。
通過以上三種情況,主要是為了更加深刻的理解類加載器的知識,同時得出我們在開發(fā)插件時,盡量避免出現(xiàn)插件和宿主中都compile依賴,只保證compile一次,其他provided即可,此外在自定義DexClassLoader的parent時要特別注意,不能隨意設(shè)置,一般設(shè)置成應(yīng)用默認(rèn)的classLoader。
關(guān)于android的類加載器相關(guān)知識就介紹到里,也可能有的地方理解的不到位,歡迎多多交流。
目前本人在公司負(fù)責(zé)熱修復(fù)相關(guān)的工作,主要是基于robust的熱修復(fù)相關(guān)工作。感興趣的同學(xué)歡迎進(jìn)群交流。
