markWu-野火燎原头像
关注

Android 系统层扫盲番外:从 Interface、Contract 到 AIDL,一步看懂跨进程接口是怎么演进出来的

上一篇我们讲到:

system_server
↓
SystemServer
↓
AMS / PMS / WMS / PowerManagerService / ...

大量 Android Framework 核心服务,其实都运行在:

system_server

进程里。

而我们自己的 App 却运行在:

App Process

于是一个问题自然出现了:

App Process

PowerManager
     │
     │ ???
     ↓

system_server

PowerManagerService

它们明明是:

两个独立的 Linux Process。

为什么 App 还能像调用普通接口一样:

powerManager.xxx()

最终让:

PowerManagerService

真正执行逻辑?

先不要急着进入 Binder。

我们先从 Android App 开发中最熟悉的:

Interface
Contract

开始。

这一篇只用一个例子:

userService.getUser(1)

然后一步一步升级:

直接调用
↓
Interface
↓
Contract
↓
跨进程
↓
Proxy
↓
Stub
↓
Binder
↓
AIDL

看看:

一个最普通的接口调用,是怎么一步一步演进成跨进程调用的。


一、第一版:直接调用实现类

先写最简单的代码。

data class User(
    val id: Int,
    val name: String
)

然后:

class UserServiceImpl {

    fun getUser(id: Int): User {
        return User(
            id = id,
            name = "Tom"
        )
    }
}

调用:

val userService =
    UserServiceImpl()

val user =
    userService.getUser(1)

现在:

userService

实际就是:

UserServiceImpl 对象

所以:

userService.getUser(1)

真实调用链非常简单:

Caller
↓
UserServiceImpl
↓
getUser(1)
↓
返回 User

没有接口。

没有代理。

没有 IPC。

就是:

直接拿实现对象,然后直接调用方法。


二、直接依赖实现类有什么问题?

现在调用方明确知道:

UserServiceImpl

也就是说:

Caller
↓
直接依赖具体实现

假设以后:

UserServiceImpl

要换成:

RemoteUserService

MockUserService

CacheUserService

调用方可能也要跟着调整。

所以通常会做第一次抽象:

Interface

三、第二版:增加 Interface

定义:

interface UserService {

    fun getUser(id: Int): User
}

实现类:

class UserServiceImpl :
    UserService {

    override fun getUser(id: Int): User {
        return User(
            id = id,
            name = "Tom"
        )
    }
}

调用方:

val userService: UserService =
    UserServiceImpl()

val user =
    userService.getUser(1)

注意:

userService.getUser(1)

这一行基本没变。

但:

userService

的类型已经变成:

UserService

四、现在这一行代码到底调用了谁?

这是理解后面所有内容的关键。

编译时:

userService
↓
UserService Interface

但运行时:

userService
↓
实际指向 UserServiceImpl 对象

所以:

userService.getUser(1)

真正执行:

Caller
↓
UserService
↓
运行时找到 UserServiceImpl
↓
UserServiceImpl.getUser(1)

最终干活的:

还是 UserServiceImpl。

所以 Interface 没有改变:

真正由谁执行业务

它改变的是:

调用方依赖谁

以前:

Caller
↓
UserServiceImpl

现在:

Caller
↓
UserService
↓
UserServiceImpl

也就是:

调用方只依赖能力定义,不依赖具体实现。


五、这就是最基础的面向接口编程

可以直接记:

Interface
=
定义“能做什么”
Impl
=
决定“具体怎么做”

调用方只关心:

userService.getUser(1)

至于后面到底是:

数据库
网络
Mock
缓存

都不重要。

这个思想接下来不会变。


六、第三版:Interface 变成模块 Contract

现在项目大了。

我们把 User 功能拆成:

app

user-contract

user-impl

结构可以理解:

        user-contract
        ↑           ↑
        │           │
       app       user-impl

app 不直接依赖:

user-impl

只依赖:

user-contract

七、Contract 模块只定义能力

前面我们只有一个用户模块:

user

现在把它拆成两个模块:

user-contract
user-impl

它们的职责完全不同:

user-contract
    ↓
定义“用户模块能提供什么能力”

user-impl
    ↓
负责“这些能力具体怎么实现”

目录大概可以这样:

user-contract
└── src/main/java/com/example/user/contract
    ├── User.kt
    └── UserContract.kt

user-impl
└── src/main/java/com/example/user/impl
    └── UserContractImpl.kt

