集成一个第三方相册功能,只需集成一个插件APK到项目中,无需集成额外代码,并且支持随时更新相册功能,无需发布版本更新,无需AndroidManifest中声明四大组件,这就是插件化。
插件化可利用性很广,但事实上大多数开发者,因为未知而放弃使用,所以本篇将深入浅出带你了解插件化原理,从基础到实现,插件化不再是你陌生的领域。
本篇主要涉及到:
- 一、Activity/Service的启动原理和流程。
- 二、插件化实现原理。
- 三、DiDi开源VirtualApk源码解析(Activity/Service)。
- VirtualApk优化反射带来损耗的小技巧。
ps:如果你对此(一、二)已经十分了解,请自行略过。
一、 Activity/Service启动流程
Activity和Service的启动流程十分复杂,一个startActivity
的背后是无数的逻辑实现,这里不深入讨论,但需要理解这个流程,因为插件化是在流程上动手脚,以达到绕过系统限制的目的。
下方图片是Activity启动的简化流程,可以看到,从Instrumentation
开始,到ActivityManagerService
和ActivityThread
结束,启动一个Activity,流程并不简单。
在Instrumentation
在execStartActivity
开始启动,到通过checkStartActivityResult
校验Activity是否在Manifest中声明,从图中可以看出,流程还是相当繁琐的。
(Activity启动流程详见图片)
下方图片是Service启动的简化流程,同样可以看到,ActivityManagerService
和ActivityThread
同样起到了关键性的作用。插件化的关键,就在于Instrumentation
、ActivityManagerService
和ActivityThread
。
(Service启动流程详见图片)
Service简化启动流程图为了更好理解插件化,如下图,是几个关键类的对应关系与实际作用,有点S/C的味道。它们的通信是通过IBinder
,实现进程通信的,可以看出,启动Activity和Service,ActivityThread
和ActivityManagerService
是关键,并且上面我们知道,Instrumentation
是Activity的启动入口,所以实现插件化的流程,便可以在这些关键类上开刀。
(下图在插件化实现中起到关键作用)
关键类关系图提前说明
好了,带了一波基础姿势的节奏,稍安勿躁,先这里在补充几个概念,如果你已经习得,可以跳过:
-
Hook:拦截某个内部流程,在其中做某些修改,以实现自己的逻辑。
-
Instrumentation:每个Activity都有一个
Instrumentation
对象,它是在Activity启动是被赋予的Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity();
这便是startActivityForResult的启动,同时返回启动结果。 -
占坑:声明一个不存在的Activity,如:
<activity android:name=".A$1" android:launchMode="standard"/>
,这样启动.A$1这个Activity可以欺骗系统检测,然后再将插件Activity注入到.A$1这个坑位中。
二、插件化实现原理。
插件化的实现就是在于加载、绕过系统限制、启动和管理插件等过程。按照VirtualApk的实现,大致流程为:
1、初始化Hook住Instrumentation
和ActivityThread
等。通过PackageParser(插件apk包信息)、AssetManager(资源文件Resources)、ClassLoader等加载一个Apk插件。
2、启动插件Activity:提前在主APP中占有坑位,通过替换Intent中的targetActivity,打开占坑声明的A$Activity,然后绕过AndroidManifest检测,再拦截newActivity方法中恢复targetActivity。
3、启动插件Service:通过启动一个代理Service统一管理,拦截所有Service方法,修改为startService到代理Service,在代理Service的onStartCommond
统一管理,创建/停止目标service 。
三、VirtualApk源码解析
1、初始化
初始化过程中,VirtualApk 创建了PluginManager ,并且hook住了Instrumentation
和SystemService,如下图所示。
如下图所示,VrutalApk通过Instrumentation
创建了一个VAInstrumentation
对象,VAInstrumentation
是一个继承Instrumentation
的类。
将VAInstrumentation
反射插入到ActivityThread
中,这样系统接下来关于Instrumentation
的操作,就会回到VAInstrumentation
中,被VrtualApk接管。
这里是如何拿到Instrumentation
的?
因为在ActivityThread
内部有一个sCurrentActivityThread
静态变量。如下图,通过反射sCurrentActivityThread
我们可以获取当前ActivityThread
,而ActivityThread
的公开方法getInstrumentation
即可拿到Instrumentation对象。
另外,上方图1还有设置HandlerCallback
的流程,其实就是拦截了ActivityThread
中的mH
这个Handler的Callback,从【 一、 Activity/Service启动流程】流程图可以看到,mH的handleMessage处理很多Activity的启动状态。
如下图, 是Hook Service的流程,如图中注释所示,通过ActivityManagerNative
的getDefault
,拿到AndroidManagerService
(详见启动流程图),而VirtualApk通过自定义ActivityManagerProxy
,重新生成了一个IActivityManager
,然后注入回AndroidManagerService
中,这样接管了系统启动、管理service等操作。
2、加载插件Apk
加载插件APK是通过PluginManager
的loadPlugin
方法,如下图所示,此处就是将apk拆开,解析,读取,加载,组装为LoadedPlugin并保存,以方便后面管理与使用。
此处对Apk进行了复杂的解析、加载、合并等操作,大致流程如下:
- 解析apk的相关包信息、判断是否加载过apk。
- 创建一些插件工具类。
- 通过
AssetManager
创建Resource
对象,平台用AssetManager创建出Resource,判断是否和宿主Apk合并资源。 - ClassLoader 根据插件APK路径创建loader,判断是否合并loader中的dex,合并nativeLIbraryDirectories。
- 将so复制到mNativeLibDir路径。
- 保存Instrumentation、Activities、Services、Providers , 注册Broadcast等。
- 创建出Apk的Application,并call Application onCreate。
3、启动插件Activity
那么是时候启动插件Activity了。通过startActivity便可以启动。从上面的流程我们知道启动是从Instrumentation.execStartActivity();
开始的,而系统的Instrumentation
已经被VAInstrumentation
替换,其中VAInstrumentation
重写了几个关键方法:
- execStartActivity:入口。
- newActivity:创建。
- callActivityOnCreate:通知。
- handleMessage:处理。
没错,如下图,在启动Activity的入口处,VirtualApk拦截了请求,然后根据Intent的参数,去匹配plugin中的Activity坑位,之后替换Intent中的Activity,以此来达到欺骗系统的效果。
但是,因为这个Activity的对象了实际上并不存在,最终我们需要启动的是实现了的targetActivity,所以需要拦截Instrumentation
的第二个方法newActivity
,因为在ActivityTread
的performLaunchActivity
中,会调用Instrumentation
的 newActivity
。
在newActivity
中,如下图,类没有找到时(坑位类肯定找不到啦),那么就去获取原本保存在Intent的目标Activity,然后调用创建VAInstrumentation
时保存的Instrumentation
(mBase)去创建Activity。
Activity虽然创建好了,但是它对应的资源和context都还不对,所以我们需要在Activity的OnCreate之前完成好Resource的注入。前面加载Apk时,这些资源都保存在Plugin
中。所以我们拦截callActivityOnCreate
方法,如下图,将Activity的Context、Application、Reource,都替换成Plugin中对应的对象。一个Activity就这样绕过AndroidManifest启动起来了。
4、启动插件Service
startService启动Service时,还记得上面我们通过ActivityManagerProxy
生成IActivityManager
吗?它主要拦截了Service相关启动和停止等方法,然后将其都转化为对应的startService方法,指向代理Service。因为startService方法的特性,他们最终都会在代理Service的onStartCommand
中被统一处理。
如下图,是ActivityManagerProxy
,其中invoke拦截了所有相关的服务请求,并做了转化处理,下面以startService为例。
startService这里,主要便是提取原本目标service信息,然后转化为代理Service,发送到代理Service,下方图片为启动流程和转化流程。
启动流程 转化流程如下图,在代理service中,根据请求类型,代理service会通过classLoader加载来创建service,并操作其attach、onCreate、onStartCommand等,让service工作起来。
启动真正的目标service自此Activity和Service都成功启动了,是不是对插件化有了不一样的了解?
5、AndroidStub
容许这里插入这一块,安利下Virtual中的AndroidStub模块,如下图
AndroidStub因为都用反射很浪费性能,所以有了AndroidStub
,它是用来欺骗编译器的。正常情况下你想操作ActivityThread
就会出现如下图情况,因为它是一个@hide
类,这时候除了反射得到ActivityThread
,你还需要再反射需要执着方法才能执行,这在一定程度会损耗一些性能。
但是如下图,VirtualApk通过AndroidStub
,模拟源码创建了如ActivityThread
类,这里你就可以如图正常使用ActivityThread
了,而AndroidStub
中的ActivityThread
,其实只是定义了和原码中一摸一样的方法,并没有其他实现。
因为CoreLibrary
依赖AndroidStub
使用的是provided
,因为provided
依赖是不打包依赖包,而是运行时提供,所以成功欺骗了编辑器,以此提高了性能。很神奇吧?
终于结束了,如果你看到了这里,相信你是一个很有耐心的同志!当然插件化还是其他实现方式,如Replugin,只Hook住了ClassLoader,流程更加复杂,如有什么建议和疑问,欢迎留言讨论。
VirtualApk:https://github.com/didi/VirtualAPK
个人github:https://github.com/CarGuo
终于结束了,点个赞不?
网友评论
assetManager = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
} else {
assetManager = hostResources.getAssets();
}
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);这两步分别加载宿主和插件的资源, 这里创建新资源 if (isMiUi(hostResources)) {
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);,似乎只用了宿主的资源,我知道他们肯定是合并了,就是没找到哪一步合并的,有空的话请帮忙指点下
Test.apk是打包到VirtualAPK中, 还是通过网络下载了.
滴滴使用场景是什么样子了.