Android插件化入門
插件化是什么
用通俗易懂的話就是隔缀,它就像我們的U盤煮剧,可以機(jī)時(shí)的插在個(gè)電腦闻镶。不單單的U盤甚脉,還有顯示器,顯卡和CPU等電腦配件都是可以插入主板提供的接口铆农,這些電腦組件通過主板提示的接口組合在一起就可以組成一部具有完整功能的電腦牺氨。
即然電腦可以以這種形式進(jìn)行組裝,哪我們android程序是不是也可以這樣墩剖?答案是肯定的猴凹,我們的各個(gè)獨(dú)立的功能模塊都可以打包成apk,讓宿主程序把a(bǔ)pk加載進(jìn)來岭皂,再運(yùn)行里面的各個(gè)activity,service等
插件化的分類
插件化在技術(shù)難度上可以為分兩種:獨(dú)立插件化和非獨(dú)立插件
非獨(dú)立插件是宿主程序與插件發(fā)開約定好插件開發(fā)規(guī)則郊霎,插件開發(fā)者要遵循這個(gè)規(guī)劃進(jìn)行開發(fā),這種方式要求的技術(shù)難度相對來說低很多爷绘,但是增加了開發(fā)者的調(diào)用成本书劝。類似比較成熟的解新局面方案有Small,需要說明的是small支持Android和iOS兩個(gè)平臺(tái)
獨(dú)立插件完全支持android的兼容四大組件的大部份的屬性,這種方式對于開發(fā)者可以說是完全透明,是十分完善的插件化解決方案土至。但是偏寫這類框架需要對android底層的代碼非常熟悉购对,要對種種api進(jìn)行hook處理,所以技術(shù)要求也非常的高陶因。類似比較成熟的方案有DroidPlugin等等
插件化優(yōu)缺點(diǎn)
-
優(yōu)點(diǎn)
模塊間的解耦
解除單個(gè)dex方法65535的限制
動(dòng)態(tài)更新骡苞,使我們的運(yùn)營更加的靈活
-
缺點(diǎn)
增加了程序開發(fā)的復(fù)雜度
技術(shù)門檻更高
非獨(dú)立插件原理
因?yàn)槲覀兿螺d的apk里面activity沒有在宿入的manifest.xml注冊,如果我們直接調(diào)用startActivity方法楷扬,就會(huì)報(bào)activity沒有注冊的異常烙如。我們可以在宿主activity中先注冊一個(gè)代理Activity,然后通過宿主activity去調(diào)用插件里面的activity的方法毅否。
哪我們怎樣去加載我們插件apk里面的類呢亚铁?下面讓我們了解下java里面幾個(gè)類加載器:
DexClassLoader :可以加載文件系統(tǒng)上的jar、dex螟加、apk
PathClassLoader :可以加載/data/app目錄下的apk徘溢,這也意味著,它只能加載已經(jīng)安裝的apk
URLClassLoader :可以加載java中的jar捆探,但是由于dalvik不能直接識(shí)別jar然爆,所以此方法在android中無法使用,盡管還有這個(gè)類
由上面的解釋可以了解到我們可以通過DexClassLoader把插件apk里面的類加載出來讓我們使用黍图。
非獨(dú)立插件實(shí)現(xiàn)
首先我們先摸擬apk下載的流程曾雕,把a(bǔ)ssets目錄下面的apk下載下來,存放在緩存目錄中,但是下載下來我們還不能直接使用助被,我們還要對apk包進(jìn)行以下處理
解密我們下載下來的apk包
校驗(yàn)apk的簽名是否正確
校驗(yàn)apk包所需的權(quán)限是否在主包中都包函
File mApkPath = this.getDir(ProxyActivity.APK_PATH, MODE_PRIVATE);
try {
String mApkName = "myapplication-release-unsigned.apk";
InputStream inputStream = getResources().getAssets().open(mApkName);
byte[] datas = FileUtil.readFromInputStream(inputStream);
String fullPath = mApkPath.getAbsolutePath() + File.separator + mApkName;
FileUtil.writeByteToFile(datas, new File(fullPath));
} catch (IOException e) {
e.printStackTrace();
}
然后我們就啟動(dòng)插件剖张,但是由于非獨(dú)立插件開發(fā)的時(shí)切诀,宿主程序?qū)Σ寮K是沒有用引的,顯示啟動(dòng)啟件的方式顯然是行不通的搔弄,所以只能通過隱式調(diào)用,具體調(diào)用如下幅虑。
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra(ProxyActivity.APK_FILE_PATH, "myapplication-release-unsigned.apk");
intent.putExtra(ProxyActivity.ACTIVITY_NAME, "com.sundar.myapplication.LoginActivity");
startActivity(intent);
細(xì)心的讀者或者己經(jīng)發(fā)現(xiàn),上面即然提到我們宿主程對插件是沒用引用的顾犹,那為什么intent創(chuàng)建時(shí)我們會(huì)帶上ProxyActivity的類呢倒庵?
其實(shí)我們是通過代理Activity來對插件apk進(jìn)行加載,然后通過宿主ProxyActivity來實(shí)現(xiàn)插件的生命周期調(diào)用炫刷,下面給出實(shí)現(xiàn)代碼
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
try {
Intent intent = getIntent();
mApkName = intent.getStringExtra(APK_FILE_PATH);
mActivityName = intent.getStringExtra(ACTIVITY_NAME);
} catch (Exception ignore) {
//
}
// 創(chuàng)建相關(guān)路徑
createFile();
// 把當(dāng)前的apk放到資源查找目錄中
mCustomAssetManager = new CustomAssetManager();
mCustomAssetManager.addAssetPath(fullPath);
// 創(chuàng)建classLoader加載類
// dex解壓釋放后的目錄
File dexOutputDir = getDir(DEX_OP, 0);
// apk存放的路徑
String fullPath = mApkPath.getAbsolutePath() + File.separator + mApkName;
// 定義DexClassLoader
// 第一個(gè)參數(shù):是dex壓縮文件的路徑
// 第二個(gè)參數(shù):是dex解壓縮后存放的目錄
// 第三個(gè)參數(shù):是C/C++依賴的本地庫文件目錄,可以為null
// 第四個(gè)參數(shù):是上一級的類加載器
mDexClassLoader = new DexClassLoader(fullPath, dexOutputDir.getAbsolutePath(), mSoPath.getAbsolutePath(), getClassLoader());
// 通過代理的方法去生成Activity類
try {
Class<PluginActivity> pluginActivityClass = (Class<PluginActivity>) mDexClassLoader.loadClass(mActivityName);
mPluginActivity = pluginActivityClass.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// 加載類錯(cuò)誤擎宝,需要顯示錯(cuò)誤信息給用戶
// 此類是插件activity
if (mPluginActivity == null) {
return;
}
//進(jìn)行必要的初始化
mPluginActivity.setmBaseActivity(this);
// 實(shí)現(xiàn)對插件activity方法進(jìn)行調(diào)用
mPluginActivity.onCreate(savedInstanceState);
}
下面是創(chuàng)建插件Activity所需要的AssetsAssetManager用于加載插件資源的
public CustomAssetManager() {
try {
this.mAssetManager = AssetManager.class.newInstance();
this.mAddedAssetsPath = new HashMap<>();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 添加apk的資源路徑放到AssetManager里面
*/
public void addAssetPath(String apkPath) {
if (mAssetManager == null) {
return;
}
//先判斷Map里面有沒有己經(jīng)添加的資源
if (mAddedAssetsPath.containsKey(apkPath)) {
return;
}
try {
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
mAssetManager, apkPath);
mAddedAssetsPath.put(apkPath, apkPath);
} catch (Exception e) {
e.printStackTrace();
}
}
下面是我們插件Activity類的主要方法,大家或許會(huì)覺得好寄為什么要重寫activity的方法呢浑玛?
答案是因?yàn)槲覀內(nèi)绻恢貙懼苯釉O(shè)置就會(huì)報(bào)找不到相應(yīng)的資源的異常绍申。那我們是不是必須要重寫這個(gè)方法呢?其實(shí)不是锄奢,我們可以把我們的插件apk添加到宿主apk的assetsManager里面,但是這里還會(huì)涉到到資源沖突的問題剧腻,具體解決資源沖突的方式可以自行百度
@Override
public void setContentView(@LayoutRes int layoutResID) {
Resources resources = mBaseActivity.getmCustomAssetManager().getBundleResource(mBaseActivity);
XmlResourceParser xmlResourceParser = resources.getLayout(layoutResID);
View view = LayoutInflater.from(mBaseActivity).inflate(xmlResourceParser, null);
mBaseActivity.setContentView(view);
}
下面就是我們的效果圖
非獨(dú)立插件實(shí)現(xiàn)總結(jié)
以上就是非獨(dú)立插件的實(shí)現(xiàn)過程拘央,但是上面只是做了實(shí)現(xiàn)的原理,在做demo過程中书在,我遇到幾個(gè)坑
因?yàn)閍ndroid是通過AssetsManager去加載資源的灰伟,此時(shí)如果在配置文件中使用資源id去引用資源,系統(tǒng)則會(huì)拋出找不到資源的異常儒旬,而我們現(xiàn)在只能自己創(chuàng)建AssetsManager去獲取apk包的資源
從上面的代碼可以知道栏账,插件的Activity的生成周期的調(diào)用只能通過代理Activity方法去調(diào)用
插件模塊開發(fā)增加難度,插件開發(fā)者必須要尊守開發(fā)的規(guī)則
后續(xù)要做的事情
是否可以參考動(dòng)態(tài)換膚的機(jī)制來實(shí)現(xiàn)對插件的資源進(jìn)行加載栈源,這樣我們就可以直接使用配置文件里的資源挡爵,具體可以參考http://www.reibang.com/p/af7c0585dd5b