先看:

user-contract

它只保存对外需要暴露的内容。

比如先定义模块之间需要传递的数据:

package com.example.user.contract

data class User(
    val id: Int,
    val name: String
)

然后定义用户模块能够提供的能力:

package com.example.user.contract

interface UserContract {

    fun getUser(id: Int): User
}

这里的意思非常简单:

外部模块
   │
   │ userId
   ▼
UserContract
   │
   │ User
   ▼
外部模块

UserContract 只是在声明:

给我一个 userId,我可以给你一个 User。

但是:

User 到底从哪里来?

Contract 完全不知道。

它可能来自:

网络
数据库
缓存
文件
本地内存

这些都属于实现细节。

所以:

user-contract

里面不会出现:

Retrofit
Room
Repository
ViewModel
具体业务逻辑

它只负责定义协议。


真正的实现放到:

user-impl

例如:

package com.example.user.impl

import com.example.user.contract.User
import com.example.user.contract.UserContract

class UserContractImpl : UserContract {

    override fun getUser(id: Int): User {

        // 真正项目中这里可能调用:
        // Repository
        // Retrofit
        // Room
        // Cache
        // 等等

        return User(
            id = id,
            name = "Tom"
        )
    }
}

这时候:

UserContract

负责:

定义能力

而:

UserContractImpl

负责:

实现能力

关系就是:

UserContract
     ▲
     │ implements
     │
UserContractImpl

注意:

user-impl 需要依赖 user-contract。

所以:

// user-impl/build.gradle.kts

dependencies {
    implementation(project(":user-contract"))
}

因为:

UserContractImpl

需要实现:

UserContract

并且使用:

User

因此整个依赖关系是:

user-impl
    │
    ▼
user-contract

而调用方,比如:

app

真正应该依赖的是:

user-contract

例如:

// app/build.gradle.kts

dependencies {
    implementation(project(":user-contract"))
}

这样 app 里面的业务代码只需要认识:

UserContract

而不需要认识:

UserContractImpl

例如我们在 MainActivity 中使用它:

package com.example.app

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.user.contract.UserContract

class MainActivity : AppCompatActivity() {

    private lateinit var userContract: UserContract

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val textView = TextView(this)
        setContentView(textView)

        val user = userContract.getUser(1)

        textView.text = """
            id = ${user.id}
            name = ${user.name}
        """.trimIndent()
    }
}

注意这里:

private lateinit var userContract: UserContract

app 持有的是:

UserContract

而不是:

UserContractImpl

真正调用时也是:

val user = userContract.getUser(1)

调用方只关心:

我能调用 getUser()

而完全不关心:

getUser() 内部到底怎么实现

所以对于 app 来说,它看到的只有:

app
 │
 ▼
UserContract

真正的实现则在另一边:

UserContract
 ▲
 │
UserContractImpl

合在一起就是:

           UserContract
           ▲          ▲
           │          │
           │          │
          app     user-impl

或者从调用链来看:

app
 │
 │ getUser()
 ▼
UserContract
 ▲
 │ implements
 │
UserContractImpl

这就是我们真正想要的结构:

调用方依赖能力定义,而不是依赖具体实现。

不过你应该马上能发现一个问题。

刚才代码里我们写了:

private lateinit var userContract: UserContract

但是:

userContract 到底是谁赋值的?

如果我们直接这样写:

val userContract: UserContract = UserContractImpl()

那么 app 又必须:

import com.example.user.impl.UserContractImpl

并且重新依赖:

implementation(project(":user-impl"))

这样就又回去了:

app
 ↓
user-impl
 ↓
user-contract

那前面拆 Contract 的意义就被削弱了。

所以现在真正的问题变成了:

app 只认识 UserContract

但是运行时:

UserContract
      ↓
到底由哪个 UserContractImpl 来实现?

也就是说,我们现在已经解决了:

“调用方应该依赖谁?”

答案是:

Contract

但还没有解决:

“Contract 的具体实现从哪里来?”

这个问题,才是后面整个架构继续演进的关键。


八、App 怎么调用 Contract?

比如通过 DI:

class HomeViewModel(
    private val userService: UserContract
) {

    fun loadUser() {

        val user =
            userService.getUser(1)
    }
}

调用仍然是:

userService.getUser(1)

没有变化。


九、Contract 以后,这一行实际调用谁?

假设 DI 最终提供:

UserContractImpl

那么运行时:

