随着应用开发的规模和复杂度越来越高,插件化技术被广泛的应用。插件化技术和热修复技术都是属于动态加载技术。
动态加载技术:
在写到插件化原理之前,要了解它的前身:动态加载技术。动态加载技术在很多领域都有应用,在Android开发领域也有。在Android传统开发中,一旦应用的代码被打包为APK并且被上传到各个市场,我们就不能修改应用的源码了,只能通过服务器来控制应用中预留的分支代码。但是很多时候我们不能提前预知情况和提前在应用中预留分支代码,这个时候就需要动态加载技术。在应用程序运行的时候,动态的加载一些程序中原本不存在的可执行文件并且运行这些文件的代码逻辑。可执行文件总的来说可以分为两种,一种是动态链接库so,另一种是dex相关的文件。动态加载技术中,热修复技术主要用来修复bug,插件化技术主要用于解决应用越来越大以及功能模块的解耦。
插件化的产生:
1.诞生背景:
在早期很少用到动态加载技术,但是随着互联网的发展,出现了以下几种情况:
- 业务复杂,模块耦合。
- 应用层的接入:一个应用不再是一个单独的应用,它可能要接入其他的应用。这样会导致可能维护的版本变多和体积增大。
- 65536限制,在代码量增加时,特别是应用接入其他应用时,那么方法数就很容易超过65536个。
2.插件化思想:
Android系统本身并没有提供太多的功能,内置的应用数量和整体功能也很有限,只能满足最基本的需求。我们可以安装更多的应用,使它提供更多的功能。插件化的客户端由宿主和插件两个部分组成,宿主就是指先被安装到手机中的APK,插件一般是指经过处理的APK,so和dex等等文件。插件可以被宿主加载,也可以作为APK独立的运行。
3.插件化框架的对比:
为了更加方便的应用插件化,出现了很多的插件化框架。例如:
插件化框架 | 作者 | 插件化框架 | 作者 |
---|---|---|---|
DynamicAPK | 携程 | dynamic-load-apk | 任玉刚 |
DroidPlugin | 360 | Small | Wequick |
RePlugin | 360 | VirtualAPK | 滴滴 |
特性 | VirtualAPK | DroidPlugin | Small | RePlugin |
---|---|---|---|---|
支持四大组件 | 支持 | 支持 | 只支持Activity | 支持 |
组件无需在宿主manifext中预注册 | Yes | Yes | Yes | Yes |
插件可以依赖宿主 | 是 | 否 | 是 | 是 |
支持PendingIntent | 是 | 是 | 否 | 是 |
Android特性支持 | 几乎全部 | 几乎全部 | 大部分 | 几乎全部 |
兼容性适配 | 高 | 高 | 中等 | 高 |
插件构建 | Gradle插件 | 无 | Gradle插件 | Gradle插件 |
如果加载的插件不需要与宿主有任何的耦合,也无须与宿主进行通信,比如:加载第三方APP,那么推荐使用RePlugin,其他的情况推荐使用VirtualApk,它是加载耦合插件方面的首选框架,具有普遍的适用性。
插件的加载机制:
普通的类插件的加载:
- 自定义ClassLoader替换掉宿主的ClassLoader,采用我们自己的ClassLoader,进而实现加载我们插件apk的目的。。这里想要替换一般会hook掉系统中的缓存的ClassLoader,进而截断这个过程。这样我们自己的classloader就可以实现针对插件apk进行加载。
- 委托加载:将我们插件的dex插入到dexElements这个数组中就可以实现对我们插件apk的加载(通过反射去构建dexElements,将宿主和插件apk的dex一起copy进去即可)。
Activity插件化:
四大组件的插件化是插件化技术的核心知识点,而Activity插件化更是重中之重。Activity插件化要解决的点在于:因为android拒绝加载没有在manifest文件注册的activity,而插件中apk的组件显然不会再宿主manifest文件中注册,因此这是首要面临的问题。Activity的插件化主要有3种实现方式,分别是反射实现,接口实现和Hook技术实现。反射实现会对性能有影响,关于接口实现可以阅读dynamic-load-mic。我们来看看Hook技术,Hook技术的实现主要有两种解决方案,分别是:通过Hook IAtivityManager来实现和通过Hook Instrumentation来实现。
1.Activity启动:
查看四大组件的启动过程(上)。
2.Hook IActivityManager方案实现:
因为AMS存在于SystemServer进程中,我们无法修改,只能在应用程序进程中做文章。可以采用预先占坑的方式来解决没有在AndroidManifest.xml中显式声明的问题。也就是在请求创建普通Activity之前使用一个在AndroidManifest.xml中注册的Activity来进行占坑,通过AMS的校验,然后用插件的Activity替换占坑的Activity。
Service插件化:
1.与Activity插件化的不同:
在Service启动过程中从ContextImpl到ActivityManagerService的调用过程是没有交给Instrumentation来处理的,也就是说Service的启动和Instrumentation是没有关联的。所以Service插件化是不能通过Hook Instrumetation来实现的。而因为:
- Activity是基于栈管理的,一个栈中的Activity的数量不会太多,所以插件化框架处理的插件Activity数量是有限的,可以声明有限的占坑Activity来实现。除去硬件和系统的限制,插件化框架处理的Service插件的数量是可以近乎无限的,所以无法用有限的占坑Service来实现。
- 在Standard模式下多次启动同一个占坑Activity可以创建多个Activity实例,但是多次启动Service并不会创建多个Service实例。
- 用户和界面的交互会影响到Activity的生命周期,因此插件Activity的生命周期需要交给系统来管理。Hook IActivityManager方案中还原Activity就是为了这一点,Service的生命周期不受用户影响,可以由开发者管理生命周期,没有必要还原插件。
所以Service插件化也不能使用Hook IActivityManager方案来实现。
2.代理分发实现:
Activity的插件化的重点在于要保证它的生命周期,而Service插件化的重点是保证它的优先级,这就需要一个真正的Service来实现,而不是像占坑Activity那样起到一个占坑的作用。当启动插件Service时,就会先启动代理的Service,当这个代理Service运行起来之后,在它的onStartCommand等方法里面进行分发,执行插件Service的onCreate等等方法。这个方案就叫做代理分发。
ContentProvider插件化:
ContentProvider的启动过程参考:四大组件启动过程(下)。
1.VirtualApk的实现:
ContentProvider插件化的关键在于将ContentProvider插件共享给整个系统。和Service插件化类似,需要注册一个真正的ContentProvider作为代理ContentProvider,并且把这个代理ContentProvider共享给整个系统,对于ContentProvider的请求全部会交给代理的ContentProvider处理并且分发给对应的ContentProvider插件。
BroadcastReceiver插件化:
1.广播插件化的思路:
静态注册的BroadcastReceiver会在AndroidManifest.xml中注册,采用类似Activity的占坑是不可行的。但是由于动态注册的BroadcastReceiver是可以动态设置感兴趣的广播的,所以可以把静态注册的BroadcastReceiver全部转换为动态注册的BroadcastReceiver来处理。
资源的插件化:
1.系统的资源加载:
我们回到Activity启动过程的3.ActivityThread启动Activity的过程这一节。其中performLaunchActivity方法与系统资源加载相关的有:通过makeApplication方法,创建了Application和ContextImpl的createAppContext方法用于创建应用的context。createAppContext方法内部还调用了LoadedApk的getResources方法得到Resources,并且把它赋值给ContextImpl的setResources方法。
LoadedApk的getResources方法会调用ResourcesManager的getResources方法,该方法内部会返回getOrCreateResources方法。getOrCreateResources方法会调用createResourcesImpl方法创建ResourcesImpl,它用于具体实现Resources,其中内部还创建了AssetManager,这是因为Resources会依赖AssetManager来加载资源。然后调用getOrCreateResourcesForActivityLocked方法创建Resources。
2.VirtualApk实现:
资源的插件化方案主要有两种:一种是合并资源方案,将插件的资源全部添加到宿主的Resources中,这种方案插件可以访问宿主的资源。另一种是构建插件资源方案,每个插件都可以构造独立的Resources,这种方案插件不可以访问宿主的资源。具体的代码逻辑在:
CoreLibrary/src/main/java/com/didi/virtualapk/internal/LoadedPlugin.java
首先调用createResources方法用于创建Resources,如果是合并资源方案,会调用ResourcesManager的createResources方法,其内部会先得到包含宿主资源的AssetManager,在通过反射调用AssetManager的addAssetPath方法来加载插件的资源,并且返回新的Resources。返回之后通过Hook的方式将新的Resources替换旧的Resources。如果是构建资源方案,会调用createAssetManager方法创建AssetManager,再创建Resources并且传入AssetManager。
so的插件化:
so的热修复方案有两种,分别是:
- 将so补丁插入到NativeLibraryElement数组的前部,使so补丁的理解先被返回和加载。
- 调用System的load方法来接管so的加载入口。
so的插件化方案和so热修复第一种方案类似,简单来说就是将so插件插入到NativeLibraryElement数组中,并且将存储so插件的文件添加到nativeLibraryDirectories集合中就可以了。来看VirtualApk实现,在LoadedPlugin的构造方法中会调用createClassLoader方法,该方法会创建用于加载插件的DexClassLoader,并且把它作为参数传入DexUtil的insertDex方法中。
DexUtil的insertDex方法中首先调用combineArray方法将宿主和插件的DexElement合并并且得到allDexElements,并且通过ReflectUtil.setField方法(反射)将dexElements替换为allDexElements。