Android-插件化技术之我也来入个门-DexClassLoa
最近完全投入Android开发一年左右了,中间也是一直补知识。到现在,还是补了蛮多的。 布局上用约束布局很爽,应该没啥大问题。 负责的布局,rv多type用的多,另外阿里的Vlayout也有尝试,还有一些其他框架,有看过一些三方框架源码,貌似也是多布局的封装,还蛮骚的样子。自定义View之前搞过,流程基本ok,问题不会太大。然后到了后面自己封装了弹窗库,新项目也用到了(近期弹窗计划正在针对地区选择进行封装,封装后正好下一个版本迭代用上),另外Android公共组件库正在考虑中,因为做了几个项目,基本很多控件都是类似的配置,而且有些还是很重复的操作,所以打算再搞一个公共组件库(当然其中包括涉及到自定义View、方便用户配置)。简单回味下....
然后一方面小萌新再看一些源码,一方面打算抽点时间再深入下其他方面,比如插件化、热修复等,想想还是蛮重要的勒!
插件化的原理相关介绍:
1. 通过DexClassLoader加载。
2. 代理模式添加生命周期。
3. Hook思想跳过清单验证。
好吧,先尝试实践下DexClassLoader加载吧,参考网友的操作我们来过一下流程! 后面就开始着手做一些较深入的分析,顺便结合相关官方资料来加深印象!
Tips: DexClassLoader.loadClass()加载后可以如下方式调用插件的方法
//通过反射调用插件的代码
//通过接口调用插件的代码(其中包括较为完善的面向切面编程调用插件的方法)
**A. **试试反射的方式:
1. 创建工程
image2. 新建一个Module- plugin
image image3. 然后plugin模块下新建一个被调用的方法,比如 PluginTest.java, 并提供如下操作
public class PluginTest {
private String feature = "不帅";
public String getFeature() {
return feature;
}
public void setFeature(String feature) {
this.feature = feature;
}
}
4. 然后打包这个模块为apk
image image5. 将plugin下的apk拷贝到app模块下的assets目录下
image6. 搞工具类将assets目录下的plugin-debug.apk拷贝到应用目录下,比如/data/user/0/popeeee.hl.com.plugin/files/Download/下,这样可以避免还需要动态申请存储权限的问题
image7. 然后就可以进行拷贝操作了哟,成功后进行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准备存储的应用本地缓存路径
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. 给当前控件添加一个点击事件,然后点击通过DexClassLoader.loadClass()加载插件对应的类,然后通过反射获取对应的方法进行调用, 之前关于反射的学习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();
///< 然后通过反射获取对应的方法
Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
setFeatureMethod.setAccessible(true);
Method getFeatureMethod = mClass.getMethod("getFeature");
getFeatureMethod.setAccessible(true);
///< 然后执行对应方法进行相关设置和获取
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. 当点击hello world后就可以看见回调信息了呀。。。
image以上方式加载过程都ok。 不过很多人都是把拷贝apk放到如下地方进行调用(其实拷贝很快的,不一定要放到这里?ContextWrapper类的源码,ContextWrapper中有一个attachBaseContext()方法,这个方法会将传入的一个Context参数赋值给mBase对象,之后mBase对象就有值了。
Application中在onCreate()方法里去初始化各种全局的变量数据是一种比较推荐的做法,但是如果你想把初始化的时间点提前到极致,也可以去重写attachBaseContext()方法),同时加载apk时进行一个简单判断:
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
///< 获取apk准备存储的应用本地缓存路径
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需要考虑第三个参数
this.dexClassLoader = new DexClassLoader(apkPath, apkDexPath, null, this.getClassLoader());
}
B. 上面的加载方法还是略显复杂,有点麻烦了,如果加载的对象可以直接转换为PluginTest对象岂不是妙哉!
由于app模块并没有这个PluginTeset类,所以没法这样操作,有个做法是,把插件的类复制一份到app模块,然后直接强制转换即可!试试是可以滴了....
image这样也是没问题的。但是这样很麻烦呀,你想想,一旦插件要加个什么东西都需要拷贝一份,太烦了。 所以我们需要一个公共库,宿主和插件都依赖它,然后由它提供相关的实体类接口,这样只要都继承对应接口即可,维护起来也方便很多呢!
1. 新建一个插件库(主要是与插件对应)
image image2. 新建实体类对应的公共接口 PluginProvider.java
public interface PluginProvider {
String getFeature();
void setFeature(String feature);
}
image
3. 宿主和插件都依赖该库,修改插件实体类继承自PluginProvider
image image- 重新打包插件apk,更新到assets目录下替换之前的插件
5. 然后宿主调用插件方式做一下改变,只需要强转为PluginProvider即可,不依赖于插件具体的实体类类型
///< 面向接口编程调用插件代码
PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
pluginProvider.setFeature("不帅么?");
///< 然后本地进行一些提示等操作
Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
image
然后就ojbk了。
image**C. **有时候我们希望通过回调的方式调用插件的方法,因为插件还要做很多事情才能回调给宿主(比如插件需要去下载皮肤主题资源,然后解压校验,成功后才能通知宿主进行相关设置),此时我们就采用接口编程回调的方式实现。回调我们经常用啦,问题不大哈...
1. 公共插件库中我们定义一个回调接口,并提供一个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 继承实现IDynamic的方法,给出回调(利用线程做一个模拟)
import popeeee.hl.com.pluginlibrary.ICallBack;
import popeeee.hl.com.pluginlibrary.IDynamic;
public class Dynamic implements IDynamic {
@Override
public void invokeCallBack(final ICallBack callBack) {
///< 操作获取某些信息,然后回调给宿主
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
PluginTest pluginTest = new PluginTest();
pluginTest.setFeature("我来自互联网,我标志了人类的一大进步!“呸,不要脸!");
callBack.callback(pluginTest);
}
}).start();
}
}
3. 然后宿主app此时不再加载对应的实体类(因为你加载了实体类也只是自己设置,自己获取信息,没什么卵用!)。 此时我们加载Dynamic类,然后调用插件的invoke方法来请求网络等操作获取我们真实想要的数据....
记得重新打包plugin模块的apk,更新下下
然后修改下加载实体类并且进行强制转换
image /**
* 默认hello world文本框添加点击事件 android:onClick="CallPlugin"
* @param view
*/
public void CallPlugin(View view) {
try {
///< 加载插件的类(插件的包名.类名)
Class<?> mClass = dexClassLoader.loadClass("popeeee.hl.com.plugin.Dynamic");
/// 1\. 反射方式调用
// ///< 获取类的实例
// Object beanObject = mClass.newInstance();
//
// ///< 然后通过反射获取对应的方法
// Method setFeatureMethod = mClass.getMethod("setFeature", String.class);
// setFeatureMethod.setAccessible(true);
// Method getFeatureMethod = mClass.getMethod("getFeature");
// getFeatureMethod.setAccessible(true);
//
// ///< 然后执行对应方法进行相关设置和获取
// setFeatureMethod.invoke(beanObject, "丑的不行呀!");
// String feature = (String) getFeatureMethod.invoke(beanObject);
// ///< 然后本地进行一些提示等操作
// Toast.makeText(this, feature, Toast.LENGTH_SHORT).show();
// /// 2\. 强制转换对应包含操作方法的对象
// PluginTest pluginTest = (PluginTest) mClass.newInstance();
// pluginTest.setFeature("丑的还可以呀2!");
//
// ///< 然后本地进行一些提示等操作
// Toast.makeText(this, pluginTest.getFeature(), Toast.LENGTH_SHORT).show();
// ///< 面向接口编程调用插件代码
// PluginProvider pluginProvider = (PluginProvider) mClass.newInstance();
// pluginProvider.setFeature("不帅么?");
//
// ///< 然后本地进行一些提示等操作
// Toast.makeText(this, pluginProvider.getFeature(), Toast.LENGTH_SHORT).show();
///< 面向切面编程调用插件代码
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();
}
}
这样就可以了
image到这里插件的入门算是有所了解,另外自己亲自实践了一把,感觉还是不一样的。另外还有插件的两个入门点,一个是插件资源的加载,一个是插件的Activity的加载启动。这个两个小萌新要后面再搞。
搞的前提:1. 小萌新要去了解资源加载相关的机制,原理,源码的解读 2. 同样Activity的加载也是需要解读一些源码方可深入些。 另外如果对ClassLoader还在陌生的话,有必要去看下官方api,做一个解读了....
Demo下载地址还是贴下吧,万一需要了 https://gitee.com/heyclock/doc/blob/master/PluginTest/PluginTest.zip
先到这,贴几个我觉得不错的文章,共勉之,一起加油, 很多东西还是要自己实践...还得有自己理解!