详解跨进程通信AIDL原理和使用

推荐首先阅读:《跨进程通信破障一击》和《像创造者一样思考跨进程通信》两篇文章!!

——————————————————————————————————————————————————————

首先仔细回顾Binder实现跨进程通信核心原理,如下:
1 当应用的一个Binder服务在binder驱动中注册后,binder驱动中就有一个binder_node与之对应。
注意!!Binder对象创建后不是立马就在 Binder驱动中注册,真正“注册”发生在 这个 Binder服务 第一次被传给其他进程或被 ServiceManager.addService("xxx", binder)时 。不是 “创建即注册”,而是 “对外可见时注册”。

2 当客户端发送数据时候,binder驱动就会根据“进程+句柄handle(就是一个数字格式的索引号,客户端拿到的IBinder的实现类的底层实现中包含了这个索引号)找到binder_ref,通过binder_ref找到binder_node
注意!! 在请求发出去的那一刻,内核也记录了这次调用是哪一个线程、哪一块缓冲区(方法栈),本次binder服务请求的reply数据也会回到这里。

3 binder驱动根据binder_node找到对应服务端进程的binder_proc
binder_proc是进程在 Binder 驱动中身份证 + 资源管理中心”,它包含了:

  • 📌 这个进程的 Binder mmap 区域(如果不特殊设置大小就是1M)
  • 📌 所有 binder_node(本进程作为服务端暴露的 Binder)
  • 📌 所有 binder_ref(引用别人的 Binder)
  • 📌 Binder 线程列表
  • 📌 todo 队列(Binder 驱动中用于暂存“尚未被某个 Binder 线程处理的工作项)
  • 📌 buffer 分配器(它管理的是该进程的 mmap区域,当传输数据时候它会从这个区域切分出一个个binder_buffer,并且在使用结束回收。如果Binder mmap 区域是停车场,那么buffer 分配器则是停车场管理员,每次根据传递的数据大小分配匹配的车位)

4 尝试选择一个服务端 binder线程

  • 若服务端有空闲线程 → 继续, 若服务端没有线程 → 阻塞客户端

5 在服务端进程中分配 binder buffer

6 copy 数据到服务端的Binder buffer中

7 根据Binder buffer中的数据构建事务,挂载到某个 服务端binder线程 的事务队列中。(这里有点像,构建一个runnable的task,通过handler丢到某一个线程中)

其中的2~7都是binder驱动自动完成,我们利用Binder机制实现跨进程通信就只有两个关键步骤:

  1. 在服务端进程中构建一个Binder服务;
  2. 在其他应用进程中拿到该Binder服务的代理类IBinder,使用这个代理类向服务端发信息。
    怎么拿到呢,这里大家可以想想,方法有很多哦~

跨进程通信的接口转换、安全校验也都是在这个流程之上进行构建。

接下来开启正文

——————————————————————————————————————————————————————

AIDL是什么?

AIDL全称Android Interface Definition Language,是Android系统用于实现跨进程通信(IPC)的接口定义语言,在该语言的语法规范下,Android跨进程的接口需要定义在扩展名为.aidl的文件中。

采用AIDL语言定义接口后,Android的aidl (位于sdk/build-tool)工具会根据.aidl文件中的接口定义,按照 Binder 驱动要求的模板自动生成对应的 Java 接口文件,接下来不管是AIDL的客户端还是服务端都可以通过这个aidl生成的java接口文件和binder驱动进行通信,从而实现跨进程通信。

接下来,我们结合代码来具体分析

——————————————————————————————————————————————————————

完整工程代码见 https://github.com/kingkong-li/networklibhttps://github.com/kingkong-li/lifechange

首先我们创建两个AIDL接口文件

aidl文件目录

内容如下

package com.example.networklib;

// 1. 必须显式 import 另一个 AIDL 接口
import com.example.networklib.ICommonCallback;

interface ICommonInterface {

    void set(String data);

    String get();

    void registerCallback(in ICommonCallback callback);
}
package com.example.networklib;

/**
 *
 * 客户端通过服务端的注册接口把自己的Binder服务的代理Ibinder给到服务端,
 * 服务端拿到这个IBinder就可以向客户端随时发送数据,实现了双向通信
 * 这个场景下等于客户端也是运行了binder服务,也可以说是服务端
 */
interface ICommonCallback {

   oneway void onEvent(String code, String msg);
}

把这两个AIDL文件+目录完整的copy客户端App中

AIDL目录