App Module

HomeViewModel
↓
UserContract
↓
实际对象 UserContractImpl
↓
getUser(1)
↓
user-impl Module

所以:

模块虽然分开了,但运行时仍然是普通方法调用。

这一点特别重要。


十、跨模块 ≠ 跨进程

现在:

app

user-contract

user-impl

虽然是三个 Gradle Module。

但是最终运行时完全可以全部位于:

同一个 App Process

因此:

App Module
↓
UserContract
↓
UserContractImpl

仍然是:

同一个 ART 进程里的对象调用。

所以:

模块边界

只是:

代码和依赖边界。

而:

进程边界

是:

操作系统级运行边界。

这两者不是一回事。


十一、到这里其实已经很接近 Binder 的设计思想了

现在:

Client
↓
Contract
↓
Impl

调用方根本不在意:

Impl

怎么实现。

它只管:

userService.getUser(1)

那么我们自然可以继续问:

如果 Impl 不仅跑到另一个 Module,而是直接跑到另一个 Process,会发生什么?

这时候事情才真正开始变化。


十二、第四版:把 UserContractImpl 放到另一个进程

原来:

Process A

Caller
↓
UserContract
↓
UserContractImpl

现在改成:

Process A

Caller
↓
UserContract


=====================
     进程边界
=====================


Process B

UserContractImpl

然后再看以前这一行:

val userService: UserContract =
    UserContractImpl()

现在还能这么写吗?

答案:

不能。


十三、为什么跨进程以后不能直接拿 Impl?

因为:

Process A

和:

Process B

拥有彼此独立的:

虚拟地址空间

Heap

Stack

Java Object

UserContractImpl 对象实际存在:

Process B Heap

Process A 不能直接拿着:

Process B

里面一个 Java 对象的引用。

所以:

Process A

UserContract
↓
UserContractImpl

原来这条直接连接:

断掉了。


十四、注意:坏掉的不是 Contract 思想

这里非常容易误解。

不是:

Interface 跨进程以后没用了。

也不是:

Contract 设计错了。

真正失效的是:

“调用方直接拿 Impl 对象,然后直接调用方法”这件事。

我们依然非常希望:

userService.getUser(1)

这样写。

因为这才是最自然的业务接口。

所以问题变成:

既然 Process A 拿不到 Process B 的真实 Impl,那我能不能在 Process A 放一个假的 Impl?

可以。

于是:

Proxy

出现了。


十五、第五版:加入 Proxy

在 Process A 中写:

class UserServiceProxy :
    UserContract {

    override fun getUser(
        id: Int
    ): User {

        TODO("把请求发送给 Process B")
    }
}

注意:

UserServiceProxy

同样实现:

UserContract

所以调用方仍然可以:

val userService: UserContract =
    UserServiceProxy()

val user =
    userService.getUser(1)

业务代码:

userService.getUser(1)

仍然没有改变。


十六、但这一行现在真正调用的是谁?

以前:

userService
↓
UserContractImpl

现在:

userService
↓
UserServiceProxy

所以:

userService.getUser(1)

真实调用链已经变成:

Caller
↓
UserContract
↓
实际对象 UserServiceProxy
↓
Proxy.getUser(1)

注意:

Proxy 不是真正的业务实现。

它不会真的去:

查数据库

请求网络

读取缓存

它只是:

远端 Impl 在当前进程中的替身。


十七、Proxy 为什么一定要实现同一个 Contract?

因为我们不希望调用方知道:

当前实现到底在本地
还是在另一个进程

调用方只依赖:

UserContract

所以:

本地实现

可以是:

UserContractImpl

而:

远程实现

在 Client 这边则可以表现成:

UserServiceProxy

两者都:

implements UserContract

于是:

Client
↓
UserContract

下面到底接:

Impl

还是:

Proxy

Client 不需要知道。

这就是代理模式非常关键的地方。


十八、那 Proxy.getUser(1) 到底干什么?

我们继续实现:

class UserServiceProxy(
    private val ipc: Ipc
) : UserContract {

    override fun getUser(
        id: Int
    ): User {

        val request =
            Request(
                method = "getUser",
                args = listOf(id)
            )

        val response =
            ipc.send(request)

        return response.toUser()
    }
}

现在:

userService.getUser(1)

实际:

Caller
↓
UserContract
↓
UserServiceProxy
↓
Proxy.getUser(1)
↓
构造 Request

Request:

