操作系统在运行程序时,会将程序分成一个个进程,每一个进程都被通过自己持有的页表通过映射的关系分配了虚拟地址,进程中指针的值是虚拟地址而非物理地址,所以指针只能在本进程使用,无法通过指针进行跨进程内存读写。

        而在Android 中,有着一种类,Parcel,它是一个高效的序列化容器,主要用于进程间通信(IPC)。

typedef int32_t status_t;//程序的返回结果,如NO_MEMORY,NO_ERROR等宏,本质是int32_t
status_t Parcel::writeAligned(const void* data, size_t size) {
    void* dest = reallocAndGetData(mDataPos, size);
    if (dest == nullptr) return NO_MEMORY;
    memcpy(dest, data, size);
    mDataPos += size;
    return NO_ERROR;
}

status_t Parcel::readAligned(void* data, size_t size) const {
    if (mDataPos + size > mDataSize) return NOT_ENOUGH_DATA;
    memcpy(data, mData + mDataPos, size);
    mDataPos += size;
    return NO_ERROR;
}

Parcel只是以特定的数据结构将数据存储到了某个内存空间,我们顶多从他那里拿到数据的指针,那具体的跨进程数据传递是咋实现的呢?
        操作系统允许多个进程的页表项映射向同一个物理内存块,其本身的内核空间也有着一个映射表映射物理内存,为了实现进程间的数据共享,接收进程会事先与内核空间映射向同一个物理内存块,当发送进程发送数据时,内核空间将数据拷贝到与接收进程共享的物理内存块,在将接收进程的页表项也一并修改以适应接收进程的虚拟内存。
 

┌─────────────────────────────────────────────────────────────────────────────────┐
│                         页表与物理内存映射关系图                                │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│  发送进程页表(App A)       内核页表(全局)         接收进程页表(App B)    │
│  ┌────────────────────┐      ┌────────────────┐      ┌────────────────────┐    │
│  │ 虚拟地址  物理页框  │      │ 虚拟地址  物理页框│      │ 虚拟地址  物理页框  │    │
│  ├────────────────────┤      ├────────────────┤      ├────────────────────┤    │
│  │ 0x7A000000 → P_send│      │ 0xFFFF8000... │      │ 0x6A000000 → P_new │    │
│  │ 0x7A001000 → P_x   │      │   → P_send    │      │ 0x6A001000 → P_y   │    │
│  │ ...                │      │ 0xFFFF8000... │      │ ...                │    │
│  │                    │      │   → P_new     │      │                    │    │
│  │                    │      │ 0xFFFF8000... │      │                    │    │
│  │                    │      │   → P_z       │      │                    │    │
│  └────────────────────┘      └────────────────┘      └────────────────────┘    │
│      
└─────────────────────────────────────────────────────────────────────────────────┘
1.CPU(Binder 驱动):将P_send 拷贝到 P_new 
2.Binder 驱动:修改接收进程页表(App B)虚拟地址 0x6A000000 由 nullptr  →  物理页框 P_new

Binder在这个过程中发挥了重要的作用,具体过程的源代码应该十分复杂,我们来看看他的java层开放的接口。

public interface IBinder {
    // 添加冻结状态变化回调
    default void addFrozenStateChangeCallback(@NonNull Executor executor, @NonNull FrozenStateChangeCallback callback) throws RemoteException {
        throw new RuntimeException("Stub!");
    }
    // 同步dump调试信息到文件描述符
    void dump(@NonNull FileDescriptor var1, @Nullable String[] var2) throws RemoteException;
    // 异步dump
    void dumpAsync(@NonNull FileDescriptor var1, @Nullable String[] var2) throws RemoteException;
    // 获取Binder接口描述符(如"android.os.IServiceManager")
    @Nullable
    String getInterfaceDescriptor() throws RemoteException;
    // 获取建议的最大IPC传输字节数(通常1MB)
    static int getSuggestedMaxIpcSizeBytes() {
        throw new RuntimeException("Stub!");
    }
    // 检查Binder服务端是否存活
    boolean isBinderAlive();
    // 注册死亡通知(服务端死后回调)
    void linkToDeath(@NonNull DeathRecipient var1, int var2) throws RemoteException;
    // Ping服务端是否响应
    boolean pingBinder();
    // 查询本地接口(如果同一进程则返回真实对象)
    @Nullable
    IInterface queryLocalInterface(@NonNull String var1);
    // 移除冻结状态回调
    default boolean removeFrozenStateChangeCallback(@NonNull FrozenStateChangeCallback callback) {
        throw new RuntimeException("Stub!");
    }
    // 核心方法:执行跨进程事务(发送Parcel数据)
    boolean transact(int var1, @NonNull Parcel var2, @Nullable Parcel var3, int var4) throws RemoteException;
    // 注销死亡通知
    boolean unlinkToDeath(@NonNull DeathRecipient var1, int var2);
    // 死亡回调接口
    public interface DeathRecipient {
        void binderDied();                    // 服务端死亡回调(无参)
        default void binderDied(@NonNull IBinder who) { // 带服务端引用的死亡回调
            throw new RuntimeException("Stub!");
        }
    }
    // 冻结状态回调
    public interface FrozenStateChangeCallback {
        int STATE_FROZEN = 0;                  // 冻结状态
        int STATE_UNFROZEN = 1;                // 解冻状态
        void onFrozenStateChanged(@NonNull IBinder var1, int var2); // 状态变化回调
    }
}

