1.编译时代码替换
所有plugin中的Activity子类都会在编译时由gradle插件进行替换,即将继承自Activity或Activity子类(如AppCompatActivity)的类改为继承PluginActivity和PluginAppCompatActivity等,这样做的原因在于:RePlugin既不想Hook AMP,Instrumentation等,又想达到动态加载未在Manifest中声明的组件的目的,那么就只能在编译期做更多的事情,即通过gradle插件来修改plugin中的代码(特别是组件的代码),比如这里所有本来继承自Activity或AppCompatActivity改为继承PluginActivity和PluginAppCompatActivity,而PluginActivity虽然也是继承自Activity,但是其内部的方法已经重写了,代码如下(在replugin-plugin-library中):
|
|
显然,这样做的结果是之前插件中Activity代码中的startActivity和startActivityForResult()方法都修改为RePluginInternal.startActivity()和RePluginInternal.startActivityForResult()方法,而RePluginInternal其实只是做了封装,最终通过反射调用到replugin-host-library的startActivity()中:
|
|
而Factory2其实仍然还是起一个封装的作用,实际调用的是PluginLibraryInternalProxy中的startActivity()方法:
|
|
在分析这个方法之前,我们先分析一下除了Activity.startActivity()之外的另外一种startActivity()的调用,即Context.startActivity()这个调用,那我们将这个调用也替换掉呢?
其实很简单,不需要改动这部分代码,只需要在插件初始化时将其Context替换为特制的Context,即PluginContext:
|
|
可以看到,PluginContext中的startActivity()方法其实也是调用 Factory2.startActivity(),到这里我们就把插件中所有startActivity()的调用都统一到Factory2.startActivity()的调用了,而至于插件中上下文的替换,是另外一个很大的话题,后面会专门写一篇文章来阐述。
2.PluginLibraryInternalProxy
2.1 首次进入插件时的startActivity()调用
此时涉及到坑位的分配,情况比较复杂。
从外部进入一个插件,通过调用RePlugin.startActivity(Context,Intent)实现,这个调用流程如下:
下面就逐步来分析。
2.1.1 RePlugin.startActivity(Context,Intent)
在看这个方法之前,先看一下这个Intent是如何来的,它跟一般的startActivity中的Intent不一样,它是特殊构造的,如下是一个调用示例:
|
|
而RePlugin.createIntent()方法如下:
|
|
显然,这个方法的作用是创建一个定向到插件中组件的Intent,所以”demo1”是插件名称,而”com.qihoo360.replugin.sample.demo1.MainActivity”是组件名。
下面看RePlugin.startActivity(Context,Intent)方法:
|
|
其实注释已经写得很完整了,就是开启一个插件的Activity,而plugin和cls刚刚在前面已经赋值,分别为”demo1”和”com.qihoo360.replugin.sample.demo1.MainActivity”. 其中IPluginManager.PROCESS_AUTO表示自动分配插件进程。
2.1.2 Factory.startActivityWithNoInjectCN(Context, Intent, String, String, int)
该方法代码如下:
|
|
显然,该方法仅仅是起一个转发以及回调的作用。没什么好分析的,PluginCommImpl.startActivity(Context, Intent, String, String, int)类似,下面就直接进入PluginLibraryInternalProxy.startActivity()的分析。
####2.1.3 PluginLibraryInternalProxy.startActivity(Context, Intent, String, String, int, boolean)
该方法如下:
|
|
这个方法是最长的一个,主要分如下几个部分来分析:
1)如果插件还未下载,则触发下载回调,让用户决定是否下载;
2)检查这个activity类是否为动态类,这里这个类并不是,所以进入下一个判断;
3)如果插件状态出现问题,则也弹出插件不存在的回调,让用户决定下一步如何做;
4)调用RePlugin.isPluginDexExtracted()来判断当前插件是否已经释放了Dex,Native库等,如果没有,则要提示用户正在加载中;
5)下面调用PluginCommImpl.loadPluginActivity(Intent, String, String, int)的方法,这个方法里面涉及到坑位的分配,这是整个RePlugin中非常核心的一部分,内容也非常多,所以单独在下个小节进行分析。
3.Activity坑位的分配–PluginCommImpl分析
PluginCommImpl.loadPluginActivity(Intent, String, String, int)方法如下:
|
|
这个方法主要分为如下几个部分。
3.1 Activity信息的获取
PluginCommImpl.getActivityInfo(String,String,Intent)
|
|
这个方法的逻辑很简单,就是先加载插件,然后从插件的组件信息中获取ActivityInfo对象。可见,重点在于PmBase.loadAppPlugin(String)的调用, 从这个方法开始,涉及到插件的加载和安装的问题,请看RePlugin解析之插件的安装与加载。
在插件安装完成并加载到内存之后,这里分两种情况:
1)activity信息不为空,调用ComponentList.getActivity()方法来获取ActivityInfo,如果看了请看RePlugin解析之插件的安装与加载这篇文章的话,就会知道ComponentList中含有插件中所有四大组件的信息,所以要从ComponentList中获取;
2)activity信息为空,那自然就要根据intent-filter信息来进行匹配了。其中IntentMatchHelper.getActivityInfo()方法如下:
|
|
其中doMatchIntent()方法如下:
|
|
注意传递到doMatchIntent中的第有一个参数为ManifestParser.INS.getActivityFilterMap(),而在RePlugin解析之插件的安装与加载一文中已经分析过,ManifestParser这个类就是解析插件的Manifest文件,并且会收集所有组件的intent-filter信息,其中getActivityFilterMap()返回的就是对应插件中所有Activity的intent-filter信息。
之后首先解析出Intent的action, type, data, scheme, categories, 然后调用IntentFilter.match()方法进行匹配,如果匹配成功则返回组件名称。
这里值得吐槽的有两点:
- 命名极不规范,pluginName其实应该命名为pluginComponentName,即插件中组件的名称
- 匹配效率太低,目前这样是要逐一遍历插件中所有对应组件(比如如果是Activity的话就要遍历所有Activity的intent-filter)的intent-filter信息,如果组件多的话效率会有点低
3.2 分配pid
再回到PluginCommImpl.loadPluginActivity()方法中,在获取到ActivityInfo之后,主要有如下语句:
|
|
第一句是将ActivityInfo中的theme放入到Intent中,第二句是调用PluginClientHelper.getProcessInt()获取进程id。这里有两个地方需要解决,首先是ActivityInfo的processName是什么时候赋值的呢?
其实就是在ComponentList的构造方法中:
|
|
显然,对于在Manifest中没有process属性的组件,将其processName全部设置为插件的包名,比如“com.qihoo360.replugin.sample.demo1”这样的,然后看一下PluginClientHelper.getProcessInt()方法:
|
|
代码中的注释都写得很清楚了,对于进程名含有”:guardservice”或者和宿主常驻进程名(如”me.ele.repluginhostsample:guardservice”)相同的,分配的pid就是IPluginManager.PROCESS_PERSIST(值为-2),对于用户自定义的进程,则从PROCESS_INT_MAP中分配,比如对于demo1这个插件,有如下映射关系:
|
|
那么由于PROCESS_INT_MAP中含有”:p0”这样的key,那么给它分配的pid就是PROCESS_INIT+0,即-100+0.
需要注意的是,这里的pid并不是真正组件进程的pid !
至于这个分配的pid有什么用,在下一小节会分析。
3.3 插件中组件进程启动
在获取到pid之后,调用MP.startPluginProcess()来启动插件进程,其实这里确切地说是启动插件中组件的进程,因为插件在之前启动时运行在宿主的UI进程中,这里其实是对于那些有自定义进程需求的组件服务的。MP.startPluginProcess()方法如下:
|
|
这里startPluginProcess()是一个IPC调用,那么重点就在于PluginProcessMain.getPluginHost()这个方法:
|
|
为了理解这个方法,我们需要先看一下sPluginHostLocal和sPluginHostRemote是怎么来的。
3.3.1 sPluginHostLocal是什么
sPluginHostLocal的赋值就只有一个地方,那就是PluginProcessMain.installHost()方法:
|
|
而这个方法的调用是在PmBase.initForServer()中,而initForServer()是在插件的常驻进程(对于HostConfigHelper.PERSISTENT_ENABLE为true,即允许常驻进程作为插件管理时),或者UI进程(此时插件的管理就在UI进程中)。所以总结起来就是initForServer()是在插件的管理进程中被调用。
|
|
可以看到其实就是创建了一个PmHostSvc对象然后传递给PluginProcessMain.installHost()方法,那么PmHostSvc到底是什么呢?
看看它继承自什么就知道了。
|
|
显然,PmHostSvc其实是实现了IPluginHost接口IPC调用的Server端的功能。
IPluginHost是一个非常重要的IPC接口,下面列一下它的方法:
|
|
可见,这些方法都是与插件管理有关的。
需要注意的是:只有在插件管理进程中的PluginProcessMain对象(PluginProcessMain只是封装了一些跨进程操作,它会在常驻进程和其他进程中被用到)中sPluginHostLocal才不会为null, 对于在其他进程中的PluginProcessMain对象,其installHost()方法并不会被调用,从而sPluginHostLocal为null. 此时,就需要sPluginHostRemote出场了!
3.3.2 sPluginHostRemote的作用,以及ContentProvider的巧妙运用
其实sPluginHostRemote是IPluginHost接口的代理,它是在connectToHostSvc()中被赋值的:
|
|
这个方法做了很多事情,主要分为如下几点:
- 首先是PluginProviderStub.proxyFetchHostBinder()的作用。从后面IPluginHost.Stub.asInterface(binder)这个语句可知这里获取到的是与插件管理进程中IPluginHost的server通信的IBinder对象, 那么为什么它能同步获取到IBinder呢?一般不是都要通过bindService()这样的异步操作来获取吗?
这里其实RePlugin采用了ContentProvider的一个特性,那就是ContentResolver().query()时返回Cursor,将IBinder包裹在Cursor中即可。见PluginProviderStub.proxyFetchHostBinder()方法:
|
|
由于uri为ProcessPitProviderPersist.URI, 所以这个query()方法最终调用到ProcessPitProviderPersist中的query(),该ContentProvider子类如下:
|
|
可以看到,对于query()回调,会调用PluginProviderStub.stubMain():
|
|
此处我们的selection是SELECTION_MAIN_BINDER,所以返回的是BinderCursor.queryBinder(PMF.sPluginMgr.getHostBinder()), 该方法如下:
|
|
可见就是将IBinder传递进去,然后使用BinderCursor包裹,那这里的关键是PMF.sPluginMrg.getHostBinder()怎么就拿到了IPluginHost的server端的IBinder?
其实答案很简单! 因为ProcessPitProviderPersist这个ContentProvider跟插件管理进程在同一个进程中,它是被gradle插件打入到Manifest文件中的,最终效果如下:
|
|
而PMF.sPluginMrg.getHostBinder()会调用到PmBase.getHostBinder(), 而其中的mHostSvc的创建在前面已经提到过了,这里不再赘述了。
拿到IBinder之后,就可以调用IPluginHost.Stub.asInterface()拿到IPluginHost的代理了,之后调用PluginManagerProxy.connectToServer()其实就是通过这个代理对象拿到IPluginManagerServer的代理。我们看一下PmHostSvc中fetchManagerServer()的实现:
|
|
只是调用PluginManagerServer.getService()方法:
|
|
而Stub对象mStub其实是实现了继承自IPluginManagerServer.Stub,代码如下:
|
|
需要注意的是IPluginManagerServer与IPluginHost的区别:IPluginHost其实更多的是为了起一个通信支撑的作用,有了它之后才能使插件,宿主进行通信,但它本身几乎没有涉及到插件业务,而IPluginManagerServer则是真正地实现插件管理,利用它来控制插件的安装、卸载、获取等,它们的相同之处则在于server端都运行在常驻进程中。
其实从IPluginManagerServer的aidl接口定义也可见一斑:
|
|
可以看到IPluginManagerServer的功能非常强大,包括安装、卸载插件,加载插件列表,更新插件列表,获取正在运行的插件列表,判断插件是否在运行,同步插件状态,获取正在运行此插件的进程名列表等。
3.3.3 进程分配
再回到MP.startPluginProcess()方法,可知会通过IPluginHost的代理经过IPC最终调用到PmHostSvc中startPluginProcess()方法:
|
|
其中mPluginMgr是PmBase对象,PmBase.startPluginProcessLocked()方法如下:
|
|
这个方法比较长,主要分为以下几个部分:
- 首先是PluginBinderInfo对象是在PluginCommImpl中的loadPluginActivity()中创建的,并且只赋予了request的值为ACTIVITY_REQUEST, pid和index为默认值-1
- 由于Constant.ENABLE_PLUGIN_ACTIVITY_AND_BINDER_RUN_IN_MAIN_UI_PROCESS为true,而且我们这里info.request==PluginBinderInfo.ACTIVITY_REQUEST,不过我们这里process==IPluginManager.PROCESS_UI;如果是process==IPluginManager.PROCESS_AUTO,即自动分配进程的话,那么process也会被赋值IPluginManager.PROCESS_UI;
之后调用PluginProcessMain.schedulePluginProcessLoop()是为了循环检查各插件进程,如果发现插件中没有组件在运行了,就收回这个进程。负责检查的task如下:
12345678910111213141516171819202122232425262728private static final void doPluginProcessLoop() {if (Constant.SIMPLE_QUIT_CONTROLLER) {...synchronized (PROCESSES) {for (ProcessRecord r : PROCESSES) {if (r.state != STATE_RUNNING) {continue;}if (r.activities > 0) {continue;}if (r.services > 0) {continue;}if (r.binders > 0) {continue;}...android.os.Process.killProcess(r.pid);waitKilled(r.pid);r.setStoped();//schedulePluginProcessLoop(CHECK_STAGE3_DELAY);return;}}}}
显然,这里就是遍历PROCESS,如果发现它不再运行,而且运行的组件和binder都为0了,就回收掉这个进程。需要注意的是这里的ProcessRecord是自定义的,不是android系统的那个ProcessRecord.
之后调用PluginProcessMain.probePluginClient()方法来获取IPluginClient对象,该方法如下:
12345678910111213141516171819202122232425262728293031static final IPluginClient probePluginClient(String plugin, int process, PluginBinderInfo info) {synchronized (PROCESSES) {for (ProcessClientRecord r : ALL.values()) {if (process == IPluginManager.PROCESS_UI) {if (!TextUtils.equals(r.plugin, Constant.PLUGIN_NAME_UI)) {continue;}/* 是否是用户自定义进程 */} else if (PluginProcessHost.isCustomPluginProcess(process)) {if (!TextUtils.equals(r.plugin, getProcessStringByIndex(process))) {continue;}} else {if (!TextUtils.equals(r.plugin, plugin)) {continue;}}if (!isBinderAlive(r)) {return null;}if (!r.binder.pingBinder()) {return null;}info.pid = r.pid;info.index = r.index;return r.client;}}return null;}
这里就是分几种情况:
- 如果process==IPluginManager.PROCESS_UI,那么遇到ProcessClientRecord.plugin与Constants.PLUGIN_NAME_UI相同的情况,就继续查找;
- 如果是用户自定义进程,并且ProcessClient.plugin和getProcessStringByIndex()不相同时,就继续查找
- 如果ProcessClientRecord.plugin与plugin相等,那么继续查找
- 如果发现相应该ProcessRecord的binder都没有活着了,就返回null
- 如果发现binder都ping不通,也返回null
- 走到最后的,说明这个插件进程还活着,所以给PluginBinderInfo对象赋值,并且返回ProcessRecord中的IPluginClient对象.
而IPluginClient的作用是什么呢?
看下它的aidl定义:
|
|
可以看到也是用于插件进程与插件管理进程通信的,包括分配容器,接收广播等。这个接口到后面再详细分析。
再回到PmBase.startPluginProcessLocked()方法,如果返回的client不为空就说明所要求的进程已经启动(比如要发现要启动的进程是UI进程,而宿主UI进程已经启动,所以可直接返回),此时当然就没必要再启动进程了。如果返回的client为null, 说明插件还没启动,此时需要先分配进程pid,会调用到PluginProcessMain.allocProcess()方法:
|
|
在这里,如果要启动的是UI进程,那么返回IPluginManager.PROCESS_UI;如果是自定义进程(process值在[-100,-100+3)之间),那么就还是返回process.
下面就到了真正启动插件中组件自定义进程的时刻。
3.3.4 组件自定义进程启动
回到PmBase.startPluginProcessLocked(), 在调用allocProcess()方法之后,获得了分配的pid,然后调用PluginProviderStub.proxyStartPluginProcess()启动插件中组件的自定义进程(目前写死了自定义进程最多只能有2个),该方法如下:
|
|
可见,在这里就是通过调用ContentResolver的insert()方法,最终调用到相应的ContentProvider,然后启动对应的进程。
那么这种映射关系是如何建立的呢?ContentProvider又是何时新建并插入到Manifest文件中的呢?
首先说映射关系,在RePlugin解析之插件的安装与加载一文中已经提到了这种映射关系是可以由插件自行指定的,比如demo1中有如下映射关系:
|
|
这就意味着com.qihoo360.replugin.sample.demo:bg这个进程要映射到hostpkg:p0这个进程,在编译过程中通过gradle插件会往Manifest中打入进程名为hostpkg:p0的ContentProvider,如下所示:
|
|
从PluginProviderStub.proxyStartPluginProcess()方法中调用的ProcessPitProviderBase.buildUri()方法也可以看出:
|
|
比如在这里index==-100,所以最终构造出来的uri就是”content://hostpkg.loader.p.mainN100/main”,当hostpkg为”me.ele.repluginhostsample”时就是”content://me.ele.repluginhostsample.loader.p.mainN100/main”,而这恰好就对应刚刚ProcessPitProviderP0的authorities.
到这里,就真正完成了插件中组件自定义进程的启动。
3.3.5 attachProcess()
前面提到,启动插件中组件的自定义进程时会调用到ProcessPitProviderBase中的insert()方法,然后调用到PluginProviderStub.stubPlugin()方法:
|
|
注释里面已经写得很清楚了,最终会调用到PluginProcessMain.connectToHostSvc()方法,
而PluginProcessMain.connectToHostSvc()方法如下:
|
|
这个方法在RePlugin解析之插件的安装与加载一文中已经分析过了,这里不再赘述。注意最后一个语句,它的作用就是注册该进程到插件管理进程中。
而PmBase.attach()方法如下:
|
|
这里是通过IPluginHost接口的代理进行IPC,最终调用到插件管理进程的PmHostSvc.attachPluginProcess()方法:
|
|
这个方法很简单,就是先获取IPluginClient的代理,然后调用PluginProcessMain.attachProcess()以记录插件进程(确切地说是插件中组件的进程)的信息:
|
|
可以看到,这个方法很简单,就是先记录插件进程名称,pid, IPluginClient的代理client等,最后将ProcessClientRecord对象放入ALL这个缓存中。
3.4 分配坑位
到3.3为止,终于完成插件中组件进程的启动了。下面要进行坑位的选择, 会调用IPluginClient.allocActivityContainer()方法进行坑位的选择,注意这里通过IPluginClient的IPC最终会调用到刚刚启动的那个进程(而不是插件管理进程), allocActivityContainer()的真正实现在PluginProcessPer中,方法如下:
|
|
这个方法很简单,先是对于 process和loadPlugin参数进行一些处理,然后调用bindActivity()获取到container:
|
|
前面获取Plugin对象和ActivityInfo对象就不说了,就说最关键的部分,如果ActivityInfo中的进程名称包含”:p”,那么就意味着是自定义进程,就调用PluginContainers.alloc2()方法(吐槽一下这个方法命名真垃圾),否则调用PluginContainers.alloc()方法。
先来看PluginContainers.alloc2()方法:
|
|
可以看到会分三种情况,一种是launchMode是LAUNCH_SINGLE_INSTANCE, 还有一种是带有taskAffinity, 最后一种是其他。
这三种情况都是调用allocLocked()方法进行坑位的分配,唯一的不同就是第2个参数。
- 第一种情况是states.mLaunchModeStates.getStates(ai.launchMode, ai.theme)
- 第二种情况是states.mTaskAffinityStates.getStates(ai)
- 第三种是states.mLaunchModeStates.getStates(ai.launchMode, ai.theme), 这其实是最普遍的情形
为了分析清楚是如何分配的,我们需要先看一下ProcessStates, TaskAffinityStates以及LaunchModeStates是什么,以及它们是如何被初始化的。
3.4.1 ProcessStates的定义
ProcessStates的定义很简单:
|
|
3.4.2 LaunchModeStates
LaunchModeStates的定义其实也很简单:
|
|
LaunchModeStates的成员非常简单,就是一个Map
- NR表示 launMode为Standard
- SI表示launchMode为singleInstance
- ST表示launchMode为singleTask
- STP表示launchMode为singleTop
而主题只有两种,一种是透明(TS,translucent缩写),另一种是不透明(NTS).
LaunchModeStates的方法都很好理解,就不赘述了。
3.4.3 TaskAffinityStates
TaskAffinityStates的定义如下:
|
|
可见TaskAffinityStates其实就是LaunchModeStates的组合,它里面有一个LaunchModeStates数组,在初始化时会将各种launchMode的占位activity添加到LaunchModeStates中。
而TaskAffinityStates的init()被调用的地方有两个,一个是在PluginContainers中的init()方法中:
|
|
其实这个方法的逻辑很简单,就是分别添加UI进程和自定义进程的坑位Activity, 其中对于UI进程的坑位Activity分别添加Standard,SingleTop, SingleTask, SingleInstance的坑位Activity,然后调用TaskAffinityStates.init()方法添加这些占坑Activity带上taskAffinity的Activity.
而对于需要在自定义进程中运行的占坑Activity,其实就是轮询3个进程(目前PluginProcessHost.PROCESS_COUNT值为3),然后调用init2()将占坑Activity添加到ProcessStates对象中。
PluginContainers的init2()方法如下:
|
|
显然,这里做的事情其实跟init1()很类似,就是将4种模式并且带有进程信息(比如:p0)的占坑Activity添加到LaunchModeStates中,然后再通过TaskAffinityStates的init()在些基础上再加上taskAffinity,然后再添加到TaskAffinityStates中。
需要注意的是,目前RePlugin对于插件进程的限制为3个,对于taskAffinity的限制为2种。
最后LaunchModeStates的值类似下面这种:
|
|
而TaskAffinityStates的值类似下面这种:
|
|
3.4.4 真正分配坑位
再回到PluginContainers.alloc2()方法:
|
|
比如,如果是LAUNCH_SINGLE_INSTANCE, 由于ProcessStates中的mLaunchModeStates中已经在前面进行了如下添加操作:
|
|
- 如果是透明主题,会返回一个类似”me.ele.repluginhostsample.loader.a.ActivityN1SINTS0”这样的坑位。
- 如果是Stanadrd launchMode,并且使用默认的taskAffinity, 主题为不透明,那么会返回一个类似”me.ele.repluginhostsample.loader.a.ActivityN1NRNTS0”这样的坑位,其中NTS表示不透明。
- 如果该Activity是自定义进程,由于RePlugin目前限制一个插件中最多有3个自定义进程,所以肯定是:p0,:p1,:p2中的一个,比如为:p0则是返回类似”me.ele.repluginhostsample.loader.a.ActivityP0NRNTS0”这样的坑位 。
4. 进入插件后的startActivity()调用
Factory2也只是起一个封装作用,即它只是调用PluginLibraryInternalProxy中相应的方法来完成操作,Factory2中的startActivity(Context,Intent)方法就是调用了PluginLibraryInternalProxy的startActivity(Context,Intent)方法:
|
|