编译程序,aidl工具就会把这两个用aidl语言定义的接口转换成Java文件,如下

AIDL编译后文件

接下来我们就看看数据是怎么从客户端发往服务端的:

package com.jingang.lifechange.aidl;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.TextView;

import com.example.networklib.ICommonCallback;
import com.example.networklib.ICommonInterface;
import com.jingang.lifechange.R;
import com.jingang.lifechange.base.BaseActivity;
import com.jingang.lifechange.utils.PublicThreadPools;

public class AidlClientActivity extends BaseActivity {

    private static final String TAG = "AidlClientActivity";
    private ICommonInterface mRemoteInterface;
    private TextView mTextView;

    private Handler UIHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_aidl_client);
        mTextView = findViewById(R.id.textView);
        UIHandler = new Handler(getMainLooper());
        connectRemoteService();
    }


    private void connectRemoteService() {
        Intent intent = new Intent("com.example.networklib.MyService");
        intent.setPackage("com.example.networklib");
        intent.putExtra("package", "com.jingang.lifechange");

        boolean success = this.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
        Log.v(TAG, "bindService success=" + success);

    }
    private final ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.v(TAG, "onServiceConnected");
            mRemoteInterface = ICommonInterface.Stub.asInterface(service);

            PublicThreadPools.getService().submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Log.v(TAG, "onServiceConnected run task");
                        mRemoteInterface.set("Client onServiceConnected");
                        mRemoteInterface.registerCallback(new ICommonCallback.Stub() {
                            @Override
                            public void onEvent(final String code, final String msg) throws RemoteException {
                                Log.v(TAG, "onEvent code=" + code + " msg=" + msg);
                                UIHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        mTextView.setText(String.format("%s:%s", code, msg));
                                    }
                                });

                            }
                        });
                    } catch (RemoteException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.v(TAG, "onServiceDisconnected");
            // 重连
            connectRemoteService();
        }
    };

    @Override
    protected void onStop() {
        super.onStop();
        PublicThreadPools.getService().submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mRemoteInterface.set("Client Activity onStop");
                } catch (RemoteException e) {
                    throw new RuntimeException(e);
                }
                mRemoteInterface = null;
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        this.unbindService(mConnection);
    }


}

如上代码通过客户端通过bindService方法获取到了服务端Binder的代理IBinder。我们可以看到服务端onBind返回的是一个Binder对象,经过内核传递就变成了一个BinderProxy,他们虽然不一样但是都是IBinder的实现类,都可以调用到Binder对象相关的方法(一个本地调用,一个通过Binder驱动远程调用)。

@Override
public IBinder onBind(Intent intent) {
    return  mBinder;
}

注意!!除了使用这个bindService来获取IBinder,还有多种方法来获取,如下所示:

传递媒介 典型场景 推荐指数
onBind (Service) 标准的 Service 绑定流程 ⭐⭐⭐
ContentProvider 同步获取接口,绕过异步等待 ⭐⭐⭐⭐⭐
Messenger 轻量级、基于消息的指令分发 ⭐⭐⭐
Bundle (Intent) Activity 跳转、广播发送临时回调 ⭐⭐
AIDL 方法参数 接口嵌套,例如注册监听器(Callback) ⭐⭐⭐⭐

2 在onServiceConnected成功后,我们就拿到了服务端Binder的代理类IBinder

接下来关键点就是如下的步骤把数据就发送到了服务端

mRemoteInterface = ICommonInterface.Stub.asInterface(service);
mRemoteInterface.set("Client onServiceConnected");

我们点击看看由aidl文件编译生成的ICommonInterface.java看看这两步到底是怎么做到的

 public static com.example.networklib.ICommonInterface asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      // 询问这个 Binder 对象:“你是不是就在我自己的进程里?”
      // 如果是同进程:直接强转对象调用方法即可,没必要走序列化的冤枉路(性能更高)。
      // 你在同一个 App 内部 bindService 时,服务端返回的 IBinder 就是你创建的那个 Stub 实例。
      // 因为 Stub 在构造函数中执行了 attachInterface(this, DESCRIPTOR)。 
      // 此时 queryLocalInterface 会直接返回这个 this(即服务端的原始对象)。
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof com.example.networklib.ICommonInterface))) {
        return ((com.example.networklib.ICommonInterface)iin);
      }
      return new com.example.networklib.ICommonInterface.Stub.Proxy(obj);
    }
