插件化——插桩式实现Activity跳转
代码(已适配android10)已上传github
中,亲测可用
先上效果图

前言
关于插件化网上比比皆是,但很遗憾之前开发一直没有真正遇到过插件化的公司项目。由于疫情原因换了家新公司并且提前转正,这个项目也是我们用组件化从0开始重构,目前已开发完成。最近领导说apk包体积太大了,而且里面有个模块,可以根据接口类型动态加载,所以这篇文章诞生了。
插件化概念
将整个app拆分成很多模块,每个模块都是一个apk,最终打包的时候将宿主apk和插件apk分开打包,插件apk通过动态下发到宿主apk,实现了动态加载插件并大大减少了包体积。
插件化优点
-
提高编译速度:开发过程中,每个模块都是独立开发的,编译的时候每次运行不需要都编译所有的业务逻辑代码,所以会适当的提高我们的开发速度;
-
业务模块完全解耦:每个业务都是完全独立的,这样开发过程中每个模块的功能改变和其他模块没有任何关系,甚至可以随意的去掉某一部分功能;
-
利于团队开发:插件开发是团队开发中用的最多的一种开发模式,可以更加的去分工,每个组只需要负责自己的功能,减少沟通成本提高开发效率;
-
动态更新插件,按需下载模块:对于一些不怎么常用的功能,可以让用户按需下载模块,从而减少工程的大小,让用户在下载的时候能够节省流量以及等待时间,而且功能升级的时候可以不更新主应用只更新插件;
-
解决android 655535问题。
插件化诞生
举个美团的例子,你就懂了


美食页面有那么多应用,如果单纯的用webview实现那里面的支付,地图和图片浏览等有点不切实际,或者全部写一个app里面,那包体积少说也有200M,可是你去应用市场看到,也才80M左右,这时插件化出场了

实现插件化的方式:
- 插桩式(本篇文章讲的就是这种方式)
- Hook方式,这个到时也会学习一个Hook的效果。
- 反射,但是在Android9.0中有很多反射是用不了了,所以这种基本上不会用了。
插桩式原理
一图胜千言,看图

上图右边美团外卖是以一个单独的apk(可以这样理解:一个apk就是一个插件)存在的,宿主App(美团)想要打开插件(美团外卖)中的某一个Activity,但是美团外卖这个插件很显然是没有上下文对象的【原因:因为此插件没有安装到手机上】,要想启动Activity必须要解决上下文这个东西,所以此时就需要在宿主APP中插一个桩,声明一个代理的Activity,如下:

此时ProxyActivity是一个空壳,可是没有显示插件的东西呀,怎么办?其实是这样的:

如何将一个未安装的插件apk的Activity能显示在这个代理ProxyActivity中呢?其实要想插件Activity显示出来肯定得要调用它里面的生命周期方法,而对于插件而言就是将自己Activity中的各种生命周期方法通过接口对外暴露给宿主的ProxyActivity,然后插件Activity中需要的Context则是借用ProxyActivity,这样最终就能达到我们调用的目的,目的达成最终插件化也就这实现了。
所以实现宿主Activity跳转插件化Activity,需要2样东西:
①暴露代理Activity的生命周期给插件化;
②提供上下文给插件化
开干
新建项目
宿主是app,插件是orderfood,这里注意orderfood也是application

宿主app和插件orderfood新建完成,此时需要一个接口来暴露插件orderfood Activity的生命周期,所以还需要定义一个宿主app和插件orderfood之间公共的library,里面会定义各种公共接口,这里起名library为:lib_plugin ,如下:

添加依赖
然后添加对它的依赖


①暴露生命周期
然后在library中定义Activity生命周期的公共接口,如下:

然后插件Activity得要将其生命周期方法对外暴露,所以需要实现这个接口:

但是如图并未对接口中的方法进行重写,因为这样写是不合适的,插件中肯定会有n个Activity的,所以需要抽取一个BaseActivity出来,然后再由它来实现抽象接口才靠谱,所以:



②提供上下文给插件化app
上面提到过,插件是不会装在手机上的apk,那么插件中的Activity是没有上下文的

所以需要在BaseActivity中来先重写一个这个方法

我们已经在插件化把相应的方法进行重写,此时需要把代理的上下文传给插件app,我们在宿主App新建一个代理Activity,并在清单文件注册:


接下来就是把代理Activity上下文传给插件化Activity中去,也就是如何调用BaseActivity中的attach()方法,这里就要用到反射了,这里需要知道要跳转插件Activity的全类名,所以这里通过Intent的参数传进来,如下:

然后通过反射来获取到要跳转插件Activity的对象,由于插件的所有Activity都继承了BaseActivity了,而BaseActivity又实现了公共模块的PluginInterface接口,所以最终就可以调用attach方法,如下:

所以代理Activity中改成如下:

我只重写了onStart() 和 onReusme(),剩余的方法是一样的。
在宿主中加载插件
对于加载插件一般有2种:内置和外置。
内置:就是插件的apk放在assert文件目录中
外置:从服务器进行下载到手机sd卡上
不管哪种方式,都需要将插件的类加载进来才行,所以对宿主app的进行修改:


接下来就是加载插件了,新建一个插件管理器
public class PluginManager {
private Context mContext;//插件的资源对象
private Resources pluginResource;
//插件的类加载器
private DexClassLoader dexClassLoader;
//插件的包信息类
private PackageInfo packageInfo;
private static PluginManager pluginManager = new PluginManager();
private PluginManager() {
}
public static PluginManager getInstance() {
return pluginManager;
}
public void setContext(Context context) {
this.mContext = context;
}
//加载插件apk
public void loadPlugin(String pluginPath) {
//获取包管理器
PackageManager packageManager = mContext.getPackageManager();
//获取插件的包信息类
packageInfo = packageManager.getPackageArchiveInfo(pluginPath, PackageManager.GET_ACTIVITIES);
//插件解压后的目录
File pluginFile = mContext.getDir("plugin", Context.MODE_PRIVATE);
//获取到类加载器
dexClassLoader = new DexClassLoader(pluginPath, pluginFile.getAbsolutePath(), null, mContext.getClassLoader());
//获取到插件的资源对象
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginPath);
pluginResource = new Resources(assetManager,mContext.getResources().getDisplayMetrics(),mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public Resources getPluginResource() {
return pluginResource;
}
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
}
public PackageInfo getPackageInfo() {
return packageInfo;
}
}
接下来打包插件,放到sd卡中:

打包成功后,如下:

然后改个名字为:orderfood.apk上传到sd卡根目录下:


接下来在宿主Activity中实现跳转插件代码
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//跳转插件
public void skipPlugin(View view) {
PluginManager.getInstance().setContext(this);
PluginManager.getInstance().loadPlugin(Environment.getExternalStorageDirectory() + "/orderfood.apk");
PackageInfo packageInfo = PluginManager.getInstance().getPackageInfo();
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
//由于插件只有一个activity,所以取数组第0个
intent.putExtra("className", packageInfo.activities[0].name);
startActivity(intent);
}
}
记得加权限

接下来运行app,由于我的手机是Android10.0,所以对于sdcard的权限得要主动申请一下,这里就不写申请的代码了,主动到权限管理中先将其打开,如下:

代码(已适配android10)已上传github
中,亲测可用