0%

Android-IpcBridge原理

概述

前段时间在读EventBus的源码,然后突然来了兴趣(闲的没事)就稍微修改了一下使其支持进程间订阅消息,实现原理很简单,其实就是进程内使用EventBus原生方式,进程间则通过Binder发送信息。实现比较简单,进程间Binder传递其实也可以不使用bindService,使用ContentProvider会更加灵活,后来就想着反正没事就试一试写个IPC框架(反正也不会有人用,练练手)。

大概原理就是通过一个AIDL接口传递要调用的类和方法名,然后通过反射调用。进程间的Binder通过ContentProvider传递(Android-IPC机制-ContentProvider),不需要bindService,然后自定义了一个Gradle插件,用来动态修改Manifest信息以及生成ContentProvider类。

项目地址:IpcBridge,ReadMe文档有使用方法。

项目工程中包括三个模块,一个是IpcBridge的Kotlin源代码,一个是IpcBridge的Gradle插件源码,主模块是demo模块。

IpcBridge

AIDL接口

1
2
3
interface IBridgeInterface {
Bundle call(in Bundle args);
}

这里参数和返回值都是Bundle类型,具体的参数和返回值都约定在Bundle里,毕竟Bundle现成就实现了Parcelable接口。

ContentProvider基础类

1
2
3
4
5
6
7
8
9
10
11
open class BaseProvider : ContentProvider() {

override fun query(
uri: Uri, projection: Array<out String>?, selection: String?,
selectionArgs: Array<out String>?, sortOrder: String?
): Cursor? {
return BinderCursor(arrayOf("service"), BridgeBinder())
}

// ...
}

此处返回的BinderCursor是一个继承了MatrixCursor的特殊Cursor,有关MatrixCursor的信息可以看这里MatrixCursor。它的使用情景就是当我想得到一个Cursor,而此时又不需要数据库来返回一个Cursor时,可以通过MatrixCursor来返回一个伪造的Cursor,而这里的作用就是通过这个Cursor对象来携带Binder对象。然后通过后面讲到的Gradle插件中动态生成的ContentProvider,将其注册到要跨进程的进程中,那么就可以嘿嘿了。

IpcBridge

IpcBridge是对外的接口,里面有三个集合:

1
2
3
4
5
6
7
8
// 存储对应接口类的对应实现类:key-接口类;value-实现类
private val mClassMap = mutableMapOf<Class<*>, Class<*>>()

// 存储对应接口类的对应实例:key-接口类;value-实例
private val mInstanceMap = mutableMapOf<Class<*>, Any>()

// 缓存客户端需要调用的远程接口的代理
private val mProxyCache = mutableMapOf<Class<*>, Any>()

在服务端进程需要注册远程接口和实现类,注册方法如下:

1
2
3
4
5
6
7
fun register(inter: Class<*>, impl: Class<*>) {
mClassMap[inter] = impl
}

fun register(inter: Class<Any>, instance: Any) {
mInstanceMap[inter] = instance
}

客户端进程可以通过获取远程接口的代理类来调用服务器进程的接口,获取代理的方法如下:

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
fun getProxy(context: Context, inter: Class<*>, authority: String): Any? {
// 首先从缓存中获取
if (mProxyCache.containsKey(inter)) {
return mProxyCache[inter] ?: newProxy(context, inter, authority)
}
return newProxy(context, inter, authority)
}

private fun newProxy(context: Context, inter: Class<*>, authority: String): Any? {
if (mBridgeInterface == null) {
// 从前面提到的ContentProvider中拿到服务端进程的binder对象
val cursor = context.contentResolver?.query(
Uri.parse("content://${authority}"), null, null, null, null
)
cursor?.let {
val binder = BinderCursor.getBinder(it)
try {
mBridgeInterface = IBridgeInterface.Stub.asInterface(binder)
} catch (e: Exception) {
e.printStackTrace()
}
}
cursor?.close()
}
// 传入接口类对象和mBridgeInterface,返回接口的代理类
val proxy = BridgeHandler.newProxyInstance(inter, mBridgeInterface ?: return null)
mProxyCache[inter] = proxy
return proxy
}

根据上面的注释,可以看看具体代理类的invoke逻辑。

代理类

通过jdk动态代理,当调用了代理类的方法时,会走invoke逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
val methodName = method?.name
args?.get(0)?.let {
if (it is Bundle) {
// 通过mBridgeInterface调用服务端进程的方法,这里传入了要调用接口的类名和方法名
it.putString("clazzName", mClass.name)
it.putString("methodName", methodName)
return mBridgeInterface.call(it)
}
}
return method?.invoke(proxy, args)
}

接下来再看看服务端的Binder实现类。

BridgeBinder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class BridgeBinder : IBridgeInterface.Stub() {
override fun call(args: Bundle?): Bundle? {
args?.let {
val clazzName = it.getString("clazzName") ?: ""
val methodName = it.getString("methodName") ?: ""
Log.i(TAG, "clazzName = $clazzName, methodName = $methodName")
try {
val clazz = Class.forName(clazzName)
val method = clazz.getMethod(methodName, Bundle::class.java)
// 通过接口名查询之前注册的实例对象
val instance = IpcBridge.getInstance(clazz)
instance?.let { inst ->
val result = method.invoke(inst, it)
return if (result is Bundle) result else null
}
} catch (e: Exception) {
e.printStackTrace()
}
}
return null
}
}

