插件化概述
提到插件化,就不得不提起方法數(shù)超過(guò)65535的問(wèn)題,我們可以通過(guò)Dex分包來(lái)解決,同時(shí)也可以通過(guò)使用插件化開(kāi)發(fā)來(lái)解決。插件化的概念就是由宿主APP去加載以及運(yùn)行插件APP。
下面是一些插件化的優(yōu)勢(shì):
- 在一個(gè)大的項(xiàng)目里面,為了明確的分工,往往不同的團(tuán)隊(duì)負(fù)責(zé)不同的插件APP,這樣分工更加明確。
- 各個(gè)模塊封裝成不同的插件APK,不同模塊可以單獨(dú)編譯,提高了開(kāi)發(fā)效率。
- 解決了上述的方法數(shù)超過(guò)限制的問(wèn)題。
- 可以通過(guò)上線新的插件來(lái)解決線上的BUG,達(dá)到“熱修復(fù)”的效果。
- 減小了宿主APK的體積。
下面是插件化開(kāi)發(fā)的缺點(diǎn):
- 插件化開(kāi)發(fā)的APP不能在Google Play上線,也就是沒(méi)有海外市場(chǎng)。
綜上所述,如果您的APP不需要支持海外的話,還是可以考慮插件化開(kāi)發(fā)的。
插件化、熱修復(fù)(思想)的發(fā)展歷程
- 2012年7月,AndroidDynamicLoader,大眾點(diǎn)評(píng),陶毅敏:思想是通過(guò)Fragment以及schema的方式實(shí)現(xiàn)的,這是一種可行的技術(shù)方案,但是還有限制太多,這意味這你的activity必須通過(guò)Fragment去實(shí)現(xiàn),這在activity跳轉(zhuǎn)和靈活性上有一定的不便,在實(shí)際的使用中會(huì)有一些很奇怪的bug不好解決,總之,這還是一種不是特別完備的動(dòng)態(tài)加載技術(shù)。
- 2013年,23Code,自定義控件的動(dòng)態(tài)下載:主要利用 Java ClassLoader 的原理,可動(dòng)態(tài)加載的內(nèi)容包括 apk、dex、jar等。
- 2014年初,Altas,阿里伯奎的技術(shù)分享:提出了插件化的思想以及一些思考的問(wèn)題,相關(guān)資料比較少。
- 2014年底,Dynamic-load-apk,任玉剛:動(dòng)態(tài)加載APK,通過(guò)Activity代理的方式給插件Activity添加生命周期。
- 2015年4月,OpenAltas/ACCD:Altas的開(kāi)源項(xiàng)目,一款強(qiáng)大的Android非代理動(dòng)態(tài)部署框架,目前已經(jīng)處于穩(wěn)定狀態(tài)。
- 2015年8月,DroidPlugin,360的張勇:DroidPlugin 是360手機(jī)助手在 Android 系統(tǒng)上實(shí)現(xiàn)了一種新的插件機(jī)制:通過(guò)Hook思想來(lái)實(shí)現(xiàn),它可以在無(wú)需安裝、修改的情況下運(yùn)行APK文件,此機(jī)制對(duì)改進(jìn)大型APP的架構(gòu),實(shí)現(xiàn)多團(tuán)隊(duì)協(xié)作開(kāi)發(fā)具有一定的好處。
- 2015年9月,AndFix,阿里:通過(guò)NDK的Hook來(lái)實(shí)現(xiàn)熱修復(fù)。
- 2015年11月,Nuwa,大眾點(diǎn)評(píng):通過(guò)dex分包方案實(shí)現(xiàn)熱修復(fù)。
- 2015年底,Small,林光亮:打通了宿主與插件之間的資源與代碼共享。
- 2016年4月,ZeusPlugin,掌閱:ZeusPlugin最大特點(diǎn)是:簡(jiǎn)單易懂,核心類只有6個(gè),類總數(shù)只有13個(gè)。
下面是插件化框架的一些對(duì)比,下面引用https://github.com/wequick/Small/blob/master/Android/COMPARISION.md。