private static final java.lang.String DESCRIPTOR = "com.example.networklib.ICommonInterface"; 
private static class Proxy implements com.example.networklib.ICommonInterface
    {
      private android.os.IBinder mRemote;
      Proxy(android.os.IBinder remote)
      {
        mRemote = remote;
      }
     
      @Override
      public void set(java.lang.String data) throws android.os.RemoteException
      {
       // 从Android 系统的 Parcel 池中取出两个对象,而不是每次都 new,这样可以提高性能。
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
         // 写入接口描述符(如 com.example.ICommonInterface)。
        // 服务端在接收时会检查这个 Token,防止数据发错给了不匹配的接口。
          _data.writeInterfaceToken(DESCRIPTOR);
        // 将你传入的 String 参数写入 _data 包。
          _data.writeString(data);
        // 这是一个阻塞式的同步调用,它会通过 Binder 驱动远程调用服务端进程执行代码,具体就是开篇讲的2~7步骤,
        // 最后把结果带回来。
        // 其中参数 TRANSACTION_set是一个整型常量(ID),告诉服务端“我要执行哪个方法”;
        // 参数0 表示这是一个双向调用。如果这个值是 IBinder.FLAG_ONEWAY (1),则表示“只管发,不看返回”,不会阻塞。
          boolean _status = mRemote.transact(Stub.TRANSACTION_set, _data, _reply, 0);
         // 若执行失败,且执行默认的本地方法 主要是做一些兼容处理 
          if (!_status && getDefaultImpl() != null) {
            getDefaultImpl().set(data);
            return;
          }
          // readException会检查 _reply 包里的头信息。如果服务端抛出了 NullPointerException 或 SecurityException,
          // 这个方法会在客户端重新抛出,这就是为什么你在客户端能看到服务端报错堆栈的原因。
          _reply.readException();
        }
        finally {
          // 资源回收
          _reply.recycle();
          _data.recycle();
        }
      }
}

接下来我们看服务端,客户端调用 mRemote.transact(Stub.TRANSACTION_set, _data, _reply, 0);发送数据,服务端就在Binder的onTransact中首先受到,具体处理如下

@Override 
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
      java.lang.String descriptor = DESCRIPTOR;
      switch (code)
      {
       // 当客户端调用getInterfaceDescriptor就是这个code
        case INTERFACE_TRANSACTION:
        {
          reply.writeString(descriptor);
          return true;
        }
        
        case TRANSACTION_set:
        {
         // 对应客户端 Proxy 端的 _data.writeInterfaceToken(DESCRIPTOR)。
         // 这是第一道防线,它从 data 包中读取一个字符串,并与服务端的 DESCRIPTOR(接口全类名)对比。
          // 如果两者不一致,会抛出 SecurityException。这能确保客户端发送的数据包确实是发给这个接口的。
          data.enforceInterface(descriptor);
         // 对应客户端 Proxy 端的 _data.writeString(data)。
         //从Parcel中按照顺序提取出字符串。因为参数是按顺序写入的,所以读取时也必须按顺序读取。
         // 如果有多个参数,你会看到 _arg0, _arg1 依次被读取。
          java.lang.String _arg0;
          _arg0 = data.readString();
          // 这行代码真正触发了你在 Service 中写的业务逻辑。
          // 这里的 this 指向的是你的 Stub 实现类对象。此时,代码从 Binder 驱动层正式回到了你的业务逻辑层。
          this.set(_arg0);
          // 告诉客户端:“我这边运行得很顺利,没有崩溃”,它会在 reply 数据包的头部写入一个代表“成功”的 0。
          // 如果你的业务代码抛出了异常(且是 Binder 支持的异常,如 NullPointerException),
          // 底层会捕获它并在 reply 中写入异常信息,而不再调用 writeNoException。
          reply.writeNoException();
          // 告诉内核驱动,这个 code(TRANSACTION_set)已经被正确识别并处理完毕。
          // 驱动随后会把 reply 包递回给客户端,对应mRemote.transact的返回值
          return true;
        }

可以看到这些自动生成的代码完成了数据传输和部分安全校验,并且把客户端的数据通过this.set(_arg0)给到我们自定义实现类

package com.example.networklib;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;

public class MyService extends Service {
    public static final String TAG = "MyService";
    private final Binder mBinder = new CustomBinderInterface();
    public MyService() {
        super();
        Log.d(TAG,"MyService");

    }

    @Override
    public void onCreate() {
        super.onCreate();
        PermissionChecker.INSTANCE.init( this.getApplicationContext());
    }


    @Override
    public IBinder onBind(Intent intent) {
        return  mBinder;
    }
    // 这个就是具体Binder服务实现类
    private static class CustomBinderInterface extends ICommonInterface.Stub{
 
        @Override
        public void set(String data) throws RemoteException {
            // 前文讲的this.set(_arg0) 就会调用到这个set方法
            Log.d(TAG,"set data="+data);
            DataController.INSTANCE.getWelComeData().postValue(data);

        }

        @Override
        public String get() throws RemoteException {
            Log.d(TAG,"get data");
            return DataController.INSTANCE.getWelComeData().getValue();
        }

        @Override
        public void registerCallback(ICommonCallback callback) throws RemoteException {
            Log.d(TAG,"registerCallback");
            DataController.INSTANCE.getIpcCallBackList().register(callback);

        }

        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            // 这里增加拦截
            int uid = Binder.getCallingUid();
            // 客户端校验
            if(!PermissionChecker.INSTANCE.checkPermission(uid)){
                Log.i(TAG,"checkPermission, result is false, do noting");
                return false;
            }

            return super.onTransact(code, data, reply, flags);
        }
    };


}
package com.example.networklib

