代碼(已適配android10)已上傳github中抹剩,親測可用撑帖,對你有用的話,記得star澳眷,謝謝
先上效果圖
前言
關(guān)于插件化網(wǎng)上比比皆是胡嘿,但很遺憾之前開發(fā)一直沒有真正遇到過插件化的公司項目。由于疫情原因換了家新公司并且提前轉(zhuǎn)正钳踊,這個項目也是我們用組件化從0開始重構(gòu)衷敌,目前已開發(fā)完成。最近領(lǐng)導(dǎo)說apk包體積太大了拓瞪,而且里面有個模塊缴罗,可以根據(jù)接口類型動態(tài)加載,所以這篇文章誕生了祭埂。
插件化概念
將整個app拆分成很多模塊面氓,每個模塊都是一個apk,最終打包的時候?qū)⑺拗鱝pk和插件apk分開打包蛆橡,插件apk通過動態(tài)下發(fā)到宿主apk侧但,實現(xiàn)了動態(tài)加載插件并大大減少了包體積。
插件化優(yōu)點
提高編譯速度:開發(fā)過程中航罗,每個模塊都是獨立開發(fā)的禀横,編譯的時候每次運行不需要都編譯所有的業(yè)務(wù)邏輯代碼,所以會適當(dāng)?shù)奶岣呶覀兊拈_發(fā)速度粥血;
業(yè)務(wù)模塊完全解耦:每個業(yè)務(wù)都是完全獨立的柏锄,這樣開發(fā)過程中每個模塊的功能改變和其他模塊沒有任何關(guān)系,甚至可以隨意的去掉某一部分功能复亏;
利于團隊開發(fā):插件開發(fā)是團隊開發(fā)中用的最多的一種開發(fā)模式趾娃,可以更加的去分工,每個組只需要負責(zé)自己的功能缔御,減少溝通成本提高開發(fā)效率抬闷;
動態(tài)更新插件,按需下載模塊:對于一些不怎么常用的功能,可以讓用戶按需下載模塊笤成,從而減少工程的大小评架,讓用戶在下載的時候能夠節(jié)省流量以及等待時間,而且功能升級的時候可以不更新主應(yīng)用只更新插件炕泳;
解決android 655535問題纵诞。
插件化誕生
舉個美團的例子,你就懂了
美食頁面有那么多應(yīng)用培遵,如果單純的用webview實現(xiàn)那里面的支付浙芙,地圖和圖片瀏覽等有點不切實際,或者全部寫一個app里面籽腕,那包體積少說也有200M嗡呼,可是你去應(yīng)用市場看到,也才80M左右皇耗,這時插件化出場了
實現(xiàn)插件化的方式:
- 插樁式(本篇文章講的就是這種方式)
- Hook方式南窗,這個到時也會學(xué)習(xí)一個Hook的效果。
- 反射廊宪,但是在Android9.0中有很多反射是用不了了,所以這種基本上不會用了女轿。
插樁式原理
一圖勝千言箭启,看圖
上圖右邊美團外賣是以一個單獨的apk(可以這樣理解:一個apk就是一個插件)存在的,宿主App(美團)想要打開插件(美團外賣)中的某一個Activity蛉迹,但是美團外賣這個插件很顯然是沒有上下文對象的【原因:因為此插件沒有安裝到手機上】傅寡,要想啟動Activity必須要解決上下文這個東西,所以此時就需要在宿主APP中插一個樁北救,聲明一個代理的Activity荐操,如下:
此時ProxyActivity是一個空殼,可是沒有顯示插件的東西呀珍策,怎么辦托启?其實是這樣的:
如何將一個未安裝的插件apk的Activity能顯示在這個代理ProxyActivity中呢?其實要想插件Activity顯示出來肯定得要調(diào)用它里面的生命周期方法攘宙,而對于插件而言就是將自己Activity中的各種生命周期方法通過接口對外暴露給宿主的ProxyActivity屯耸,然后插件Activity中需要的Context則是借用ProxyActivity,這樣最終就能達到我們調(diào)用的目的蹭劈,目的達成最終插件化也就這實現(xiàn)了疗绣。
所以實現(xiàn)宿主Activity跳轉(zhuǎn)插件化Activity,需要2樣?xùn)|西:
①暴露代理Activity的生命周期給插件化铺韧;
②提供上下文給插件化
開干
新建項目
宿主是app多矮,插件是orderfood,這里注意orderfood也是application
宿主app和插件orderfood新建完成哈打,此時需要一個接口來暴露插件orderfood Activity的生命周期塔逃,所以還需要定義一個宿主app和插件orderfood之間公共的library讯壶,里面會定義各種公共接口,這里起名library為:lib_plugin 患雏,如下:
添加依賴
然后添加對它的依賴
①暴露生命周期
然后在library中定義Activity生命周期的公共接口鹏溯,如下:
然后插件Activity得要將其生命周期方法對外暴露,所以需要實現(xiàn)這個接口:
但是如圖并未對接口中的方法進行重寫淹仑,因為這樣寫是不合適的丙挽,插件中肯定會有n個Activity的,所以需要抽取一個BaseActivity出來匀借,然后再由它來實現(xiàn)抽象接口才靠譜颜阐,所以:
②提供上下文給插件化app
上面提到過,插件是不會裝在手機上的apk吓肋,那么插件中的Activity是沒有上下文的
所以需要在BaseActivity中來先重寫一個這個方法
我們已經(jīng)在插件化把相應(yīng)的方法進行重寫凳怨,此時需要把代理的上下文傳給插件app,我們在宿主App新建一個代理Activity是鬼,并在清單文件注冊:
接下來就是把代理Activity上下文傳給插件化Activity中去肤舞,也就是如何調(diào)用BaseActivity中的attach()方法,這里就要用到反射了均蜜,這里需要知道要跳轉(zhuǎn)插件Activity的全類名李剖,所以這里通過Intent的參數(shù)傳進來,如下:
然后通過反射來獲取到要跳轉(zhuǎn)插件Activity的對象囤耳,由于插件的所有Activity都繼承了BaseActivity了篙顺,而BaseActivity又實現(xiàn)了公共模塊的PluginInterface接口,所以最終就可以調(diào)用attach方法充择,如下:
所以代理Activity中改成如下:
我只重寫了onStart() 和 onReusme()德玫,剩余的方法是一樣的。
在宿主中加載插件
對于加載插件一般有2種:內(nèi)置和外置椎麦。
內(nèi)置:就是插件的apk放在assert文件目錄中
外置:從服務(wù)器進行下載到手機sd卡上
不管哪種方式宰僧,都需要將插件的類加載進來才行,所以對宿主app的進行修改:
接下來就是加載插件了观挎,新建一個插件管理器
public class PluginManager {
private Context mContext;//插件的資源對象
private Resources pluginResource;
//插件的類加載器
private DexClassLoader dexClassLoader;
//插件的包信息類
private PackageInfo packageInfo;
private static PluginManager pluginManager = new PluginManager();
private PluginManager() {
}
public static PluginManager getInstance() {
return pluginManager;
}
public void setContext(Context context) {
this.mContext = context;
}
//加載插件apk
public void loadPlugin(String pluginPath) {
//獲取包管理器
PackageManager packageManager = mContext.getPackageManager();
//獲取插件的包信息類
packageInfo = packageManager.getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);
//插件解壓后的目錄
File pluginFile = mContext.getDir("plugin", Context.MODE_PRIVATE);
//獲取到類加載器
dexClassLoader = new DexClassLoader(pluginPath, pluginFile.getAbsolutePath(), null, mContext.getClassLoader());
//獲取到插件的資源對象
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginPath);
pluginResource = new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public Resources getPluginResource() {
return pluginResource;
}
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
}
public PackageInfo getPackageInfo() {
return packageInfo;
}
}
接下來打包插件撒桨,放到sd卡中:
打包成功后,如下:
然后改個名字為:orderfood.apk上傳到sd卡根目錄下:
接下來在宿主Activity中實現(xiàn)跳轉(zhuǎn)插件代碼
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//跳轉(zhuǎn)插件
public void skipPlugin(View view) {
PluginManager.getInstance().setContext(this);
PluginManager.getInstance().loadPlugin(Environment.getExternalStorageDirectory() + "/orderfood.apk");
PackageInfo packageInfo = PluginManager.getInstance().getPackageInfo();
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
//由于插件只有一個activity键兜,所以取數(shù)組第0個
intent.putExtra("className", packageInfo.activities[0].name);
startActivity(intent);
}
}
記得加權(quán)限
接下來運行app凤类,由于我的手機是Android10.0,所以對于sdcard的權(quán)限得要主動申請一下普气,這里就不寫申請的代碼了谜疤,主動到權(quán)限管理中先將其打開,如下:
代碼(已適配android10)已上傳github
中,親測可用