method = getUser

id = 1

然后:

ipc.send(request)

十九、这里发生了一个非常重要的变化

最开始:

getUser(1)

是:

方法调用

现在 Proxy 把它转换成:

一份数据

例如:

我要调用的方法:
getUser

参数:
id = 1

为什么?

因为:

Java Method 本身不能直接穿过 Linux Process Boundary。

跨进程之前,必须先把:

“我要调用什么”

和:

“参数是什么”

描述出来。

所以:

方法调用
↓
Proxy
↓
变成可传输数据

这是理解 Binder 的关键一步。


二十、Request 到 Process B 以后怎么办?

假设:

ipc.send()

已经把请求送过去。

Process B 收到:

method = getUser

id = 1

但这只是:

数据

真正业务实现仍然是:

UserContractImpl.getUser(1)

所以现在还需要有人做:

数据
↓
重新变成方法调用

于是:

Stub

出现了。


二十一、第六版:加入 Stub

我们自己写一个最简单的:

class UserServiceStub(
    private val impl: UserContract
) {

    fun onRequest(
        request: Request
    ): Response {

        return when (
            request.method
        ) {

            "getUser" -> {

                val id =
                    request.args[0] as Int

                val user =
                    impl.getUser(id)

                Response(user)
            }

            else -> {
                error("Unknown method")
            }
        }
    }
}

它干的事情其实非常直接。

收到:

method = getUser

id = 1

以后:

识别方法
↓
取出参数
↓
impl.getUser(1)

最终进入:

UserContractImpl.getUser(1)

二十二、现在完整调用链终于跑通了

Process A:

Caller
↓
UserContract
↓
UserServiceProxy
↓
Proxy.getUser(1)
↓
把调用转换成 Request
↓
method = getUser
id = 1
↓
IPC

跨过:

=====================
     进程边界
=====================

到 Process B:

Stub
↓
识别 getUser
↓
读取 id = 1
↓
UserContractImpl
↓
getUser(1)
↓
返回 User

现在终于真正做到:

userService.getUser(1)

虽然真正 Impl:

根本不在当前 Process

调用还是完成了。


二十三、Proxy 和 Stub 到底分别是什么?

现在就不需要背定义了。

Proxy

做:

方法调用
↓
转换成远程请求

所以:

Proxy = 远程对象在 Client 进程里的本地替身。


Stub

做:

远程请求
↓
重新转换成方法调用

所以:

Stub = Server 侧接收和分发远程调用的入口。

两边刚好相反:

Interface Call
↓
Proxy
↓
IPC
↓
Stub
↓
Interface Call

二十四、返回值也必须跨进程

Server:

UserContractImpl.getUser(1)

返回:

User(
    id = 1,
    name = "Tom"
)

但这个:

User Object

存在:

Process B

不能直接把对象引用塞给:

Process A

所以返回值同样需要:

User
↓
转换成数据
↓
IPC
↓
Process A
↓
重新得到 User

所以远程调用完整其实是:

Request
↓
Server
↓
Response

二十五、到这里我们已经自己“发明”出 RPC 了

现在整个模型:

Client
↓
Interface
↓
Proxy
↓
Request
↓
IPC
↓
Stub
↓
Impl

已经是:

RPC

也就是:

Remote Procedure Call

远程过程调用。

什么意思?

真正的方法:

getUser()

在:

Process B

但 Process A 写起来仍然像:

userService.getUser(1)

本地方法调用。


二十六、现在问题只剩一个:IPC 到底怎么实现?

前面我们故意写了一个不存在的抽象:

ipc.send(request)

但真正 Android 中:

谁负责把数据从 Process A 运到 Process B?

答案之一就是:

Binder

所以我们刚才自己设计出来的:

Interface
↓
Proxy
↓
Request
↓
IPC
↓
Stub
↓
Impl

到了 Android Binder 世界,会进一步变成:

AIDL Interface
↓
Proxy
↓
Parcel
↓
Binder
↓
Stub
↓
Impl

二十七、Request 在 Binder 里会变成什么?

刚才我们的 Request:

method = getUser

id = 1

到了 Binder 世界,可以拆成两部分:

Transaction Code
+
Parcel

Transaction Code:

告诉 Server
我要调用哪个方法

例如:

TRANSACTION_getUser

Parcel:

装具体参数

例如:

id = 1

所以:

getUser(1)

会被 Proxy 转换成:

Method:
TRANSACTION_getUser