import android.content.Context
import android.content.pm.PackageManager
import android.os.Process
import android.util.Log

object PermissionChecker {
    private const val TAG: String = "PermissionChecker"

    private const val CLIENT_PACKAGE_NAME = "com.jingang.lifechange"
    private var mPackageManager: PackageManager? = null
    fun init(context: Context) {
        mPackageManager = context.packageManager
    }

    fun checkPermission(uid: Int): Boolean {
        // 首次onTransact来自android.uid.system进程
        if (uid == Process.SYSTEM_UID) {
            Log.i(TAG, "checkPermission: uid == Process.SYSTEM_UID")
            return true
        }
        // 获取包名,校验报名是否一致,还可以进一步根据报名获取签名信息来校验签名实现更严格的安全检查
        val packageName: String? = mPackageManager?.getNameForUid(uid)
        Log.i(TAG, "packageName:$packageName")
        return CLIENT_PACKAGE_NAME == packageName
    }
}

至此,完成了一次从客户端到服务端的数据传递,可以看到我们的代码只有AidlClientActivity 和MyService ,其他都是AIDL帮我们自动生成的。

接下来,我们讲讲AIDL和Binder通信中常见问题

1、从客户端来看Binder 是同步调用。

如果在客户端UI线程发起同步 Binder 调用,就会阻塞 UI;因此调用方通常需要在后台线程发起 IPC,而 服务端 也需要有 Binder 线程池来接收并处理请求。

比如上面代码** ** mRemoteInterface.set("Client onServiceConnected");就会阻塞当前线程会服务端返回 或发生异常 / 超时 / 死亡。

所以我们上文中的 mRemoteInterface.set("Client onServiceConnected");若放在UI线程调用是有问题的,但是一般请情况又没有出问题是因为Binder远程调用是也是很快的。

2、Binder缓冲区是进程维度的,如果不特殊设置就是一个进程总共1M。

就是说在一个服务端进程不管你有多少个Binder服务,多少个binder线程,多少个正在处理的事务,他们通信所占用的Binder驱动中的使用的那个共享内存就总共只有1M。

这意味着:

  1. 大数据会挤占整个进程的 IPC 能力
  2. 一个慢事务会阻塞其他事务
  3. Buffer 不够 → 直接失败

Android 官方的潜台词:

Binder 是 IPC 控制通道,不是数据通道

如果想跨进程中传递大数据可以使用ContentProvider或者共享内存。

3 oneway关键字用于标记一个方法是单向、异步调用,即客户端发送请求后不会等待服务器返回结果,从而提高了通信效率,常用于耗时操作或通知,但不能用于需要返回值的场景。这能避免主线程被阻塞,但服务器端也要注意线程安全。

oneway的作用和特点:

  • 异步执行:调用oneway方法时,客户端不会停顿,会立即返回,请求被发送到Binder线程池处理.
  • 无返回值oneway方法不能有返回值,也不能是void类型(因为void也算一种结果),它只负责通知服务器端执行操作.
  • 效率提升:对于不关心即时结果的通知(如播放/停止音乐、发送日志),使用oneway能显著提升IPC性能.
  • 线程管理:多个oneway调用发送到同一个Binder的同一线程时会按顺序执行,但在不同线程中执行可能并发.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容