[1] 獨(dú)立插件:一個(gè)完整的apk包,可以獨(dú)立運(yùn)行。比如從你的程序跑起淘寶、QQ,但這加載起來(lái)是要鬧哪樣?
非獨(dú)立插件:依賴于宿主,宿主是個(gè)殼,插件可使用其資源代碼并分離之以最小化,這才是業(yè)務(wù)需要嘛。
-- “所有不能加載非獨(dú)立插件的插件化框架都是耍流氓”。
[2] ACDD加載.so用了Native方法(libdexopt.so),不是Java層,源碼見(jiàn)dexopt.cpp。
[3] Service更新頻度低,可預(yù)先注冊(cè)在宿主的manifest中,如果沒(méi)有很好的理由說(shuō)服我,現(xiàn)不支持。
[4] 要實(shí)現(xiàn)宿主、各個(gè)插件資源可互相訪問(wèn),需要對(duì)他們的資源進(jìn)行分段處理以避免沖突。
[5] 這些框架修改aapt源碼、重編、覆蓋SDK Manager下載的aapt,我只想說(shuō)_“殺(wan)雞(de)焉(kai)用(xin)牛(jiu)刀(hao)”。Small使用gradle-small-plugin,在后期修改二進(jìn)制文件,實(shí)現(xiàn)了PP_段分區(qū)。
[6] 使用public-padding對(duì)資源id的_TT_段進(jìn)行分區(qū),分開(kāi)了宿主和插件。但是插件之間無(wú)法分段。
[7] 除了宿主提供一些公共資源與代碼外,我們?nèi)孕璺庋b一些業(yè)務(wù)層面的公共庫(kù),這些庫(kù)被其他插件所依賴。公共插件打包的目的就是可以單獨(dú)更新公共庫(kù)插件,并且相關(guān)插件不需要?jiǎng)拥健?[8] AppCompat: Android Studio默認(rèn)添加的主題包,Google主推的Metrial Design包也依賴于此。大勢(shì)所趨。
[9] 聯(lián)調(diào)插件:使用Android Studio調(diào)試宿主時(shí),可直接在插件代碼中添加斷點(diǎn)調(diào)試。