Data:
Parcel {
    id = 1
}

二十八、Parcel 可以理解成 Binder 的参数箱

例如:

getUser(1)

Proxy 可以做:

Parcel
↓
writeInt(1)

Server 收到以后:

Parcel
↓
readInt()
↓
id = 1

返回值也一样:

User
↓
写入 Reply Parcel
↓
跨进程
↓
Client 从 Reply Parcel 读取

所以:

Parcel 负责承载 Binder RPC 的参数和返回值。


二十九、Binder 真正负责什么?

现在:

Proxy

已经把:

方法
参数

转换好了。

真正需要跨越:

Process A

和:

Process B

的,就是:

Binder

于是:

Process A

Proxy
↓
Parcel
↓
Binder

经过:

=====================
     进程边界
=====================

进入:

Process B

Stub
↓
Impl

所以可以非常清楚地区分:

Interface / Contract 解决代码边界。

而:

Binder 解决进程边界。


三十、为什么还需要 AIDL?

现在假设:

UserService

有几十个方法:

getUser()

deleteUser()

updateUser()

getUserList()

login()

logout()

updateAvatar()

...

那么每个方法我们都要自己写:

Proxy

Request / Parcel

Transaction Code

Stub

参数解析

返回值解析

显然很麻烦。

于是 Android 提供:

AIDL

三十一、AIDL 是什么?

AIDL 全称:

Android Interface Definition Language

可以写:

interface IUserService {

    User getUser(int id);

}

我们只负责:

定义跨进程接口。

AIDL 工具会帮我们生成大量:

Proxy

Stub

Transaction Code

Parcel 读写

transact

onTransact

相关样板代码。


三十二、所以 AIDL 可以怎么理解?

对有模块化经验的人来说,可以先理解成:

AIDL = 跨进程版本的 Contract 描述。

普通模块:

interface UserContract {
    fun getUser(id: Int): User
}

跨进程:

interface IUserService {
    User getUser(int id);
}

核心思想都一样:

先定义能力,再隐藏具体实现。

区别是:

Contract

背后的 Impl 通常:

就在同一个 Process

而:

AIDL Interface

背后的 Impl:

可以在另一个 Process

因此需要:

Proxy
Binder
Stub

来搭桥。


三十三、AIDL 生成的 Proxy,本质就是刚才我们手写的 Proxy

概念代码:

User getUser(int id) {

    Parcel data =
        Parcel.obtain();

    Parcel reply =
        Parcel.obtain();

    data.writeInt(id);

    remote.transact(
        TRANSACTION_getUser,
        data,
        reply,
        0
    );

    return readUser(reply);
}

核心仍然:

getUser(1)
↓
把参数写进去
↓
发送远程调用
↓
读取返回值

没有什么魔法。


三十四、AIDL 生成的 Stub 也一样

Server:

Stub.onTransact()

可以概念化理解:

switch (code) {

    case TRANSACTION_getUser:

        int id =
            data.readInt();

        User user =
            getUser(id);

        writeUser(
            reply,
            user
        );

        break;
}

还是:

远程数据
↓
识别方法
↓
读取参数
↓
执行接口
↓
写返回值

所以:

AIDL 本质就是把我们刚才手写的 Proxy / Stub 自动化了。


三十五、现在四次升级就非常清楚了

第一阶段:直接实现

Caller
↓
UserServiceImpl

调用:

userService.getUser(1)

实际:

UserServiceImpl.getUser(1)

第二阶段:Interface

Caller
↓
UserService
↓
UserServiceImpl

调用仍然:

userService.getUser(1)

实际仍然:

UserServiceImpl.getUser(1)

变化:

调用方不依赖具体实现类型。


第三阶段:模块 Contract

App Module
↓
UserContract
↓
User Impl Module
↓
UserContractImpl

调用仍然:

userService.getUser(1)

实际仍然:

UserContractImpl.getUser(1)

变化:

接口成为模块之间的能力边界。

但:

仍然是同进程直接调用

第四阶段:跨进程

Client Process
↓
IUserService
↓
Proxy
↓
Parcel
↓
Binder
↓
Stub
↓
UserServiceImpl
↓
Server Process

调用仍然:

userService.getUser(1)

但实际:

Proxy.getUser(1)
↓
Binder RPC
↓
UserServiceImpl.getUser(1)

变化:

Interface 和 Impl 之间出现了 Linux Process Boundary。


