插件化项目总结
前言
先简单介绍一下Android插件化。很早之前已经有公司在研究这项技术,淘宝做得比较早,但淘宝的这项技术一直是保密的。直到2015年才陆续出现很多框架,Android插件化分成很多技术流派,实现的方式都不太一样。
Android大型项目中为了减小apk的体积,可以采用插件化的方法,即一些不常用的功能独立成插件,当用户需要的使用的时候再从服务器上下载回来,动态加载。这样就避免了为了满足所有用户需求而把功能全部打包到apk,导致apk体积的膨胀。所谓的插件,其实也是一个apk,但是一般都依赖正式对外发布的app,也叫宿主。
框架对比
Android插件化框架有很多,我们选取了市场上一些稳定的项目进行调研,调研项目如下:
DyLA : Dynamic-load-apk @singwhatiwanna, 百度
DyAPK : DynamicAPK @TediWang, 携程
DPG : DroidPlugin @cmzy, 360早期项目
RPG : RePlugin @360近期项目
APG : ApkPlug @北京点豆公司项目
功能
1、DyLA
[项目地址]:(https://github.com/singwhatiwanna/dynamic-load-apk)
项目目前状态:停止维护
是否加载独立插件:是
四大组建的支持:支持部分
1. 目前不支持service
2. 目前只支持动态注册广播
3. 插件apk必须实现DLBasePluginActivity,属于侵入式的
通信方式:
官方是否提供交互方案:否
交互难度:高
如何实现插件间通信:host与plugin共同引用的interface,然后通过interface来达到调用的效果
交互流程:
1 在 host中新建module plugininterface , 并添加接口类
2. 将plugin类反射出来
3. 反射出来之后,我们通过host 开始调用插件的方法
局限性:
1. 慎用this(接口除外):因为this指向的是当前对象,即apk中的activity,但是由于activity已经不是常规意义上的activity,所以this是没有意义的,但是如果this表示的是一个接口而不是context,比如activity实现了而一个接口,那么this继续有效。
2. 使用that:既然this不能用,那就用that,that是apk中activity的基类BaseActivity中的一个成员,它在apk安装运行的时候指向this,而在未安装的时候指向宿主程序中的代理activity,anyway,that is better than this。
3. activity的成员方法调用问题:原则来说,需要通过that来调用成员方法,但是由于大部分常用的api已经被重写,所以仅仅是针对部分api才需要通过that去调用用。同时,apk安装以后仍然可以正常运行。
4. 启动新activity的约束:启动外部activity不受限制,启动apk内部的activity有限制,首先由于apk中的activity没注册,所以不支持隐式调用,其次必须通过BaseActivity中定义的新方法startActivityByProxy和startActivityForResultByProxy,还有就是不支持LaunchMode。
5. 目前暂不支持Service、BroadcastReceiver等需要注册才能使用的组件,但广播可以采用代码动态注册。
2、DyAPK
[项目地址]:(https://github.com/CtripMobile/DynamicAPK)
项目目前状态:停止维护
是否加载独立插件:否
四大组建的支持:支持部分
1. 目前不支持service
2. 目前只支持动态注册广播
3. 支持Activity、ContentProvider组件
通信方式:
官方是否提供交互方案:否
交互难度:高
如何实现插件间通信:插件与宿主App通信方式只能使用安卓系统级别通信(跨进程通信) |
交互流程:
1. 约定aidl通信接口
2. 实现通信接口本地代理
局限性:
1. 项目没有详细的文档
2. 项目已于2年前停止维护
3、DPG
[项目地址]:(https://github.com/DroidPluginTeam/DroidPlugin)
项目目前状态:维护状态
是否加载独立插件:是
四大组建的支持:全部支持
1. 插件的四大组件完全不需要在Host程序中注册
2. 支持Service、Activity、BroadcastReceiver、ContentProvider四大组件
通信方式:
官方是否提供交互方案:否
交互难度:高
如何实现插件间通信:
如何实现插件间通信:插件与宿主App通信方式只能使用安卓系统级别通信(跨进程通信)
交互流程:
1. 约定aidl通信接口
2. 实现通信接口本地代理
局限性:
1. 无法Notification使用自定义资源发送,例如:一个通知自定义RemoteLayout,这意味着的Notification并且必须为空。contentView tickerView bigContentView headsUpContentView通过R.drawable.XXX定制的图标通知。框架会将其转换为Bitmap。
2. 无法限定指定Intent Filter为插入应用程序的Service,Activity,BroadcastReceiver 和ContentProvider。所以插件应用程序对于外部系统和应用程序是不可见的。
3. 缺乏Hook的Native层,从而APK(例如大多数游戏应用程序)与native代码不能被加载插件。
4、RPG
[项目地址]:(https://github.com/Qihoo360/RePlugin)
项目目前状态:近期项目
是否加载独立插件:是
四大组建的支持:全部支持
1. 插件的四大组件完全不需要在Host程序中注册
2. 支持Service、Activity、BroadcastReceiver、ContentProvider四大组
通信方式:
官方是否提供交互方案:否
交互难度:高
如何实现插件间通信:
如何实现插件间通信:插件与宿主App通信方式只能使用安卓系统级别通信(跨进程通信)
交互流程:
1. 约定aidl通信接口
2. 实现通信接口本地代理
局限性:
1. 插件权限声明无效,只认主程序的
2. 插件声明Target SDK无效,只认主程序的
3. 不支持Notification的Layout资源
4. 暂不支持“宿主和插件之间直接调用类和方法”(宿主和主程序形成父子类ClassLoader)方案
5、APG
[项目地址]:(http://www.apkplug.com)
项目目前状态:维护状态
是否加载独立插件:是
四大组建的支持:全部支持
1. 对于广播组件,按常规使用即可,无其他要求
2. provider组件需要在宿主配置代理标签
3. activity和service组件有两种使用方式,代理方式和穿透方式
通信方式:
官方是否提供交互方案:是
交互难度:中
如何实现插件间通信:支持3种通信方式,OSGI、Dispatch和RPC通信,官方推荐使用RPC方式。
交互流程:
1. 约定通信接口
2. ShareProcessor实现
3. 注册bundlerpc服务
局限性:
1. 暂不支持“宿主和插件之间直接调用类和方法”(宿主和主程序形成父子类ClassLoader)方案
2. 插件权限声明无效,只认主程序的
插件调用流程
1. DyLA
//插件文件
File plugin = new File(apkPath);
PluginItem item = new PluginItem();
//插件文件路径
item.pluginPath = plugin.getAbsolutePath();
//PackageInfo = PackageManager.getPackageArchiveInfo
item.packageInfo = DLUtils.getPackageInfo(this, item.pluginPath);
//launcherActivity
if (item.packageInfo.activities != null && item.packageInfo.activities.length > 0) {
item.launcherActivityName = item.packageInfo.activities[0].name;
}
//launcherService
if (item.packageInfo.services != null && item.packageInfo.services.length > 0) {
item.launcherServiceName = item.packageInfo.services[0].name;
}
//加载apk信息
DLPluginManager.getInstance(this).loadApk(item.pluginPath);
2. DyAPK
- 首先要做插件的一系列初始化
BundleCore.getInstance().init(this);
BundleCore.getInstance().ConfigLogger(true, 1);
Properties properties = new Properties();
properties.put("ctrip.android.sample.welcome", "ctrip.android.sample.WelcomeActivity"); // launch page
sharedPreferences = getSharedPreferences("bundlecore_configs", 0);
String lastBundleKey = sharedPreferences.getString("last_bundle_key", "");
bundleKey = buildBundleKey();
if (!TextUtils.equals(bundleKey, lastBundleKey)) {
properties.put("ctrip.bundle.init", "true");
isDexInstalled = false;
HotPatchManager.getInstance().purge();
}
BundleCore.getInstance().startup(properties);
if (isDexInstalled) {
HotPatchManager.getInstance().run();
BundleCore.getInstance().run();
}
else {
new Thread(new Runnable() {
@Override
public void run() {
try {
ZipFile zipFile = new ZipFile(getApplicationInfo().sourceDir);
List bundleFiles = getBundleEntryNames(zipFile, BundleCore.LIB_PATH, ".so");
if (bundleFiles != null && bundleFiles.size() > 0) {
processLibsBundles(zipFile, bundleFiles);
SharedPreferences.Editor edit = getSharedPreferences("bundlecore_configs", 0).edit();
edit.putString("last_bundle_key", bundleKey);
edit.commit();
}
else {
Log.e("Error Bundle", "not found bundle in apk");
}
if (zipFile != null) {
try {
zipFile.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
BundleCore.getInstance().run();
}
catch (IOException ex) {
ex.printStackTrace();
}
}
}
).start();}
- 启动插件
startActivity(new Intent(getApplicationContext(), Class.forName("xxx.xxx.xxx.MainActivity")));
- DPG
- 初始化
PluginHelper.getInstance().applicationAttachBaseContext(base);
super.attachBaseContext(base);
- 主要API调用
//1.插件安装、更新
PluginManager.getInstance().installPackage(String filepath, int flags);
//2.获取已安装插件:
List installedPlugin = PluginManager.getInstance().getInstalledPackages(PackageManager.GET_ACTIVITIES);
//3.启动插件:
Intent intent = mPackageManager.getLaunchIntentForPackage("com.yunda.com." + info.getName() + "plugin");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
- RPG
- 初始化
//继承RePlug的Application,并实现初始化
public class SampleApplication extends RePluginApplication {
@Override
protected RePluginConfig createConfig() {
//进行初始化
return c;
}
@Override
protected RePluginCallbacks createCallbacks() {
return new HostCallbacks(this);
}
/**
* 宿主针对RePlugin的自定义行为
*/
private class HostCallbacks extends RePluginCallbacks {
private static final String TAG = "HostCallbacks";
private HostCallbacks(Context context) {
super(context);
}
@Override
public boolean onPluginNotExistsForActivity(Context context, String plugin, Intent intent, int process) {
return super.onPluginNotExistsForActivity(context, plugin, intent, process);
}
}
private class HostEventCallbacks extends RePluginEventCallbacks {
private static final String TAG = "HostEventCallbacks";
public HostEventCallbacks(Context context) {
super(context);
}
@Override
public void onInstallPluginFailed(String path, InstallResult code) {
super.onInstallPluginFailed(path, code);
}
@Override
public void onStartActivityCompleted(String plugin, String activity, boolean result) {
// FIXME 当打开Activity成功时触发此逻辑,可在这里做一些APM、打点统计等相关工作
super.onStartActivityCompleted(plugin, activity, result);
}
}
- 主要API调用
//安装插件、升级插件
RePlugin.install("/sdcard/exam.apk");
//卸载插件
RePlugin.uninstall("exam");
//3.启动插件
Intent intent = new Intent(v.getContext(), xxx.class);
context.startActivity(intent);
- ApkPlug
- 初始化
FrameworkFactory.getInstance().start(null, this).getSystemBundleContext();
PlugManager.getInstance().init(this, bundleContext,publickey,isDebug);
- 主要API调用
//安装插件
PlugManager.getInstance().installPlug(Context context, PlugInfo plugInfo, OnInstallListener listener)
//获取云端插件信息
PlugManager.getInstance().getPlugInfo(GetPlugInfoRequest request, OnGetPlugInfoListener listener)
//获取本地安装插件的更新版本信息
checkAllLocalPlugVersion(Context context,final OnCheckVersionListener listener)
//使用云端插件更新
updataPlug(Bundle bundle,final OnUpdataListener listener)
//插件启动与停止
Bundle.start()
Bundle.stop()
小结
1. DyLA
1 宿主和插件没有任何联系,但是插件需要继承DLBasePluginActivity,这个不太友好
2 侵入式的,对插件apk的开发限制太多,例如:必须继承DLBasePluginActivity,启动时候必须调用startPluginActivity(new DLIntent(getPackageName(),TestActivity.class))
3 这个框架学习成本高,限制多,联调不方便,不建议使用
2. DyAPK
1. 这个框架争议比较多,且市场上的用例很少,没有参考用例
2. 项目于2年前停止维护,一些遗留问题没有解决,不建议使用
3. DPG
1. 宿主和插件完全隔离,插件不依赖宿主,可以独立安装运行
2. 低入侵设计,插件不需要继承任何类
3. 插件apk和普通apk一样的,所以插件开发没有门槛
4. 插件启动速度太慢,而且宿主只能调用插件的LaunchMode的Activity,不能调用其他Activity
5. 插件间通行没有单独的API调用
4. RPG
1. 宿主和插件完全隔离,插件不依赖宿主,可以独立安装运行
2. 低入侵设计,插件不需要继承任何类
3. 插件apk和普通apk一样的,所以插件开发没有门槛
4. 插件间通行没有单独的API调用
5. 文档尚在补充当中,有些API及使用方式还没有文档可以查阅
5. ApkPlug
1. 宿主和插件完全隔离,插件不依赖宿主,可以独立安装运行
2. 低入侵设计,插件不需要继承任何类
3. 插件apk和普通apk一样的,所以插件开发没有门槛
4. 支持3种通信方式,OSGI、Dispatch和RPC通信