Android插件化技术入门
插件化概述
提到插件化,就不得不提起方法数超过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个。
下面是插件化框架的一些对比,下面引用https://github.com/wequick/Small/blob/master/Android/COMPARISION.md。
插件化框架对比.png 插件化框架对比.png[1] 独立插件:一个完整的apk包,可以独立运行。比如从你的程序跑起淘宝、QQ,但这加载起来是要闹哪样?
非独立插件:依赖于宿主,宿主是个壳,插件可使用其资源代码并分离之以最小化,这才是业务需要嘛。
-- “所有不能加载非独立插件的插件化框架都是耍流氓”。
[2] ACDD加载.so用了Native方法(libdexopt.so),不是Java层,源码见dexopt.cpp。
[3] Service更新频度低,可预先注册在宿主的manifest中,如果没有很好的理由说服我,现不支持。
[4] 要实现宿主、各个插件资源可互相访问,需要对他们的资源进行分段处理以避免冲突。
[5] 这些框架修改aapt源码、重编、覆盖SDK Manager下载的aapt,我只想说_“杀(wan)鸡(de)焉(kai)用(xin)牛(jiu)刀(hao)”。Small使用gradle-small-plugin,在后期修改二进制文件,实现了PP_段分区。
[6] 使用public-padding对资源id的_TT_段进行分区,分开了宿主和插件。但是插件之间无法分段。
[7] 除了宿主提供一些公共资源与代码外,我们仍需封装一些业务层面的公共库,这些库被其他插件所依赖。公共插件打包的目的就是可以单独更新公共库插件,并且相关插件不需要动到。
[8] AppCompat: Android Studio默认添加的主题包,Google主推的Metrial Design包也依赖于此。大势所趋。
[9] 联调插件:使用Android Studio调试宿主时,可直接在插件代码中添加断点调试。
插件化框架对比.png
插件化的原理
通过上面的框架介绍,插件化的原理无非就是这些:
- 通过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());
}
注:有关Android资源加载机制的可以参考《Android源码与设计模式》这本书。
最后,我们就可以访问到插件的资源了(这里只给出核心代码):
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的启动流程。
相关的文章:http://www.jianshu.com/p/69bfbda302df
写在最后
通过上面的例子我们体验了插件式开发的精髓,学习这些例子是为了更好的研究市面上的插件化框架,了解它们实现原理,明白这些框架对项目以及插件的侵入性、修改这些框架以适应自己的项目等。当然有兴趣的可以自己做一个。
在学习插件化的时候,需要掌握Android系统的一些Framework层面的知识以及一些编程相关的知识,其中包括:
- Binder机制
- Android系统、APP、Activity等四大组件的启动流程
- APK安装过程
- Android资源的加载过程
- Hook机制
- 面向接口(抽象)编程
- 面向切面编程
- 等等
相关的参考资料有:
- 《Android开发艺术探索》
- 《Android源码与设计模式》
- 写给Android App开发人员看的Android底层知识:http://www.cnblogs.com/Jax/p/6864103.html
- Small主页:https://github.com/wequick/Small
- Small使用介绍:http://www.jianshu.com/p/7990714d10cb
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
公众号:Android开发进阶我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。