三十六、真正变化的是“Interface 到 Impl 的距离”

最开始:

Caller
↓
Impl

距离:

一个对象。

后来:

Caller
↓
Interface
↓
Impl

距离:

一个抽象层。

再后来:

App Module
↓
Contract
↓
Impl Module

距离:

一个模块边界。

最后:

Client Process
↓
Interface
↓
Proxy
↓
Binder
↓
Stub
↓
Server Process

距离:

一个操作系统进程边界。

所以:

边界越远,需要的基础设施越多。


三十七、但 userService.getUser(1) 为什么可以一直不变?

这其实就是整篇最有意思的地方。

第一版:

userService.getUser(1)

第二版:

userService.getUser(1)

模块化:

userService.getUser(1)

跨进程:

userService.getUser(1)

调用方式几乎没变。

但是下面已经从:

直接调用 Impl

一路演进成:

Proxy
↓
Parcel
↓
Binder
↓
Stub
↓
远端 Impl

这就是:

接口抽象真正厉害的地方。

调用方只关心:

我需要什么能力?

而不用关心:

实现在哪个类?

在哪个模块?

甚至在哪个进程?

三十八、Proxy 本质也是一个“实现类”

这个非常值得记。

假设:

UserContract

现在有两个实现:

UserContractImpl

和:

UserServiceProxy

它们都:

implements UserContract

但区别:

UserContractImpl
↓
自己真正干活

而:

UserServiceProxy
↓
自己不干业务
↓
把工作转给远端 Impl

所以:

Proxy 本质上也是 Interface 的一个实现。

只不过它实现 UserContract 的方式不是“自己完成业务”,而是“帮你找到真正干活的人”。

只是:

它的实现方式是“远程转发”。

三十八(append)、我们可以用“海外代购”来理解。

假设张三想买一套韩国化妆品。

真正能够提供商品的是:

韩国商家

我们把它理解成:

UserContractImpl

韩国商家真正负责:

收订单
↓
找商品
↓
扣库存
↓
打包
↓
发货

也就是说:

真正的业务

是在它那里完成的。

但是现在有个问题:

张三在中国
韩国商家在韩国

张三没办法直接跑到韩国商家那里调用:

买这个商品

怎么办?

中间出现一个:

海外代购

张三只需要告诉代购:

商品:A
数量:2
规格:XX
收货地址:XX

代购收到这些信息之后:

张三
 ↓
海外代购
 ↓
把订单传给韩国商家
 ↓
韩国商家真正处理订单

这里的海外代购,就非常像:

UserServiceProxy

关键就在这里:

为什么代购也必须“懂购买协议”?

因为张三本来想调用的是:

购买商品

那么代购至少也得能够接收:

商品是什么
数量是多少
规格是什么
钱是多少

也就是说,代购必须和真正的商家遵守同一套:

购买协议

这套协议就相当于:

UserContract

例如:

interface BuyContract {

    fun buy(
        productId: String,
        count: Int
    ): Product
}

真正商家:

class KoreaShop : BuyContract {

    override fun buy(
        productId: String,
        count: Int
    ): Product {

        // 真正扣库存、生成订单、发货

        return Product(...)
    }
}

代购:

class OverseasProxy : BuyContract {

    override fun buy(
        productId: String,
        count: Int
    ): Product {

        // 自己没有库存
        // 自己也不真正发货

        // 把 productId、count
        // 转发给韩国商家

        ...
    }
}

于是对于张三来说:

BuyContract
     ▲
     │
     ├── KoreaShop
     │
     └── OverseasProxy

它们两个都是:

BuyContract

但是一个:

KoreaShop
↓
真正干活

另一个:

OverseasProxy
↓
帮你传话
↓
让真正的人干活

换回 Android IPC:

张三

=

客户端 App

BuyContract

=

UserContract

海外代购
=

UserServiceProxy

韩国真正商家
=
UserContractImpl

所以整个过程就是:

客户端
  │
  │ getUser(1)
  ▼
UserServiceProxy
  │
  │ 把方法名 + 参数打包
  ▼
Binder IPC
  │
  ▼
远端进程
  │
  ▼
UserContractImpl
  │
  │ 真正执行 getUser(1)
  ▼
返回 User

当然,真正的 Binder 中间还会多一个 Stub:

客户端
  ↓
Proxy
  ↓
Binder
  ↓
Stub
  ↓
Impl

所以更完整地说:

app
 │
 │ getUser(1)
 ▼
UserServiceProxy
 │
 │ IPC
 ▼
Binder
 │
 ▼
UserServiceStub
 │
 ▼
UserContractImpl
 │
 │ 真正干活
 ▼
User

但站在客户端角度,它根本不需要知道这些。

客户端手里拿到的可能只是:

val userService: UserContract

然后正常调用:

val user = userService.getUser(1)

客户端甚至不需要知道:

这个 UserContract
到底是 UserContractImpl

还是 UserServiceProxy

因为:

UserContractImpl
        ▲
        │
        │ implements
        │
UserContract

UserServiceProxy
        ▲
        │
        │ implements
        │
UserContract

两者对调用方来说:

长得一样
↓
都提供 UserContract 的能力

只是内部实现完全不同:

UserContractImpl
↓
自己做

UserServiceProxy
↓
让别人做

所以 Proxy 最核心的一句话就是:

我看起来像真正的服务,我也实现同一个 Contract,但真正的业务不是我做的,我只是替你把调用送到真正的服务那里。

这也解释了为什么一定要让 Proxy:

implements UserContract

因为只有这样,调用方才能完全不用改变原来的调用方式:

userContract.getUser(1)

本地实现也这么调:

UserContract
↓
UserContractImpl

远程调用仍然这么调:

UserContract
↓
Proxy
↓
IPC
↓
Stub
↓
UserContractImpl

调用方式没变,只是实现方式从“本地直接执行”,变成了“Proxy 远程转发”。

这就是 Proxy 最有意思的地方。


三十九、Stub 则负责把远程调用重新接回 Impl

Proxy:

接口调用
↓
变成远程消息

Stub:

远程消息
↓
重新变成接口调用

所以:

Client
↓
Interface
↓
Proxy
↓
IPC
↓
Stub
↓
Interface
↓
Impl

其实整条链头尾:

仍然都是 Interface。

IPC 只是中间桥梁。


四十、现在映射回真实 Android Framework

回到上一篇留下的问题:

App Process

PowerManager
     │
     │ ???
     ↓

system_server

PowerManagerService

现在:

???

已经可以先展开成:

PowerManager
↓
IPowerManager
↓
Proxy
↓
Binder
↓
Stub
↓
PowerManagerService

所以:

App Process

PowerManager
↓
IPowerManager.Proxy
↓
Binder

=====================
     进程边界
=====================

IPowerManager.Stub
↓
PowerManagerService

system_server

是不是和我们刚才的:

UserContract
↓
Proxy
↓
Binder
↓
Stub
↓
UserContractImpl

完全是同一套思想?


四十一、WindowManager 也是一样

App Process

WindowManager
↓
IWindowManager
↓
Proxy
↓
Binder

=====================

Stub
↓
WindowManagerService

system_server

AMS:

App
↓
IActivityManager
↓
Binder
↓
ActivityManagerService

ATMS:

App
↓
IActivityTaskManager
↓
Binder
↓
ActivityTaskManagerService

所以:

Android Framework 并没有凭空发明一种完全陌生的设计思想。

它只是把我们平时熟悉的:

Interface
↓
Impl

扩展到了:

跨进程

这个级别。


四十二、三个边界现在可以连起来了

类之间

Interface
↓
Impl

解决:

调用方和具体实现类解耦。


模块之间

Contract
↓
Module Impl

解决:

模块调用方和模块内部实现解耦。


进程之间

AIDL Interface
↓
Proxy
↓
Binder
↓
Stub
↓
Remote Impl

解决:

调用方和另一个 Linux Process 中的实现建立接口调用关系。


四十三、所以 Interface、Contract、AIDL 不是三个毫无关系的东西

更好的理解应该是:

Interface
↓
最基础的接口抽象

扩大到模块:

Interface
↓
Contract

再扩大到进程:

Interface
↓
AIDL Interface
↓
Binder

它们背后的思维方式始终是:

面向接口,隐藏实现。


四十四、但 Binder 和它们负责的事情不同

这里最后一定要区分。

Interface

负责:

定义能力。

Contract

负责:

定义模块之间的能力边界。

AIDL

负责:

描述跨进程接口,并生成大量 Binder 样板代码。

而:

Binder

才是真正:

把调用跨过 Linux Process Boundary 的 IPC 基础设施。

所以:

AIDL
≠
Binder

就像:

接口定义
≠
通信网络

那 Binder 更像什么?