这里的逻辑主要就是通过接收到客户端进程传入的方法和类名,反射调用对应的方法,并返回返回值。

总结

IpcBridge的整体逻辑其实挺简单的,客户端进程通过获取到指定接口的代理类,然后将要调用的方法和类名传给服务端进程,服务端通过反射调用相关的实现类。

中间涉及到ContentProvider如何返回对应进程的Binder对象,以及服务端对实例对象的注册和查找。

这里有个问题,就是每个服务端进程都需要有一个ContentProvider与之对应,而且无法手动创建以及Manifest注册,因此就需要开发一个Gradle插件来在编译期间帮助我们完成这个任务。

IpcBridge Gradle

配置

在应用了IpcBridge gradle插件后,需要在Gradle脚本中进行配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IpcBridge {
providerConfigs {
BridgeProvider {
authorities "com.hearing.ipcbridge.provider"
process ":bridge"
exported false
}
TestProvider {
authorities "com.hearing.ipcbridge.test"
process ":test"
exported false
}
}
}

上面配置了两个ContentProvider:BridgeProvider和TestProvider,相关参数与Manifest参数对应。

BridgeExtension

BridgeExtension是一个Gradle自定义的扩展,可以新增一个类似于android {}的命名空间:

1
2
3
4
5
6
7
8
9
10
11
class BridgeExtension {
NamedDomainObjectContainer<ProviderConfig> providerConfigs

BridgeExtension(Project project) {
providerConfigs = project.container(ProviderConfig)
}

void providerConfigs(Action<NamedDomainObjectContainer<ProviderConfig>> action) {
action.execute(providerConfigs)
}
}

上面的ProviderConfig类与Gradle脚本中的配置参数对应:

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
class ProviderConfig {
String name
String authorities
String process
boolean exported = false

ProviderConfig(String name) {
this.name = name
}

void authorities(String authorities) {
this.authorities = authorities
}

void process(String process) {
this.process = process
}

void exported(boolean exported) {
this.exported = exported
}

@Override
String toString() {
return "name: $name, authorities: $authorities, process: $process, exported: $exported"
}
}

然后在自定义的插件中创建这个扩展:

1
project.getExtensions().create("IpcBridge", BridgeExtension, project)

要获取用户配置的扩展信息,可以这样:

1
2
3
4
BridgeExtension extension = project.getExtensions().getByName("IpcBridge")
extension.providerConfigs.each { ProviderConfig provider ->
// ...
}

创建ContentProvider

接下来根据配置新建对应的ContentProvider类,在IpcBridge的源码中已经讲到了一个BaseProvider类,新创建的Provider只需要继承它即可,比如说:

1
2
3
4
5
package com.hearing.ipcbridge;

// Automatically generated by IpcBridge.
public class BridgeProvider extends BaseProvider {
}

创建的Java文件很简单,因此不需要用到类似于JavaPoet的框架,这里生成的是Java源文件,而不是class字节码,因此需要在gradle编译Java代码的task执行之前创建,然后我们将生成的Java文件加入到sourceSets中,即可正常被Gralde构建系统所编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void init() {
def sourceSetPath = "${project.buildDir}/bridge"
srcPath = "${sourceSetPath}/com/hearing/ipcbridge"
project.android.sourceSets {
main {
java.srcDir "${project.buildDir}/bridge"
}
}
}

def src = new File("$srcPath/${provider.name}.java")
if (src.exists()) {
println("java exists.")
src.delete()
}
src.createNewFile()
src.withWriter { writer ->
writer.write("package com.hearing.ipcbridge;\n\n")
writer.write("// Automatically generated by IpcBridge.\n")
writer.write("public class ${provider.name} extends BaseProvider {\n")
writer.write("}")
}

在创建了ContentProvider源文件后,还需要在Manifest文件中注册它。

动态注册

在Gralde构建过程中有一个task叫:processDebugManifest/processReleaseManifest/more flavor,可以在这个task执行后插入我们的动态注册代码:

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
31
32
33
project.android.applicationVariants.all { variant ->
String variantName = variant.name.capitalize()
def processManifestTask = project.tasks.getByName("process${variantName}Manifest")

processManifestTask.doLast { pmt ->
pmt.manifestOutputDirectory.getAsFileTree().getFiles().each { File f ->
if (f.getName().endsWith("AndroidManifest.xml")) {
ensurePath()

println(">>>manifestPath = ${f.getAbsolutePath()}")

def manifest = f.getText()
def xml = new XmlParser().parseText(manifest)
BridgeExtension extension = project.getExtensions().getByName("IpcBridge")
extension.providerConfigs.each { ProviderConfig provider ->
println(">>----------------------------------------<<")
println("provider = $provider")
xml.application[0].appendNode(
"provider",
["android:name" : "com.hearing.ipcbridge.${provider.name}",
"android:authorities" : provider.authorities,
"android:grantUriPermissions": true,
"android:process" : provider.process,
"android:exported" : provider.exported]
)
}

def serialize = XmlUtil.serialize(xml)
f.write(serialize)
}
}
}
}

build结束后便可看到最终的Manifest文件中包含了我们插入的代码。

总结

Gradle插件主要有两个作用:

  1. 创建ContentProvider源文件
  2. 动态注册Provider

总结

IpcBridge在配置上还有些麻烦,后面有时间会考虑优化一下,争取实现最少配置,同时优化下代码。