插件化概述
提到插件化,就不得不提起方法数超过65535的问题,我们可以通过Dex分包来解决,同时也可以通过使用插件化开发来解决。插件化的概念就是由宿主APP去加载以及运行插件APP。
下面是一些插件化的优势:
- 在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件APP,这样分工更加明确。
- 各个模块封装成不同的插件APK,不同模块可以单独编译,提高了开发效率。
- 解决了上述的方法数超过限制的问题。
- 可以通过上线新的插件来解决线上的BUG,达到“热修复”的效果。
减小了宿主APK的体积。
下面是插件化开发的缺点:
插件化开发的APP不能在Google Play上线,也就是没有海外市场。
综上所述,如果您的APP不需要支持海外的话,还是可以考虑插件化开发的。
插件化、热修复(思想)的发展历程
- 2012年7月,AndroidDynamicLoader,大众点评,陶毅敏:思想是通过Fragment以及schema的方式实现的,这是一种可行的技术方案,但是还有限制太多,这意味这你的activity必须通过Fragment去实现,这在activity跳转和灵活性上有一定的不便,在实际的使用中会有一些很奇怪的bug不好解决,总之,这还是一种不是特别完备的动态加载技术。
- 2013年,23Code,自定义控件的动态下载:主要利用 Java ClassLoader 的原理,可动态加载的内容包括 apk、dex、jar等。
- 2014年初,Altas,阿里伯奎的技术分享:提出了插件化的思想以及一些思考的问题,相关资料比较少。
- 2014年底,Dynamic-load-apk,任玉刚:动态加载APK,通过Activity代理的方式给插件Activity添加生命周期。
- 2015年4月,OpenAltas/ACCD:Altas的开源项目,一款强大的Android非代理动态部署框架,目前已经处于稳定状态。
- 2015年8月,DroidPlugin,360的张勇:DroidPlugin 是360手机助手在 Android 系统上实现了一种新的插件机制:通过Hook思想来实现,它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
- 2015年9月,AndFix,阿里:通过NDK的Hook来实现热修复。
- 2015年11月,Nuwa,大众点评:通过dex分包方案实现热修复。
- 2015年底,Small,林光亮:打通了宿主与插件之间的资源与代码共享。
- 2016年4月,ZeusPlugin,掌阅:ZeusPlugin最大特点是:简单易懂,核心类只有6个,类总数只有13个。
插件化的原理
通过上面的框架介绍,插件化的原理无非就是这些:
- 通过DexClassLoader加载。
- 代理模式添加生命周期。
- Hook思想跳过清单验证。
- 插件化需要掌握一些系统底层的知识,比如说IPC,Android系统、APP、四大组件的启动过程,APK的安装过程。
插件化实战体验
通过DexClassLoader加载这个插件APK
下面写一个简单的例子,仅起到抛砖引玉的作用。
首先我们需要有一个插件APK,我们在里面放入一个类:
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里面,通过DexClassLoader加载这个插件APK,并且通过反射实例化Bean并调用Bean的方法。
public class MainActivity extends AppCompatActivity {
private ClassLoader mPluginClassLoader;
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
//把Assets里面的文件复制到 /data/data/包名/files 目录下
//注意:不同手机厂商可能目录不一样
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加载的时候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);
//通过DexClassLoader加载插件APK
mPluginClassLoader = new DexClassLoader(dexPath, fileReleasePath, null, getClassLoader());
//通过反射调用插件的代码
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();
}
}
});
}
}
这里需要注意的一点就是,我们最好先把经过验证的插件APK复制到宿主APP的files目录下面,这样保证了APK的安全性。然后通过DexClassLoader进行加载的时候,需要指定插件APK的路径以及解压之后的dex存放路径。
通过面向接口(抽象)编程调用插件的代码
通过反射调用插件的代码,为了简化代码提高可读性,这里引入面向接口(抽象)编程的思想。
首先我们需要添加一个pluginlibrary,我们的app以及plugin模块都要引用这个库pluginlibrary
可以看到,我们在pluginlibrary里面添加了IBean接口:
public interface IBean {
String getName();
void setName(String name);
}
然后plugin里面的Bean类实现这个接口,最后在宿主加载的时候,直接把创建的对象转换为这个接口就可以,省去了反射的一系列繁琐操作,这也就是一种面向接口(抽象)编程的思想:
//通过面向接口编程调用插件的代码
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();
通过面向切面程调用插件中的带回调方法
比如说现在插件里面有一个方法methodWithCallback,它被调用的时候,最终会回调宿主APP。
先在pluginlibrary添加一个接口专门用于宿主与插件的交互的:
public interface IDynamic {
void methodWithCallback(Callback callback);
}
其中的Callback是自定义的一个简单的接口:
public interface Callback {
void callback(IBean bean);
}
这个IDynamic的实现类由插件来实现:
public class Dynamic implements IDynamic {
@Override
public void methodWithCallback(Callback callback) {
Bean bean = new Bean();
bean.setName("璐宝宝");
//回调宿主APP的方法
callback.callback(bean);
}
}
这样我们就可以通过回调的方式实现了插件调用宿主的方法了。最终宿主的调用如下:
Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();
dynamic.methodWithCallback(new Callback() {
@Override
public void callback(IBean bean) {
//插件回调宿主
Toast.makeText(MainActivity.this, bean.getName(), Toast.LENGTH_SHORT).show();
}
});
宿主访问插件的资源文件
如果我们直接去加载插件的资源的话,就会报如下错误:
android.content.res.Resources$NotFoundException: String resource ID #0x7f060022
因为插件的资源没有被Android系统加载进来,那么我们就需要手动加载资源,主要是重写下面三个方法:
@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;
}
然后在合适的时机调用loadPluginResources方法来加载插件的资源:
/**
* 加载插件的资源:通过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());
}
最后,我们就可以访问到插件的资源了(这里只给出核心代码):
Class<?> dynamicClass = mPluginClassLoader.loadClass("com.nan.plugin.Dynamic");
IDynamic dynamic = (IDynamic) dynamicClass.newInstance();
String res = dynamic.methodWithResources(MainActivity.this);
Log.e(TAG, res);
无需验证启动Activity
我们可以利用Hook机制来启动一个没有在清单文件中注册的插件Activity。但是需要我们熟悉Activity的启动流程。