Android进程间通信-Binder实战

记录通过binder通信+小米push使natave具有push能力的完整过程

Posted by Nela on July 7, 2021

Android进程间通信-Binder实战

简介

本文介绍不采用AIDL模版,基于binder的进程间通信方式。

首先介绍了Binder通信的基本原理,然后通过需求切入,提供了两种通信方案(即java层注册service和c层注册service)然后根据实际情况,实现方案。并记录了实现过程中出现的问题和解决思路。

AIDL做了什么?

封装了Stub类和子类proxy

1.其中的stub类封装了binder远端ontransact的调用过程,stub子类proxy封装了客户端拿到远端binder进行transact的调用过程(比如transact参数组装等)

  • Stub类的asInterface做了什么?

1.asInterface(android.os.IBinder obj) 用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的 就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象。

  • Stub类的onTransact做了什么?

Stub就是一个Binder类,服务端会实现此类中的接口,当发生RPC调用时,会走onTransact过程,然后调用服务端实现的接口。

Binder基本概念

Binder通讯数据复制一次的最本质原因:

得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。

内存映射: Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

sercice注册服务-通过binder对象连接-servicemager和用户空间 并做内存映射。以达到让用户空间访问servcie服务的方法。

小结: 理清思路,即service端产生Binder服务重写ontransact。通过serviceManager注册至系统内核. client端通过serviceManager获取Binder服务,调用Transact完成通信。

需求

实现如下需求:

linux进程服务想要获得推送功能。但c层无法实现。实现思路是通过安装apk并通过三方平台注册push功能。然后在系统开启时使apk变为常驻服务。当apk收到推送消息时,能通过apk所在进程将将消息传递给linux服务进程。

那么实现进程间通信的方式就选择了上述所说的Binder机制。

实现前首先考虑以下几个问题:

  1. linux进程死亡Binder失效,如何回调通知apk?
  2. Binder机制是不是只能单向通信,即linux只能调apk方法?如果是单向通信该如何解决。

实现思路

由于不知道如上两个问题如何解决。不知道是否是单向通信,那么我们确保通信的稳定。则要按照如下步骤进行实现。

  • 方案一 c层实现service,java层实现client

  • 方案二 java实现service,c层实现client

具体实现

方案一

第一步. 服务端生成Binder:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
android::status_t CJuBinder::onTransact( uint32_t code, const android::Parcel& data, android::Parcel* reply, uint32_t flags)
{
    Mtc_AnyLogInfoStr((ZCHAR*)"CJuBinder", (ZCHAR*)"CJuBinder::onTransact code:%d", code);
    switch (code)
    {
        case 1:
            {   
                peerCb = data.readStrongBinder();
                NotifyPeer(1);
            }
            break;
        case 2:
            /* 收到push,唤醒终端 */
            MyProcNotify(MtcActionWakeupThread, 0, NULL, 0);
            break;
        default:
            return android::BBinder::onTransact(code, data, reply, flags);
    }

    return 0;
}

代码讲解: onTansact为服务端需要实现的方法,其中code是service和client约定好方法标识,data为客户端传递参数。reply为返回值。

第二步. 注册binder服务至系统内核

1
2
3
4
5
6
7
8
9
10
11
int CJuBinder::RegisterService()
{
    android::sp<android::IServiceManager> sm = android::defaultServiceManager();
    android::status_t ret;
    ret = sm->addService(mydescriptor, this);
    Mtc_AnyLogInfoStr((ZCHAR*)"CJuBinder", (ZCHAR*)"OnBinderLoop addService ret:%d", ret);
    //call binder thread pool to start
    android::ProcessState::self()->startThreadPool();
    android::IPCThreadState::self()->joinThreadPool(true);
}

代码讲解: addservice方法中的mydescriptor 是service名字,client端也是通过这个名字查找binder的

特别注意上述代码均是由c层同事编写。可能粘贴有不准确或遗漏的部分。可参考此网站: https://blog.csdn.net/ganyue803/article/details/41315733

第三步.客户端获取Ibinder对象

问题: android并没有serviceManager这个类。那么我们通过反射获取serviceManager对象并调用getService方法获取IBinder.

