最近完全投入Android開發(fā)一年左右了靶累,中間也是一直補知識岭妖。到現(xiàn)在晾匠,還是補了蠻多的姑食。 布局上用約束布局很爽褥紫,應(yīng)該沒啥大問題送朱。 負責(zé)的布局熙尉,rv多type用的多橘沥,另外阿里的Vlayout也有嘗試臀晃,還有一些其他框架觉渴,有看過一些三方框架源碼,貌似也是多布局的封裝徽惋,還蠻騷的樣子案淋。自定義View之前搞過,流程基本ok险绘,問題不會太大踢京。然后到了后面自己封裝了彈窗庫誉碴,新項目也用到了(近期彈窗計劃正在針對地區(qū)選擇進行封裝,封裝后正好下一個版本迭代用上)瓣距,另外Android公共組件庫正在考慮中黔帕,因為做了幾個項目,基本很多控件都是類似的配置蹈丸,而且有些還是很重復(fù)的操作成黄,所以打算再搞一個公共組件庫(當(dāng)然其中包括涉及到自定義View、方便用戶配置)逻杖。簡單回味下....
然后一方面小萌新再看一些源碼奋岁,一方面打算抽點時間再深入下其他方面,比如插件化弧腥、熱修復(fù)等厦取,想想還是蠻重要的勒!
插件化的原理相關(guān)介紹:
1. 通過DexClassLoader加載管搪。
2. 代理模式添加生命周期虾攻。
3. Hook思想跳過清單驗證。
好吧更鲁,先嘗試實踐下DexClassLoader加載吧霎箍,參考網(wǎng)友的操作我們來過一下流程! 后面就開始著手做一些較深入的分析澡为,順便結(jié)合相關(guān)官方資料來加深印象漂坏!
Tips: DexClassLoader.loadClass()加載后可以如下方式調(diào)用插件的方法
//通過反射調(diào)用插件的代碼
//通過接口調(diào)用插件的代碼(其中包括較為完善的面向切面編程調(diào)用插件的方法)
**A. **試試反射的方式:
1. 創(chuàng)建工程
2. 新建一個Module- plugin
3. 然后plugin模塊下新建一個被調(diào)用的方法,比如 PluginTest.java, 并提供如下操作
public class PluginTest {
private String feature = "不帥";
public String getFeature() {
return feature;
}
public void setFeature(String feature) {
this.feature = feature;
}
}
4. 然后打包這個模塊為apk
5. 將plugin下的apk拷貝到app模塊下的assets目錄下
6. 搞工具類將assets目錄下的plugin-debug.apk拷貝到應(yīng)用目錄下,比如/data/user/0/popeeee.hl.com.plugin/files/Download/下,這樣可以避免還需要動態(tài)申請存儲權(quán)限的問題
7. 然后就可以進行拷貝操作了喲媒至,成功后進行apk的裝載
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import popeeee.hl.com.plugin.utils.FileUtil;
import popeeee.hl.com.plugin.utils.SystemUtils;
public class MainActivity extends AppCompatActivity {
private String pluginApkName = "plugin-debug.apk"; ///< 插件apk名稱
private String apkPath; ///< apk存儲路徑
private String apkDexPath; ///< apk解壓dex的目錄顶别、和apk存放路徑為一個路徑
private DexClassLoader dexClassLoader;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
///< 獲取apk準備存儲的應(yīng)用本地緩存路徑
this.apkDexPath = SystemUtils.getCacheDirectory(this, Environment.DIRECTORY_DOWNLOADS).getPath();
///< 拷貝assets下的plugin-debug.apk到apkPath目錄并獲取實際路徑
this.apkPath = FileUtil.copyFilesFromAssets(this, pluginApkName, apkDexPath);
///< 加載apk并獲取DexClassLoader對象
this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
}
}
8. 給當(dāng)前控件添加一個點擊事件,然后點擊通過DexClassLoader.loadClass()加載插件對應(yīng)的類拒啰,然后通過反射獲取對應(yīng)的方法進行調(diào)用驯绎, 之前關(guān)于反射的學(xué)習(xí)MonkeyLei:Android-自定義注解-控件注解等
/**
* 默認hello world文本框添加點擊事件 android:onClick="CallPlugin"
* @param view
*/
public void CallPlugin(View view) {
try {
///< 加載插件的類(插件的包名.類名)
Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.PluginTest");
///< 獲取類的實例
Object beanObject = mClass.newInstance();
///< 然后通過反射獲取對應(yīng)的方法
Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
setFeatureMethod.setAccessible(true);
Method getFeatureMethod = mClass.getMethod("getFeature");
getFeatureMethod.setAccessible(true);
///< 然后執(zhí)行對應(yīng)方法進行相關(guān)設(shè)置和獲取
setFeatureMethod.invoke(beanObject, "丑的不行呀!");
String feature = (String) getFeatureMethod.invoke(beanObject);
///< 然后本地進行一些提示等操作
Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
9. 當(dāng)點擊hello world后就可以看見回調(diào)信息了呀谋旦。剩失。。
以上方式加載過程都ok册着。 不過很多人都是把拷貝apk放到如下地方進行調(diào)用(其實拷貝很快的拴孤,不一定要放到這里?ContextWrapper類的源碼甲捏,ContextWrapper中有一個attachBaseContext()方法演熟,這個方法會將傳入的一個Context參數(shù)賦值給mBase對象,之后mBase對象就有值了司顿。
Application中在onCreate()方法里去初始化各種全局的變量數(shù)據(jù)是一種比較推薦的做法绽媒,但是如果你想把初始化的時間點提前到極致蚕冬,也可以去重寫attachBaseContext()方法),同時加載apk時進行一個簡單判斷:
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
///< 獲取apk準備存儲的應(yīng)用本地緩存路徑
this.apkDexPath = SystemUtils.getCacheDirectory(this, Environment.DIRECTORY_DOWNLOADS).getPath();
///< 拷貝assets下的plugin-debug.apk到apkPath目錄并獲取實際路徑
this.apkPath = FileUtil.copyFilesFromAssets(this, pluginApkName, apkDexPath);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
///< 判斷apk是否存在
File file = new File(apkPath);
if (!file.exists()){
Toast.makeText(this, "文件不存在", Toast.LENGTH_SHORT).show();
return;
}
///< 加載apk并獲取DexClassLoader對象是辕,如果有.so需要考慮第三個參數(shù)
this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
}
B. 上面的加載方法還是略顯復(fù)雜囤热,有點麻煩了,如果加載的對象可以直接轉(zhuǎn)換為PluginTest對象豈不是妙哉!
由于app模塊并沒有這個PluginTeset類获三,所以沒法這樣操作旁蔼,有個做法是,把插件的類復(fù)制一份到app模塊疙教,然后直接強制轉(zhuǎn)換即可棺聊!試試是可以滴了....
這樣也是沒問題的。但是這樣很麻煩呀贞谓,你想想限佩,一旦插件要加個什么東西都需要拷貝一份,太煩了裸弦。 所以我們需要一個公共庫祟同,宿主和插件都依賴它,然后由它提供相關(guān)的實體類接口理疙,這樣只要都繼承對應(yīng)接口即可晕城,維護起來也方便很多呢!
1. 新建一個插件庫(主要是與插件對應(yīng))
2. 新建實體類對應(yīng)的公共接口 PluginProvider.java
public interface PluginProvider {
String getFeature();
void setFeature(String feature);
}
3. 宿主和插件都依賴該庫窖贤,修改插件實體類繼承自PluginProvider
- 重新打包插件apk砖顷,更新到assets目錄下替換之前的插件
5. 然后宿主調(diào)用插件方式做一下改變,只需要強轉(zhuǎn)為PluginProvider即可赃梧,不依賴于插件具體的實體類類型
///< 面向接口編程調(diào)用插件代碼
PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
pluginProvider.setFeature("不帥么?");
///< 然后本地進行一些提示等操作
Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
然后就ojbk了滤蝠。
**C. **有時候我們希望通過回調(diào)的方式調(diào)用插件的方法,因為插件還要做很多事情才能回調(diào)給宿主(比如插件需要去下載皮膚主題資源授嘀,然后解壓校驗物咳,成功后才能通知宿主進行相關(guān)設(shè)置),此時我們就采用接口編程回調(diào)的方式實現(xiàn)粤攒∷回調(diào)我們經(jīng)常用啦囱持,問題不大哈...
1. 公共插件庫中我們定義一個回調(diào)接口夯接,并提供一個invokeCallBack(ICallBack callBack)方法. IDynamic.java
public interface IDynamic {
void invokeCallBack(ICallBack callBack);
}
public interface ICallBack {
void callback(PluginProvider pluginProvider);
}
public interface PluginProvider {
String getFeature();
void setFeature(String feature);
}
2. 然后插件模塊就可以新建一個Dynamic 繼承實現(xiàn)IDynamic的方法,給出回調(diào)(利用線程做一個模擬)
import popeeee.hl.com.pluginlibrary.ICallBack;
import popeeee.hl.com.pluginlibrary.IDynamic;
public class Dynamic implements IDynamic {
@Override
public void invokeCallBack(final ICallBack callBack) {
///< 操作獲取某些信息纷妆,然后回調(diào)給宿主
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
PluginTest pluginTest = new PluginTest();
pluginTest.setFeature("我來自互聯(lián)網(wǎng)盔几,我標志了人類的一大進步!“呸掩幢,不要臉!");
callBack.callback(pluginTest);
}
}).start();
}
}
3. 然后宿主app此時不再加載對應(yīng)的實體類(因為你加載了實體類也只是自己設(shè)置逊拍,自己獲取信息上鞠,沒什么卵用!)芯丧。 此時我們加載Dynamic類芍阎,然后調(diào)用插件的invoke方法來請求網(wǎng)絡(luò)等操作獲取我們真實想要的數(shù)據(jù)....
記得重新打包plugin模塊的apk,更新下下
然后修改下加載實體類并且進行強制轉(zhuǎn)換
/**
* 默認hello world文本框添加點擊事件 android:onClick="CallPlugin"
* @param view
*/
public void CallPlugin(View view) {
try {
///< 加載插件的類(插件的包名.類名)
Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.Dynamic");
/// 1\. 反射方式調(diào)用
// ///< 獲取類的實例
// Object beanObject = mClass.newInstance();
//
// ///< 然后通過反射獲取對應(yīng)的方法
// Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
// setFeatureMethod.setAccessible(true);
// Method getFeatureMethod = mClass.getMethod("getFeature");
// getFeatureMethod.setAccessible(true);
//
// ///< 然后執(zhí)行對應(yīng)方法進行相關(guān)設(shè)置和獲取
// setFeatureMethod.invoke(beanObject, "丑的不行呀缨恒!");
// String feature = (String) getFeatureMethod.invoke(beanObject);
// ///< 然后本地進行一些提示等操作
// Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();
// /// 2\. 強制轉(zhuǎn)換對應(yīng)包含操作方法的對象
// PluginTest pluginTest = (PluginTest) mClass.newInstance();
// pluginTest.setFeature("丑的還可以呀2谴咸!");
//
// ///< 然后本地進行一些提示等操作
// Toast.makeText(this, pluginTest.getFeature(), Toast.LENGTH_SHORT).show();
// ///< 面向接口編程調(diào)用插件代碼
// PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
// pluginProvider.setFeature("不帥么?");
//
// ///< 然后本地進行一些提示等操作
// Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
///< 面向切面編程調(diào)用插件代碼
IDynamic iDynamic = (IDynamic) mClass.newInstance();
iDynamic.invokeCallBack(new ICallBack() {
@Override
public void callback(PluginProvider pluginProvider) {
Looper.prepare();
///< 然后本地進行一些提示等操作
Toast.makeText(MainActivity.this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
Looper.loop();
}
});
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
這樣就可以了
到這里插件的入門算是有所了解,另外自己親自實踐了一把骗露,感覺還是不一樣的岭佳。另外還有插件的兩個入門點,一個是插件資源的加載萧锉,一個是插件的Activity的加載啟動珊随。這個兩個小萌新要后面再搞。
搞的前提:1. 小萌新要去了解資源加載相關(guān)的機制柿隙,原理叶洞,源碼的解讀 2. 同樣Activity的加載也是需要解讀一些源碼方可深入些。 另外如果對ClassLoader還在陌生的話优俘,有必要去看下官方api京办,做一個解讀了....
Demo下載地址還是貼下吧,萬一需要了 https://gitee.com/heyclock/doc/blob/master/PluginTest/PluginTest.zip
先到這帆焕,貼幾個我覺得不錯的文章惭婿,共勉之,一起加油, 很多東西還是要自己實踐...還得有自己理解叶雹!