在RePlugin中,我们经常使用的一个功能,就是在宿主或插件中,打开另一个插件中的Activity(该Activity位于独立进程中)。
本文将介绍宿主如何识别这个插件中自定义进程,并让开发者无感知的,像在单品中一样运行起来。
例如,插件中存在这样一个Activity,需要运行到“:image1”进程中:
1 进程映射
插件中的这个进程标签,宿主肯定是不认识的,所以,第一件要做的事情,就是得让宿主认识这个进程,只有认识后,才能进一步的启动、管理它。
识别的核心思想就是占坑,通过在宿主中预埋若干个可用的进程坑位(“:p0”,“:p1”,“:p2”三个进程),在运行时,将插件中的Activity进程(根本不认识)映射为宿主中的预留进程(这就认识了)。
映射过程是在插件加载时执行的,加载插件时,会读取插件AndroidMainfext.xml中四大组件的“android:process”属性,得到自定义进程列表,然后调整进程名字。
具体流程如下图所示:
1.1 获取PackageInfo
使用PackageManager的getPackageArchiveInfo接口,可以根据传入的插件APK绝对路径,获取到一个未安装APK的PackageInfo信息:
PackageInfo packageInfo=PackageManager.getPackageArchiveInfo(path);
1.2 拿到插件的四大组件列表
取到了PackageInfo,也就意味着已经取到了APK中的四大组件了,我们定义一个数据结构存储起来。
class ComponentList {
// Activity列表
finalHashMap<String, ActivityInfo> mActivities = new HashMap<>();
// Provider列表
finalHashMap<String, ProviderInfo> mProvidersByName = new HashMap<>();
// Service列表
finalHashMap<String, ServiceInfo> mServices = new HashMap<>();
// Receiver列表(系统中直接用ActivityInfo来承载一个Receiver了)
finalHashMap<String, ActivityInfo> mReceivers = new HashMap<>();
}
1.3 生成进程映射表
生成进程映射表,有两种方式:
静态映射:
通过在插件APK的AndroidMainfext.xml文件中手工配置进程映射文件:
<meta-data
android:name="process_map"
android:value="[{'from':'com.qihoo360.replugin.sample.demo1:bg','to':'$p0'}]"/>
宿主在加载插件时,会读取插件AndroidMainfext的meta-data配置,根据配置调整其进程标识。
动态映射:
动态的将插件APK的AndroidMainfext.xml文件中的自定义进程标签映射到宿主。
1)int processIndex = 插件中自定义进程数 % 宿主中坑位进程数。
2)String process = 宿主进程坑位数组[processIndex]
具体过程如下:
1.4 调整进程名字
根据以上步骤取到的四大组件列表,以及进程映射表,调整插件中组件的进程名称,四大组件依次调整:
//Activity
adjust(HashMap<String, String> processMap, activityMap);
//Service
adjust (HashMap<String, String> processMap, serviceMap);
//provider
adjust (HashMap<String, String> processMap, providerMap);
//receiver
adjust (HashMap<String, String> processMap, receiverMap);
具体如何调整?
来看Android中描述四大组件信息的类图:
从上图可以看出,只要我们修改ComponentList数据结构中存储的,四大组件信息Map中的,ComponentInfo的 processName字段即可修改该组件的进程名字。
运行到这里,当插件被加载时,在内存中,插件中四大组件的android:process=”:xxxProcess”标签(宿主并不认识),就已经被完全替换成android:process=”:p0”(宿主认识)了。
接下来会介绍,这些被修改后的坑位进程是如何被使用的。
2 启动进程
宿主编译期间,会通过Gradle脚本,在AndroidMainfest.xml中注入了3个Provider(在processXXXXManifestTask 被执行后):
<provider android:name="com.qihoo360.replugin.component.process.ProcessPitProviderP0/1/2"
android:exported="false"
android:process=":p0"
android:authorities="{packageName}.loader.p.mainN100"/>
同时,宿主中也内置了ProcessPitProviderP0,ProcessPitProviderP1,ProcessPitProviderP2三个Provider。
我们想启动P0进程,只需要执行:
getContentResolver().insert(ProcessPitProviderP0Uri, values),即可借助Provider的同步特性,拉活ProcessPitProviderP0所在的进程。
为什么我们尝试去查P0进程中的一个Provider,就能把P0进程唤醒呢?这个问题要从系统源码中寻找答案。
我们需要从Application对象的创建过程开始看起,在ActivityThread的main()方法中,我们看这个逻辑:
ActivityThread#attach()方法,一开始就做了以下事情:
其中,红框中的IActivityManager mgr,就是AMS留在APP进程中的代理,使用这个代理对象,从APP进程调用到AMS所在进程,AMS中具体的实现逻辑位于ActivityManagerService中的attachApplicationLocked ()方法(不同Android版本可能略有不同):
通过上图,可以看到,首先,会通过pid取到ProcessRecord对象,并为ProcessRecord对象填充必要数据。
接下来,调用PMS,获取该应用的Provider列表:
然后,通过APP进程留在AMS中的代理IApplicationThread thread,调用APP进程中的bindApplication()方法:
如图所示,processName,ApplicationInfo,providers,等信息,也跨进程传递给了APP进程。
APP进程收到调用后,会通过Handler(H),从当前所在的Binder线程,分发到UI线程:
在UI线程中,真正处理这个消息的是handleBindApplication()方法:
在handleBindApplication()方法中,会真正的创建Application,我们来看详细过程:
1)获取LoadedApk(ClassLoader和Resource都会随之被获取到)
2)创建ContextImpl
3)makeApplication
makeApplication 中,通过以上拿到的ClassLoader,加载Application的class,并且创建Application的实例,随后调用Application的attach()方法,在 attach()方法中,将会回调Application的attachBaseContext()接口。
4)installContentProviders(),即:在本地进程安装所有Providers ,并且把这些Providers的Binder,publish给AMS。
仔细来看installProvider():
A)首先,会在本地进程中,依次安装每一个ContentProivder,即:加载Provider的class,创建其实例,然后通过ContentProivder的attachInfo方法,调用ContentProivder的onCreate()接口。
B)然后,为每一个Provider创建一个ContentProviderHolder封装对象,这些封装对象,将会一起发布给AMS。
5)借助Instrumentation ,回调Application 的 onCreate() 接口。
分析到这里,我们就可以看出来了:
从APP进程的角度来看,ContentProivder的install过程,是介于Application#attachBasecontext()和Application#onCreate()之间的,ContentProivder的初始化,是和Application的初始化耦合在一起的。
换句话说,当我们尝试去query一个进程中的ContentProivder时,这个进程会被同步拉活,有了这个机制,当我们想启动一个进程时,就可以先同步拉活它,然后通过Binder,在该进程中执行必要的逻辑了。
3 进程回收
每一个坑位进程启动后,都会同时启动一个定时任务,每20秒轮循一次,发现当前进程中没有组件正在运行,则自杀(可以参考AMS中进程管理逻辑)。
如何定义没有组件正在运行?须同时满足以下条件:
{
当前持有activities==0
当前持有services ==0
当前持有binders ==0
}
有一点需要我们注意,就是,我们前面提到插件进程的动态分配过程。
例如:插件中 A 进程和 B 进程,都被动态分配到 P0进程中,这时候 A 进程做完了事情,主动调用了自杀逻辑,就会连累 B 进程也被意外杀死。
这种情况,可以通过编译期字节码修改来解决,即:在插件编译期间,通过Gradle 插件+字节码修改技术(如ASM,Javassist,AspectJ等技术),把System.exit(),android.os.Process.killProcess(pid)等能杀死进程的方法调用换成我们自己的实现逻辑,如:System2.exit2(),当插件中某个自定义进程尝试自杀时,通过计数器来判断是否可以真正的自杀。
4 启动自定义进程后的Activity
4.1 Activity坑位信息
前面,已经介绍了如何把插件中的自定义进程,映射为宿主中的预埋占坑进程,并且介绍了这些自定义进程的启动过程,接下来就来看看,具体的Activity启动过程。
先来看预埋的Activity坑位,宿主中的坑位信息如下图所示(UI进程和自定义进程P0,P1,P2中都包含一组完整坑位):
每个进程中的一组Activity坑位:
例如,以下Activity坑位代表的就是:运行在”:p2”进程中,taskAffinity=”:t3”的,launchMode=”singleTask”的坑位Activity,AMS将直接与它交互。
4.2 启动过程
自定义进程Activity,启动流程如下图所示:
1) 在宿主中或插件中,通过RePlugin.startActivity()启动插件中的Activity,会先入常驻进程(或者叫插件管理进程)。
2) 在插件管理进程中,插件管理器,会为该Activity分配一个目标进程(参考上述介绍的进程分配过程)。
3) 然后,同步的启动该目标进程(参考上述介绍的进程启动过程)。
4) 在插件管理进程中,通过Binder调用,使用目标进程留在插件管理进程中的代理,在目标进程中为该Activity动态分配坑位(每个进程中独立管理自己的Activity坑位信息,此时该坑位Activity还没有被AMS打开)。
5) 把在目标进程中分配好的Activity坑位信息返回给调用方(Step1)。
6) 调用者获取到了一个坑位Activity,然后会把这个坑位Activity(带着自定义进程标签),会交给AMS,由于AMS去启动。
7) AMS启动Activity是一个并不存在的坑位Activity,由于宿主的ClassLoader已经在进程启动之初(Application#attachBaseContext()接口中),被修改为我们自定义过的ClassLoader了,当类加载器去loadClass时,如果发现当前正在被load的Class是一个坑位Activity,会自动分发给指定插件的ClassLoader去加载,这样一来,就既绕过了AMS,也绕过了ClassLoader,实现了:打开一个并未安装的Activity。
到现在为止,本文之初提到的,插件中的独立进程Activity,就被我们打开了。在后续的系列文章中,我们将继续介绍Activity打开过程中涉及到的ClassLoader、Resources、Layout相关处理、Activity坑位分配算法,以及其他技术细节。