什么是插件化开发
我的理解很简单,主应用就像一个插座,任何符合其要求的插头(插件应用),都可以通过这个插座来让自己从一个“死应用(未安装)”变成一个“活应用(运行)”。而对主应用来说,插上插头或者拔出插头,它都可以正常运行,只不过会丧失插头提供的额外功能。
这样,当我们需要为主应用添加功能时,在主应用中只需要编写一个功能入口即可,剩下的工作就是按照主应用提供的接口要求开发或者修改插件应用,从而完成新功能的接入。
使用了插件化开发的应用
比如我们最熟悉的支付宝,其大小只有60多MB,但是它却像一个应用市场一般,为我们提供了非常丰富的功能应用,比如淘票票(应用62.9MB)、共享单车(ofo:39.6MB、哈啰出行59.3MB…)等等,光这几个应用加起来就远比安装包大得多了。当然,支付宝很多功能都是采用H5实现的,但上面提到的三个应用则都是使用插件化实现的。那这是如何分辨出来的呢?
我们只需要打开开发者选项 -> 显示布局边界即可看到安卓每一个控件的布局边界,而在WebView中的控件,则无法识别,只会显示整个WebView的边界,由此我们就可以分辨出哪些是H5哪些是原生控件了。效果如下图:
我们可以明显的看到,每一个按钮,每一张图片都被标识了其布局边界,说明它们都是安卓原生控件,而不是使用WebView承载的。
而共享单车的选择页面却是使用WebView开发的,如下图:
我们看到中间部分就是一个WebView,只能看到最外圈的布局边界。其实这样的设计就很巧妙,我们知道,如果只需要修改H5,那么就不需要更新app,只需要更新H5就可以了,同时,这也是为了应对变化,例如下一次再添加一个XX共享单车,则只需要在H5中再添加一个按钮即可。而单个共享单车的页面又是使用原生应用,某方面的因素也是为了提高用户体验,毕竟H5的速度比起原生还是差了些。
插件化开发如何实现
通过前面的叙述我们了解到,所谓的插件,可以认为就是一个app,而这个app不需要安装就可以通过其他应用打开,那这是如何做到的呢?
其实这中间最重要的就是生命周期管理。我们都知道,Activity都有自己的生命周期函数,但这些函数的调用却不是由我们自己控制的,而是由系统调用的。当Art在启动一个Activity的过程中,就会为其注入上下文,也就是我们最熟悉的Context,有了Context,Activity就可以拿到当前应用的资源文件,从而就能获取到布局然后显示在屏幕上(当然,Context还有很多其他的用途)。但是,Art只能启动已经安装的应用中的AndroidManifet.xml中声明过的Activity,只有这样的Activity我们才可以通过调用Context.startActivity(Intent intent)来启动。
也就是说,未安装的应用的Activity无法通过Context.startActivity(Intent intent)启动,那我们又如何去启动一个无法启动的Activity呢?
如何启动一个无法启动的Activity
答案就是曲线救国啦:
没法启动未安装的?那就启动已安装的,也就是我们的主应用的Activity(后文简称宿主Activity)。
首先,我们分析下Activity最典型的特点是什么?
1.布局,用来显示界面;
2.生命周期,控制Activity的生老病死。
布局转移
先解决第一个问题,布局。
我们需要把插件应用中的Activity的布局显示出来,其实就只需要把它的布局显示在宿主Activity上就可以了。
传递生命周期
第二个问题,生命周期。
同样的,我们只需要把宿主Activity(由系统管理,有正常的生命周期回调)的生命周期,同步到插件应用的Activity中即可。例如:在宿主Activity的onStart()方法中调用插件应用的Activity的onStart()方法,这样就实现了生命周期的传递。
具体实现
接下来就是撸码环节。
统一接口
前面说到,插头要想插到插座上,就需要满足插座提供的要求,也就是接入规范,所以我们就来制定这个规范。
我们提供一个接口,要求接入时必须实现对应的方法,主要包括Activity的所有生命周期函数(onCreate, onStart, onResume, onPause, onStop, onDestroy…)以及事件处理函数(onBackPressed, onKeyDown, onTouchEvent…)等等。
PluginInterface.java
public interface PluginInterface {
/**
* 对应Activity生命周期方法-onCreate
* @param savedInstanceState 保存的状态信息
*/
void onCreate(Bundle savedInstanceState);
/**
* 对应Activity生命周期方法-onStart
*/
void onStart();
/**
* 对应Activity生命周期方法-onResume
*/
void onResume();
/**
* 对应Activity生命周期方法-onPause
*/
void onPause();
/**
* 对应Activity生命周期方法-onStop
*/
void onStop();
/**
* 对应Activity生命周期方法-onDestroy
*/
void onDestroy();
/**
* 对应Activity方法-onActivityResult
* @param requestCode 请求码
* @param resultCode 结果码
* @param data 回调数据
*/
void onActivityResult(int requestCode, int resultCode, Intent data);
/**
* 对应Activity方法-onRestoreInstanceState
* @param savedInstanceState 保存的状态
*/
void onRestoreInstanceState(Bundle savedInstanceState);
/**
* 对应Activity方法-onSaveInstanceState
* @param outState 保存的状态
*/
void onSaveInstanceState(Bundle outState);
/**
* 对应Activity方法-onKeyDown
* @param keyCode 按键码
* @param event 事件
* @return 是否消费事件
*/
boolean onKeyDown(int keyCode, KeyEvent event);
/**
* 对应Activity方法-onTouchEvent
* @param event 事件
* @return 是否消费事件
*/
boolean onTouchEvent(MotionEvent event);
/**
* 对应Activity方法-onBackPressed
*/
void onBackPressed();
/**
* 宿主注入上下文
* @param activity 宿主activity
*/
void attach(Activity activity);
/**
* 移除宿主activity
*/
void detach();
}
这里额外添加了两个函数,attach和detach,attach是用来将宿主Activity的引用传递给插件应用的Activity,这样就可以在其中使用基本的Activity的方法了,detach则是为了防止内存泄漏啦,在onDestroy中移除宿主Activity的引用即可。
提供统一的BaseActivity
接口写好了,难道直接由插件应用的Activity去继承实现吗?当然不行啦,要是直接这样扔一个接口给开发者,那可能会比产品经理死得还惨。。。
所以我们需要对其再进行封装,尽量减少接入者的工作量,这样也有利于统一规范。
假设当前宿主Activity会正确回调上述接口中所有的方法(后文会详细介绍宿主Activity的编写),那么我们的插件Activity就有了生命周期了。那么在这里我们还需要重写所有跟Context有关的方法,因为插件Activity是没有上下文的,我们只能通过手动为其设置上下文,才能让它正确的调用Activity的方法和使用应用资源。
那么与Context有关的方法有哪些呢?
主要有setContentView(int layoutResID), findViewById(int id), getLayoutInflater(), getClassLoader(), getWindow() 以及 getWindowManager()等等,可能还有漏掉的,这里只列出了最常用的几个。
BasePluginActivity.java
public class BasePluginActivity extends AppCompatActivity implements PluginInterface {
/**
* 宿主Activity
*/
protected Activity mHostActivity;
/**
* 将布局设置到宿主Activity上
* @param layoutResID 布局资源ID
*/
@Override
public void setContentView(int layoutResID) {
if (mHostActivity == null) {
super.setContentView(layoutResID);
} else {
mHostActivity.setContentView(layoutResID);
}
}
/**
* 布局都设置到别人那了
* 自然只能去别人那要了
*/
@Override
public <T extends View> T findViewById(int id) {
if (mHostActivity == null) {
return super.findViewById(id);
} else {
return mHostActivity.findViewById(id);
}
}
/**
* 这里获取到的ClassLoader是使用插件应用的apk生成的DexClassLoader
*/
@Override
public ClassLoader getClassLoader() {
if (mHostActivity == null) {
return super.getClassLoader();
} else {
return mHostActivity.getClassLoader();
}
}
@NonNull
@Override
public LayoutInflater getLayoutInflater() {
if (mHostActivity == null) {
return super.getLayoutInflater();
} else {
return mHostActivity.getLayoutInflater();
}
}
@Override
public Window getWindow() {
if (mHostActivity == null) {
return super.getWindow();
} else {
return mHostActivity.getWindow();
}
}
@Override
public WindowManager getWindowManager() {
if (mHostActivity == null) {
return super.getWindowManager();
} else {
return mHostActivity.getWindowManager();
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (mHostActivity == null) {
super.onCreate(savedInstanceState);
}
}
@Override
public void onStart() {
if (mHostActivity == null) {
super.onStart();
}
}
@Override
public void onResume() {
if (mHostActivity == null) {
super.onResume();
}
}
@Override
public void onPause() {
if (mHostActivity == null) {
super.onPause();
}
}
@Override
public void onStop() {