Binder 就像中韩之间那套可靠的国际物流 + 通信体系。

代购负责:

把张三的需求整理好

Binder 负责:

把这份需求真正跨国送过去

韩国那边的接单窗口 Stub:

收到需求
↓
看懂需求
↓
交给真正商家处理

所以整个过程:

张三
↓
代购 Proxy
↓
【Binder 跨进程运输】
↓
接单窗口 Stub
↓
真正商家 Impl

现阶段直接记住:

Binder 本质 = Android 的跨进程 RPC 机制。

再说得更形象一点:

Binder 是“桥”,Proxy 和 Stub 是桥两边的翻译员,Impl 才是真正干活的人。


四十五、为什么 Binder 看起来复杂?

因为平时:

Interface
↓
Impl

中间什么都没有。

而跨进程:

Interface
↓
Proxy
↓
Parcel
↓
Binder
↓
Stub
↓
Impl

突然多出很多东西。

但是现在每一层为什么存在已经很清楚:

Proxy
=
让调用方继续像调用接口一样使用远端对象
Parcel
=
让参数和返回值能够被传输
Binder
=
真正跨越 Linux Process
Stub
=
把远程请求重新恢复成方法调用
AIDL
=
自动生成大量重复样板代码

所以 Binder 并不是:

“Android 故意搞得很复杂。”

而是:

跨进程以后,原来一个简单 Interface → Impl 调用所隐含的事情,都必须显式解决。


四十六、这一篇最核心的一张图

              调用方看到的世界

          userService.getUser(1)
                    │
                    ↓
                Interface
                    │
       ┌────────────┴────────────┐
       │                         │
     同进程                    跨进程
       │                         │
       ↓                         ↓
     Impl                      Proxy
                                 │
                                 ↓
                               Parcel
                                 │
                                 ↓
                               Binder
                                 │
                           Linux Process
                              Boundary
                                 │
                                 ↓
                               Stub
                                 │
                                 ↓
                               Impl

调用方:

永远只想看到 Interface。

至于实现:

就在本地

还是:

在另一个 Module

甚至:

在另一个 Linux Process

都可以隐藏在接口后面。


四十七、最后重新看一次 userService.getUser(1)

最开始:

userService.getUser(1)

代表:

直接调用 Impl

增加 Interface:

userService.getUser(1)

代表:

Interface
↓
Impl

模块化:

userService.getUser(1)

代表:

Contract
↓
Module Impl

跨进程:

userService.getUser(1)

代表:

AIDL Interface
↓
Proxy
↓
Binder
↓
Stub
↓
Remote Impl

表面:

这一行代码几乎没变。

下面:

已经从一个普通对象调用,演进成了跨 Linux Process 的 RPC。

这就是这篇真正想讲清楚的东西。


四十八、一句话总结

从:

Interface

到:

Contract

再到:

AIDL + Binder

并不是三套完全割裂的思想。

核心一直都是:

调用方依赖能力定义,而不是具体实现。

真正变化的是:

Interface
和
Impl

之间的距离越来越远。

从:

同一个类调用

到:

模块边界

再到:

Linux Process Boundary

于是中间才逐渐需要:

Proxy
Parcel
Binder
Stub

所以以后再看到:

Proxy
Stub
AIDL
Parcel
Binder

不要先把它们当成几个陌生的系统名词。

只需要先想:

Interface
↓
Impl

然后问一句:

如果 Impl 跑到另一个进程里了,我还想保持同样的接口调用方式,该怎么办?

Proxy、Stub、Parcel、Binder、AIDL:

就是这个问题一步一步推出来的答案。


下一篇:

《Android 系统层扫盲 08:Binder 到底怎么跨进程?从 PowerManager 一路跑到 system_server》

这一篇我们解决的是:

为什么跨进程以后会自然出现 Proxy、Stub、AIDL 和 IPC。

下一篇则正式解决:

Android 到底是怎么把这套设计真正实现出来的?

我们会沿着真实调用:

PowerManager
↓
IPowerManager.Proxy
↓
Parcel
↓
transact()
↓
Java Binder
↓
JNI
↓
Native Binder
↓
Linux Binder Driver
↓
system_server Binder Thread
↓
IPowerManager.Stub
↓
PowerManagerService

完整跑一次。

也就是说:

这一篇先理解设计思想,第八篇再理解 Binder 的真正实现链路。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/wulong756273/article/details/165240313

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--