1
2
3
4
5
6
7
8
9
10
11
 private static IBinder getRemoteBinder() {
        try {
            Class serviceManager = Class.forName("android.os.ServiceManager");
            Method method = serviceManager.getMethod("getService", String.class);
            mRemoteBinder = (IBinder) method.invoke(serviceManager.newInstance(), "yourdescriptor");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        .... 省略部分代码
        return null;
    }

第四步.调用方法

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
public boolean sendTokenAndPackageName(Context context, String token) {
        Log.d(TAG, "enforceInterface");
        mContext = context;
        mToken = token;
        JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("token", token);
            jsonObject.put("packName", context.getPackageName());
            jsonObject.put("through", 1);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        String pushInfo = jsonObject.toString();
        Log.d(TAG, "token " + jsonObject.toString());
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        _data.writeString(pushInfo);
        _data.writeInterfaceToken("nela.AndroidPush");
        boolean result = false;
        try {
            result = mRemoteBinder.transact(1, _data, _reply, 0);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        _reply.readException();
        _reply.readInt();
        return result;
    }

代码讲解: 调用c层提供给我们的方法,code为int型,我们定义方法为1。 这里我们参数传递了string类型字符串,由于要传递的string很多,那么我们通过定义json数据格式传递。

注意:_data.writeInterfaceToken() 这个方法用于标识的打包对象,和_data.enforceInterface()方法为一对,也可以不填加此标识。

遇到的问题: 上述代码传递字符串发生了c层获取字符串乱码的问题,c层通过getCstring.获取不到或者不对。可以用data.size 判断java层是否将数据传递至c层。每次尝试时,记得将apk卸载重试。原因不详。

小结: 调试结果,c层可以拿到参数并调用相应方法。至此c注册服务-android获取并调用的流程就已经简单打通。

但是为了解决最开始的两个问题,那么java层也要注册服务,c层获取并调用。

方案二

JAVA层实现service

第一步.实现Binder,继承binder对象

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
public class LocalBinder extends Binder implements IPushInterface {

    public static String TAG = "LocalBinder";

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            case 1:
                data.enforceInterface("Juphoon.AndroidPush");
                sendTokenAndPackageName();
                Log.d(TAG, "onTransact getTokenAndKey");
                break;
        }
        return super.onTransact(code, data, reply, flags);
    }

    @Override
    public void sendTokenAndPackageName() {
        Log.d(TAG, "LocalBinder" + "sendTokenAndPackageName");
        BinderManager binderManager=BinderManager.getInstance();
        binderManager.sendTokenAndPackageName(context, token);
    }

    @Override
    public IBinder asBinder() {
        return this;
    }
}

实现IInterface接口

1
2
3
4
5
6
7
public interface IPushInterface extends IInterface{

    public void sendTokenAndPackageName();

}

代码讲解: binder对象主要是重写onTransact 方法。IInterface 接口重写 asBinder()。这两个方法是最重要的。也是android中AIDL实现的基本原理。在AIDL自动构建好的文件中上述这个LocalBinder被叫做stub.

attachInterface也是接口标示,而queryLocalInterface则和我们后面说到的传递Ibinder对象有关,会判断是否是本地binder.

第二步. 注册Binder

binder创建好后则需要通过ServiceManager注册至系统内核,同理也是通过反射调用addService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   try {
            Class serviceManager = Class.forName("android.os.ServiceManager");
            Method method = serviceManager.getMethod("addService", String.class,IBinder.class);
            LocalBinder localBinder=new LocalBinder();
            method.invoke(serviceManager.newInstance(),"Myservice",localBinder.asBinder());

        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

调用结果: 系统报出异常 InvocationTargetException 查阅资料是内部异常,最终堆栈信息打印的是java.lang.SecurityException。

问题: java.lang.SecurityException异常处理

查看serviceManager类发现: 并不是所有的注册请求都会得到响应,当发出请求的进程具有相应的权限时,才会做出相应操作。权限可以分为以下2种:

  • 特权。具有root和system权限的进程可以注册任何服务
  • 普通权利。对于一般进程,需要查看该进程是否有权限注册它所请求的Service。每个Service对应有一个uid表

参考: https://blog.csdn.net/wlwl0071986/article/details/50977573

解决方案:

  • 将apkpush至系统system/priv-app 使其变成系统apk,再次尝试
  • 删除旧的系统apk 需要权限,需执行如下代码获取读写权限 mount -o remount rw /system

具体过程 参考如下文章: https://blog.csdn.net/starhosea/article/details/78697007 最后仍然报出如上异常。

  • 解决方法2 第一步:在AndroidManifest.xml中,添加 android:sharedUserId=”android.uid.system” 第二步:使用系统签名后再安装即可。此方法未实验

总结:  Android 层可能无权限,将binder注册通过serviceManager注册至系统内核,那么如何确保通信的稳定和双向通信的问题呢?

Binder死亡代理

通过IBinder提供的api:linkToDeath/unlinkToDeath

1
2
3
4
5
6
7
8
9
10
11
12
mRemoteBinder.linkToDeath(mdeathRecipient, 0);
 
IBinder.DeathRecipient mdeathRecipient = new IBinder.DeathRecipient() {

        @Override
        public void binderDied() {
            mRemoteBinder.unlinkToDeath(mdeathRecipient, 0);
            Log.d(TAG, "mdeathRecipient");
            mRemoteBinder = null;
            mRemoteBinder = getRemoteBinder();
        }
    };

当服务进程死掉时会回调binderDied;

测试方法:

1
2
3
adb shell
ps | gerp "进程名"
kill 进程id

双向通信

通过Binder类发现 client 通过transact方法传递的参数data,为Parcel类 将数据打包发送和序列化相关。

其中writeStrongBinder() 允许传递一个Binder,

service通过readStringBinder()即可获取。并调用传递binder定义的接口方法完成回调。

1
_data.writeStrongBinder(localBinder.asBinder());

问题:传入binder获取不到.

解决:要调用asBinder 将自定义的binder传递进去,强转或者new 对象都无效!!

那么我们这里就把上述的本来要在java层要注册的Binder对象直接专递进去,即完成了双向通信。

延伸问题

AIDL 为什么能注册 ?

因为它底层通过service-》activityServiceManager注册至系统内核 activityServiceManager是系统内核控制的。

代码地址

https://github.com/cuizehui/PushAPK-Binder

参考文章

https://blog.csdn.net/carson_ho/article/details/73560642

https://www.cnblogs.com/hpboy/archive/2012/07/12/2587797.html

https://www.cnblogs.com/zhangxinyan/p/3487866.html

https://blog.csdn.net/ganyue803/article/details/41315733

https://toutiao.io/posts/wlalk9/preview