插件化的原理
通過(guò)上面的框架介紹,插件化的原理無(wú)非就是這些:
- 通過(guò)DexClassLoader加載。
- 代理模式添加生命周期。
- Hook思想跳過(guò)清單驗(yàn)證。
插件化需要掌握一些系統(tǒng)底層的知識(shí),比如說(shuō)IPC,Android系統(tǒng)、APP、四大組件的啟動(dòng)過(guò)程,APK的安裝過(guò)程。
插件化實(shí)戰(zhàn)體驗(yàn)
通過(guò)DexClassLoader加載這個(gè)插件APK
下面寫(xiě)一個(gè)簡(jiǎn)單的例子,僅起到拋磚引玉的作用。
首先我們需要有一個(gè)插件APK,我們?cè)诶锩娣湃胍粋€(gè)類:
package com.nan.plugin;
/**
* Created by huannan on 2017/6/20.
*/
public class Bean {
private String name = "璐寶寶";
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后在宿主APP里面,通過(guò)DexClassLoader加載這個(gè)插件APK,并且通過(guò)反射實(shí)例化Bean并調(diào)用Bean的方法。
public class MainActivity extends AppCompatActivity {
private ClassLoader mPluginClassLoader;
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
//把Assets里面的文件復(fù)制到 /data/data/包名/files 目錄下
//注意:不同手機(jī)廠商可能目錄不一樣
Utils.extractAssets(newBase, "plugin-debug.apk");
} catch (Throwable throwable) {
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//插件APK路徑
// /data/user/0/com.nan.dynalmic/files/plugin-debug.apk
String dexPath = getFileStreamPath("plugin-debug.apk").getAbsolutePath();
//DexClassLoader加載的時(shí)候Dex文件釋放的路徑
// /data/user/0/com.nan.dynalmic/app_dex
String fileReleasePath = getDir("dex", Context.MODE_PRIVATE).getAbsolutePath();
Log.e("acy", dexPath);
Log.e("acy", fileReleasePath);
//通過(guò)DexClassLoader加載插件APK
mPluginClassLoader = new DexClassLoader(dexPath, fileReleasePath, null, getClassLoader());
//通過(guò)反射調(diào)用插件的代碼
findViewById(R.id.btn_1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Class<?> beanClass = mPluginClassLoader.loadClass("com.nan.plugin.Bean");
Object beanObject = beanClass.newInstance();
Method setNameMethod = beanClass.getMethod("setName", String.class);
setNameMethod.setAccessible(true);
Method getNameMethod = beanClass.getMethod("getName");
getNameMethod.setAccessible(true);
setNameMethod.invoke(beanObject, "huannan");
String name = (String) getNameMethod.invoke(beanObject);
Toast.makeText(MainActivity.this, name, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
這里需要注意的一點(diǎn)就是,我們最好先把經(jīng)過(guò)驗(yàn)證的插件APK復(fù)制到宿主APP的files目錄下面,這樣保證了APK的安全性。然后通過(guò)DexClassLoader進(jìn)行加載的時(shí)候,需要指定插件APK的路徑以及解壓之后的dex存放路徑。
通過(guò)面向接口(抽象)編程調(diào)用插件的代碼
上文介紹了通過(guò)反射調(diào)用插件的代碼,為了簡(jiǎn)化代碼提高可讀性,這里引入面向接口(抽象)編程的思想。
首先我們需要添加一個(gè)pluginlibrary,我們的app以及plugin模塊都要引用這個(gè)庫(kù)pluginlibrary,如下圖所示:

可以看到,我們?cè)趐luginlibrary里面添加了IBean接口:
public interface IBean {
String getName();
void setName(String name);
}
然后plugin里面的Bean類實(shí)現(xiàn)這個(gè)接口,最后在宿主加載的時(shí)候,直接把創(chuàng)建的對(duì)象轉(zhuǎn)換為這個(gè)接口就可以,省去了反射的一系列繁瑣操作,這也就是一種面向接口(抽象)編程的思想:
//通過(guò)面向接口編程調(diào)用插件的代碼
Class<?> beanClass = mPluginClassLoader.loadClass("com.nan.plugin.Bean");
IBean bean = (IBean) beanClass.newInstance();
bean.setName("test");
Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();
通過(guò)面向切面程調(diào)用插件中的帶回調(diào)方法
比如說(shuō)現(xiàn)在插件里面有一個(gè)方法methodWithCallback,它被調(diào)用的時(shí)候,最終會(huì)回調(diào)宿主APP。
先在pluginlibrary添加一個(gè)接口專門用于宿主與插件的交互的:
public interface IDynamic {
void methodWithCallback(Callback callback);
}
其中的Callback是自定義的一個(gè)簡(jiǎn)單的接口:
public interface Callback {
void callback(IBean bean);
}
這個(gè)IDynamic的實(shí)現(xiàn)類由插件來(lái)實(shí)現(xiàn):
public class Dynamic implements IDynamic {
@Override
public void methodWithCallback(Callback callback) {
Bean bean = new Bean();
bean.setName("璐寶寶");
//回調(diào)宿主APP的方法
callback.callback(bean);
}
}
這樣我們就可以通過(guò)回調(diào)的方式實(shí)現(xiàn)了插件調(diào)用宿主的方法了。最終宿主的調(diào)用如下:
Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();
dynamic.methodWithCallback(new Callback() {
@Override
public void callback(IBean bean) {
//插件回調(diào)宿主
Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();
}
});
宿主訪問(wèn)插件的資源文件
如果我們直接去加載插件的資源的話,就會(huì)報(bào)如下錯(cuò)誤:
android.content.res.Resources$NotFoundException: String resource ID #0x7f060022
因?yàn)椴寮馁Y源沒(méi)有被Android系統(tǒng)加載進(jìn)來(lái),那么我們就需要手動(dòng)加載資源,主要是重寫(xiě)下面三個(gè)方法:
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
@Override
public Resources.Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}
然后在合適的時(shí)機(jī)調(diào)用loadPluginResources方法來(lái)加載插件的資源:
/**
* 加載插件的資源:通過(guò)AssetManager添加插件的APK資源路徑
*/
protected void loadPluginResources() {
//反射加載資源
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
注:有關(guān)Android資源加載機(jī)制的可以參考《Android源碼與設(shè)計(jì)模式》這本書(shū)。
最后,我們就可以訪問(wèn)到插件的資源了(這里只給出核心代碼):
Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();
String res = dynamic.methodWithResources(MainActivity.this);
Log.e(TAG, res);
無(wú)需驗(yàn)證啟動(dòng)Activity
我們可以利用Hook機(jī)制來(lái)啟動(dòng)一個(gè)沒(méi)有在清單文件中注冊(cè)的插件Activity。但是需要我們熟悉Activity的啟動(dòng)流程。
相關(guān)的文章:http://www.itdecent.cn/p/69bfbda302df
寫(xiě)在最后
通過(guò)上面的例子我們體驗(yàn)了插件式開(kāi)發(fā)的精髓,學(xué)習(xí)這些例子是為了更好的研究市面上的插件化框架,了解它們實(shí)現(xiàn)原理,明白這些框架對(duì)項(xiàng)目以及插件的侵入性、修改這些框架以適應(yīng)自己的項(xiàng)目等。當(dāng)然有興趣的可以自己做一個(gè)。
在學(xué)習(xí)插件化的時(shí)候,需要掌握Android系統(tǒng)的一些Framework層面的知識(shí)以及一些編程相關(guān)的知識(shí),其中包括:
- Binder機(jī)制
- Android系統(tǒng)、APP、Activity等四大組件的啟動(dòng)流程
- APK安裝過(guò)程
- Android資源的加載過(guò)程
- Hook機(jī)制
- 面向接口(抽象)編程
- 面向切面編程
- 等等
相關(guān)的參考資料有:
- 《Android開(kāi)發(fā)藝術(shù)探索》
- 《Android源碼與設(shè)計(jì)模式》
- 寫(xiě)給Android App開(kāi)發(fā)人員看的Android底層知識(shí):http://www.cnblogs.com/Jax/p/6864103.html
- Small主頁(yè):https://github.com/wequick/Small
- Small使用介紹:http://www.itdecent.cn/p/7990714d10cb
如果覺(jué)得我的文字對(duì)你有所幫助的話,歡迎關(guān)注我的公眾號(hào):
我的群歡迎大家進(jìn)來(lái)探討各種技術(shù)與非技術(shù)的話題,有興趣的朋友們加我私人微信huannan88,我拉你進(jìn)群交(♂)流(♀)。