其中关于使用parcel的关键函数是:

 // 核心方法:执行跨进程事务(发送Parcel数据)
    boolean transact(int var1/*事务码*/,
                      @NonNull Parcel var2/*发送给服务端的请求数据*/,
                      @Nullable Parcel var3/*服务端返回的响应数据*/, 
                      int var4/*控制标志*/) throws RemoteException;

但在实际项目中,我们不会粗暴的使用这么底层的东西,我们会使用aidl.exe这么一个工具来自动处理这一切,而他会自动生成如下的.java文件,比手写舒服多了,我们只需要关心它传递过来的对象咋用就行了。
 

public interface XXXService extends android.os.IInterface{
    @Override public android.os.IBinder asBinder()
    {
      return this;
    }
    
     @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        try {
            _data.writeInt(a);
            _data.writeInt(b);
            mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
            return _reply.readInt();
        } finally {
            _data.recycle();
            _reply.recycle();
        }
    }
}

其中的IInterface类到底是啥接口呢?我们来看看他的接口。
 

package android.os;

public interface IInterface {
    IBinder asBinder();
}

可以看到它不过是对IBinder进行了业务的封装。他的本质还是通过Binder进行跨进程通信的。那具体的Binder该咋用呢?

interface IMusicService {
    fun play()
    fun pause()
    fun getCurrentSong(): String
}

class MusicService : Service() {
    private var currentSong = "No song"

    // 创建 Binder 实例,实现接口
    private val binder = object : Binder(), IMusicService {
        override fun play() {
            currentSong = "Playing: Yesterday"
            Log.d("MusicService", "play")
        }
        override fun pause() = Log.d("MusicService", "pause")
        override fun getCurrentSong() = currentSong
    }

    override fun onBind(intent: Intent?) = binder//关键步骤,将binder给对方
}

上边的代码不是跨进程的,onBind() 返回的 Binder 对象,就是客户端和服务端之间的通信通道,Binder 调度极其简单,说白了就是 直接把对象指针传过去,然后调方法。客户端拿到它,就能直接调用服务端的方法,跨进程需要用到上面说的接口。

android其实还提供了一种工具,Parcelable。当你在翻看android原生的Surface的源代码时你会发现:

package android.os;

public interface Parcelable {
    int CONTENTS_FILE_DESCRIPTOR = 1; // 常量:表示包含文件描述符
    int PARCELABLE_WRITE_RETURN_VALUE = 1; // 常量:表示是跨进程返回值(系统用)

    int describeContents(); // 返回内容类型:0(无特殊)或 CONTENTS_FILE_DESCRIPTOR(有FD)

    void writeToParcel(Parcel dest, int flags); // 序列化:对象 → Parcel,写入顺序必须和读取一致

    interface Creator<T> {
        T createFromParcel(Parcel source); // 反序列化:Parcel → 对象,读取顺序必须和写入一致
        T[] newArray(int size); // 创建对象数组
    }

    interface ClassLoaderCreator<T> extends Creator<T> {
        T createFromParcel(Parcel source, ClassLoader loader); // 反序列化:支持自定义ClassLoader(插件化/热修复用)
    }
}
package android.view;

public class Surface implements Parcelable {
......
// 序列化:把 Surface 句柄写入 Parcel(不是拷贝像素数据!)
public void writeToParcel(Parcel dest, int flags);

// 反序列化:从 Parcel 重建 Surface 对象(拿到同一块缓冲区的引用)
public static final Creator<Surface> CREATOR;

// 辅助反序列化(从 Parcel 读取数据填充当前对象)
public void readFromParcel(Parcel source);
......
}

Parcelable 本质是一套对象↔字节流的序列化协议,它定义了对象如何被拍平成 Parcel 能读懂的内存数据,以及如何从 Parcel 的内存数据中重建对象。
Surface 的源码实现了 Parcelable,但它的 writeToParcel() 不是拷贝图像数据,而是通过 Binder 传递一个图形缓冲区的句柄,具体原理我也不太清楚,毕竟想知道底层需要阅读很多cpp代码,工作量巨大,反正知道他跨进程时传递句柄就够了,不需要拷贝。

那这个跨进程的数据传递有啥应用呢,在Android项目中,我们可以利用这一能力构建CS架构的软件集群。C指客户端,S指服务端,比如说可以先构建一个3D游戏引擎,他的功能主要是负责运算图像,然后搭配一群不同游戏的客户端。为啥能这么做呢,因为当Client通过Binder把Surface传给Server时,内核传递的只是一个整数值。这个整数值在Server进程里会被映射到同一个物理内存页

+-------------------+      +----------------------+
|   GPU渲染引擎      |      |  显示控制器 (DPU)     |
|  (写入像素)        | ---> |  (读取像素)           |
+-------------------+      +----------------------+
         |                            |
         v                            v
  +------------------------------------------------+
  |       共享物理内存页 (Frame Buffer)              |
  |  [R][G][B][A] [R][G][B][A] ... (1920x1080)   |
  +------------------------------------------------+
                ^                          ^
                |                          |
        +-------+----------+    +----------+--------+
        | 游戏客户端A      |    | 游戏客户端B       |
        | (映射到自身虚拟地址)|    | (映射到自身虚拟地址)|
        +------------------+    +-------------------+

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