相关概念
插件化是怎么出现的
- app体积越来越大,功能模块越来越多
- 模块耦合度高,协同开发沟通成本极大
- 方数可能超过65535,占用内存过大(没采用分包的情况下)
- 应用之间需要相互调用
包括但不限于以上问题的出现,就有人提出了解决方案:将一个大的apk按照业务分隔成多个小的apk,每个小的apk既可以运行又可以作为插件运行。这个方案听起来是不错的,我们安装的时候,只需要安装最外面的apk,当我们需要里面具体的业务功能的时候,再动态加载具体的apk。这样也解决了加载其它平台功能的问题,比如淘宝要集成聚划算,只需要动态加载聚划算相关业务的apk即可,而聚划算也不用为多个不同的平台维护多套代码。
这种方案就是插件化。听起来也比较类似大数据的分布式集群处理。
插件化优势
- 业务模块基本完全解耦
- 高效并行开发(编译速度更快)
- 按需加载,内存占用更低等等。
插件化相关概念
- 宿主
主App,可以加载插件,也称为Host。只有一些基本的类库和功能,需要用户下载。相当于一个容器,会动态加载插件。
- 插件
插件App,被宿主加载的App,可以跟普通App一样的Apk文件。
- 插件化
将一个应用按照宿主插件化的方案改造。
- 插件化后的apk结构
不管是否插件化的apk,都包括AndroidManifest.xml、classes.dex、lib、res、assets、resources.arcs。 不同在于lib文件夹,普通的apk里lib文件夹是分体系结构的,什么armeabi、X86等等。里面放的是由 C/C++ 编译的so文件,而插件化的宿主apk与之不同,不光是有由C/C++编译的so文件,还有由业务插件改造后的so文件。
插件化与动态更新
- 都是动态加载技术的应用
- 动态更新是为了解决线上bug或小功能的更新而出现的
- 插件化是为了解决应用越来庞大而出现的
插件化框架介绍
框架 | dynamic-load-apk | DynamicAPK | Small | DroidPlugin | VirtualAPK |
---|---|---|---|---|---|
作者 | 任玉刚 | 携程 | 林光亮 | 360手机助手 | 滴滴 |
是否支持四大组件 | 只支持Activity | 只支持Activity | 只支持Activity | 全支持 | 全支持 |
组件是否要在宿主中注册 | √ | × | √ | √ | √ |
插件是否可以依赖宿主 | √ | √ | √ | × | √ |
支持PendingIntent | × | × | × | √ | √ |
Android特性支持 | 大部分 | 大部分 | 大部分 | 几乎全部 | 几乎全部 |
兼容性适配 | 一般 | 一般 | 中等 | 高 | 高 |
插件构建 | 无 | 部署aapt | Gradle插件 | 无 | Gradle插件 |
如何加载插件的类
关于反射
反射会对性能有一定的影响:
- 因为在使用过程中会产生大量的临时对象,会造成GC
- 会检查可见性,private还是public
- 会生成没有优化的字节码
- 类型转换,封箱拆箱
关于安卓的类加载
之前写过一篇 Android虚拟机和类加载机制 ,这里需要补充点东西。
- PathClassLoader和DexClassLoader
我们以前认为PathClassLoader只能加载已经安装到系统中的apk的一些class文件,DexClassLoader加载未安装过的jar/apk/dex。其实这种说法是不可靠的。
在8.0之前,它们唯一的区别是DexClassLoader多了一个必传参数optimizedDirectory,这个参数的意思是生成的odex(优化的dex)存放的路径。而8.0及以后,这个参数置null了,所以二者没有什么区别。
- BootClassLoader和PathClassLoader
PathClassLoader的parent是BootClassLoader,而BootClassLoader没有parent。**这里parent指的是ClassLoader类型的对象,不是指的父类**!
PathClassLoader加载应用的类,像我们自己写的比如MainActivity,还有依赖的第三方库,比如Glide、AppCompatActivity ;BootClassLoader加载SD K的类,并不是说加载FrameWork的类,比如Activity属于SDK的,就是BootClassLoader加载的。
如何加载一个类
- dex文件生成命令
1 | //利用Sdk/build-tools/xxx/dx.bat工具 |
- 加载上面生成的dex中的Test类
1 | //用PathClassLoader其实也可以 |
- 加载插件apk中的普通类
1 | public class LoadUtil { |
1 | //使用 |
如何启动插件的四大组件
主要实现如何利用动态代理技术Hook掉系统的AMS服务,来实现拦截Activity的启动流程,在宿主中启动插件的MainActivity。
因为MainActivity是没有在宿主清单文件注册的,AMS会检测清单文件,这个时候就会异常的,我们希望有个已经注册过的ProxyActivity替代MainActivity来进行被检测,等检测完了再用MainActivity替换掉ProxyActivity。
如何用ProxyActivity替代MainActivity
我们跟着startActivity(intent)看一下源码,发现在Instrumentation类的下面方法execStartActivity启动Activity的。
1 | public ActivityResult execStartActivity( |
ActivityManager.getService()就是IActivityManager,IActivityManager的创建是在Singleton类里。而AMS实现了IActivityManager这个接口,这里是AIDL技术,所以最终ActivityManager.getService().startActivity()就是相当于调用的AMS的startActivity()
1 | public static IActivityManager getService() { |
综上,我们想通过动态代理去代理iActivityManager类,来拦截它的startActivity方法,修改intent,替换成宿主中已经注册的ProxyActivity。实现的代码:
1 | public static void hookAMS() { |
如何用MainActivity替换掉ProxyActivity
我们需要在AMS检测清单文件之后,在真的启动Activity之前把我们想启动的MainActivity替换回来,不然就真的启动ProxyActivity了。而启动Activity是由Handler消息机制参与的,我们看一下源码:
1 | public void handleMessage(Message msg) { |
我们根据Handler的源码可以知道,当mCallback != null的时候我们可以让它进入Callback的handleMessage(msg)方法,这个方法返回false的话,还是会接着执行Handler的handleMessage(msg)方法的,这样既可以拦截到msg,也不会影响原有的执行流程。
1 | public void dispatchMessage(Message msg) { |
而系统的Hander创建的时候是没有传入Callback的,所以我们想的办法就是创建一个Callback,来拦截系统的Handler消息流程。
1 | //H是Handler子类 |
实现的代码:
1 | public static void hookHandler() { |
如何加载插件的资源
资源加载主要包括assets、res两个目录下的。Resources 类也是通过 AssetManager 类来访问那些被编译过的应用程序资源文件的,不过在访问之前,它会先根据资源 ID 查找得到对应的资源文件名。而 AssetManager 对象既可以通过文件名访问那些被编译过的,也可以访问没有被编译过的应用程序资源文件。
raw文件夹和assets文件夹有什么区别
raw:Android会自动为这目录中的所有资源文件生成一个ID,这意味着很容易就可以访问到这个资源,甚至在xml中也可以访问,使用ID访问速度是最快的。
assets:不会生成ID,只能通过AssetManager访问,xml中不能访问,访问速度会慢些,不过操作更加方便。
加载插件的资源实现方式有两种:
- 插件的资源和宿主的资源直接合并,但是会造成资源id冲突,不过可以用aapt解决。
- 专门创建一个Resource/AssetManager加载插件的资源,这里主要介绍这种方法。
加载插件资源之错误实践
- 创建插件Resources
1 | public class LoadUtil { |
- 将插件Reources暴露给插件
1 | public class MyApplication extends Application { |
3.插件中使用
1 | //插件BaseActivity中 |
至此,插件资源正常加载,但是存在以下问题:
- 现在宿主和插件中的Resources都是插件Resources了
- 现在宿主的获取到的Application是宿主的,插件自定义Application也不会执行
所以这种写法是不行滴。
接下来继续,既然Resources是给插件用,那么我们就在插件中创建行不行呢?
1 | //放在插件中 |
1 | //插件中BaseActivity中 |
这种方式可以解决上面存在的两种问题,但是会出现新的问题,就是资源冲突,所以不行。
加载插件资源之正确实践
- 创建插件Resources
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class LoadUtil {
private final static String apkPath = "/sdcard/plugin-debug.apk";
private static Resources mResources;
public static Resources getResources(Context context) {
if (mResources == null) {
mResources = loadResource(context);
}
return mResources;
}
private static Resources loadResource(Context context) {
// assets.addAssetPath(key.mResDir)
try {
AssetManager assetManager = AssetManager.class.newInstance();
// 让 这个 AssetManager对象 加载的 资源为插件的
Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, apkPath);
Resources resources = context.getResources();
// 加载插件的资源的 resources
return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
} - 创建插件Context
1 | Resources resources = LoadUtil.getResources(getApplication()); |
- 使用
1 | public class MainActivity extends BaseActivity { |
结束语
圣诞节快乐!