Android跨进程通信-IPC初探(三) - 使用AIDL

IPC初探(三) - 使用AIDL


本文将会介绍AIDL通信。

本系列的其它文章:

Android跨进程通信-IPC初探(一)

Android跨进程通信-IPC初探(二) - 使用Messenger


1. 示例的新需求

  1. 回顾一下在IPC初探(一)中的示例,服务端为客户端提供了两个功能:

    1. addStudent() : 在客户端为服务端的List添加新的数据
    2. getStudentList() :在客户端获取服务端的List数据。
  2. 现在我们提出了新需求:

    • 问题:当客户端调用addStudent() 添加一条新的数据后,需要再次查询才能获取最新的数据。这意味着客户端必须主动向服务端发起查询,才能确定什么时候List更新了数据。
    • 新需求:现在我们要改变这个流程,当服务端添加了新的数据时,会主动通知客户端。这就是观察者模式。
    • 解决方案:常用的回调接口方式在这里行不通—-因为这是跨进程,需要提供AIDL接口,来达到跨进程的目的。
  3. 设计解决方案

    • 设计一个回调接口IOnNewStudentAddedListener,用于服务端通知客户端。由于跨进程,这个接口使用AIDL,由系统自动生成。具体实现由客户端提供,而服务端在完成Student添加后,将会调用这个接口。
    • IStudentManager添加注册和注销方法。这两个方法在由服务端实现,提供给客户端调用。
    • 服务端需要提供注册和注销方法的具体实现,客户端获取到远程binder后,通过binder(在合适的地方)调用这两个方法。
    • 客户端需要提供IOnNewStudentAddedListener的具体实现,并通过注册/注销方法,通知服务端添加/删除这个接口对象。
    • 为了做模拟测试,我们在服务端添加了一个线程,它每5秒会添加一个新的Student,并通知客户端。
  4. 实现:

    1. 回调接口IOnNewStudentAddedListener

      //IOnNewStudentAddedListener.aidl
      package com.dou.ipcsimple;
      import com.dou.ipcsimple.Student;
      
      interface IOnNewStudentAddedListener {
          void onNewStudentAdded(in Student student);
      }
    2. IStudentManager添加两个新的方法,用于注册和注销观察者。

      package com.dou.ipcsimple;
      import com.dou.ipcsimple.Student;
      import com.dou.ipcsimple.OnNewStudentAddedListener;
      interface IStudentManager {
          void addStudent(in Student student);
          List<Student> getStudentList();
          //新增的接口,用于注册和注销观察者
          void registerListener(OnNewStudentAddedListener listener);
          void unregisterListener(OnNewStudentAddedListener listener);
      }
      
    3. 服务端StudentManagerService 修改:

      • 在Binder中,提供注册/注销两个方法的具体实现:

        private CopyOnWriteArrayList<IOnStudentAddedListener> mListeners = new CopyOnWriteArrayList<>();
            private Binder binder = new IStudentManager.Stub() {
        
                @Override
                public void addStudent(Student student) throws RemoteException {
                    students.add(student);
                }
        
                @Override
                public List<Student> getStudentList() throws RemoteException {
                    return students;
                }
                //注册
                @Override
                public void registerListener(IOnStudentAddedListener listener) throws RemoteException {
                    if (mListeners.contains(listener)) {
                        mListeners.add(listener);
                    } else {
                        Log.d(TAG,"already exists.");
                    }
                    Log.d(TAG,"registerListener size:"+ mListeners.size());
                }
                //注销
                @Override
                public void unregisterListener(IOnStudentAddedListener listener) throws RemoteException {
                    if (mListeners.contains(listener)) {
                        mListeners.remove(listener);
                        Log.d(TAG,"unregister listener succeed.");
                    } else {
                        Log.w(TAG,"Warning:listener not found, cannot unregister.");
                    }
                    Log.d(TAG,"unregisterListener size:"+ mListeners.size());
                }
            };
      • 此外,我们做一个模拟测试,在服务端每5秒添加一个新的Student:

            private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false);
            private class AddStudentWork implements Runnable {
                @Override
                public void run() {
                    while (!mIsServiceDestroyed.get()){
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        int studentID = students.size() + 1;
                        Student student = new Student(studentID, "newStu_" + studentID);
                        try {
                            noticeStudentAdded(student);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            //挨个儿通知各位观察者
            private void noticeStudentAdded(Student student) throws RemoteException {
                students.add(student);
                Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + mListeners.size());
                for (int i = 0; i < mListeners.size(); i++) {
                    IOnStudentAddedListener listener = mListeners.get(i);
                    listener.onStudentAdded(student);
                }
            }
        
            @Override
            public void onDestroy() {
                mIsServiceDestroyed.set(true);
                super.onDestroy();
            }
    4. 客户端修改:

      • IOnNewStudentAddedListener的具体实现,以及这个接口对象的创建:

            //这里提供了IOnNewStudentAddedListener的匿名实现,这个mOnStudentAdded将作为接口对象注册/注销到远程服务端。
            private IOnStudentAddedListener mOnStudentAdded = new IOnStudentAddedListener.Stub() {
        
                @Override
                public void onStudentAdded(Student student) throws RemoteException {
                    //这里我们给handler发送消息,后续操作将由handler处理。
                    handler.obtainMessage(MESSAGE_NEW_STUDENT_ADDED, student).sendToTarget();
                }
            };
            //在handler中处理消息,可以更新UI线程。
            private Handler handler = new Handler(){
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case MESSAGE_NEW_STUDENT_ADDED:
                            Log.d(TAG, "receive new student:" + msg.obj);
                            break;
                        default:
                            super.handleMessage(msg);
                    }
                }
            };
        
      • 在合适的地方注册/注销 mOnStudentAdded:

            private IStudentManager remoteStudentManager;
        
            private ServiceConnection mServiceConnection = new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    bindService = true;
                    IStudentManager studentManager = IStudentManager.Stub.asInterface(service);
                    try {
                        ...
                        ...
                        //注册
                        studentManager.registerListener(mOnStudentAdded);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
        
                @Override
                public void onServiceDisconnected(ComponentName name) {
                    remoteStudentManager = null;
                    Log.d(TAG, "binder died.");
                }
            };
        
            @Override
            protected void onDestroy() {
                if (remoteStudentManager != null && remoteStudentManager.asBinder().isBinderAlive()) {
                    Log.i(TAG, "unregister listener:" + mOnStudentAdded);
                    try {
                       //注销
                       remoteStudentManager.unregisterListener(mOnStudentAdded);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                if (bindService){
                    unbindService(mServiceConnection);
                }
                super.onDestroy();
            }
        
  5. 完整的代码可以从Github获取IPCSimple

2. 添加回调接口之后的运行结果分析

  1. 运行测试:

    • 客户端:能观察到正常的运行结果:

      04-23 09:03:14.146 21025-21025/com.dou.ipcsimple D/IPCSimple: Client Request students:[[stuId:1001, name:Tom], [stuId:1002, name:Jerry]]

      04-23 09:03:14.149 21025-21025/com.dou.ipcsimple D/IPCSimple: Client Request students:[[stuId:1001, name:Tom], [stuId:1002, name:Jerry], [stuId:1003, name:Jack]]

      04-23 09:03:19.141 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:4, name:newStu_4]

      04-23 09:03:24.143 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:5, name:newStu_5]

      04-23 09:03:29.146 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:6, name:newStu_6]

      04-23 09:03:34.149 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:7, name:newStu_7]

    • 服务端:能正常接受注册。但当我们点击后退键,退出这个Activity时,onDestroy 调用的注销方法出了BUG:

      04-23 09:05:44.276 22645-22662/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:1

      04-23 09:05:45.646 22645-22658/com.dou.ipcsimple:remote W/StudentService: Warning:listener not found, cannot unregister.

      04-23 09:05:45.647 22645-22658/com.dou.ipcsimple:remote D/StudentService: unregisterListener size:1

  2. 注销失败:反序列化—-每一个生成的对象都是崭新的。

    • 原因探究:显然mListeners.contains(listener) 返回了false,注销的listener与注册的listener并不是同一个对象。
      • 这里通过binder传递对象,而这个传递的本质是序列化与反序列化。所以,对客户端的listener分别通过注册和注销传递给服务端,反序列化得到的两个listener,是两个不同的对象—-因为每次序列化都会创建一个独立的对象。
    • 解决方案:使用RemoteCallbackList.

      • RemoteCallbackList 内部使用ArrayMap<IBinder,Callback>来保存所有的AIDL回调。对于本例中的3个listener对象(客户端1个,服务端2个),它们的低层Binder都是同一个。利用RemoteCallbackList 这个特性,就可以修复上面注销的BUG。
      • 使用RemoteCallbackList 替代CopyOnWriteArrayList<IOnStudentAddedListener>, 然后修改注册/注销方法的实现:

        private RemoteCallbackList<IOnStudentAddedListener> mListeners = new RemoteCallbackList<>();
        // 注册
        mListeners.register(listener);
        // 注销
        mListeners.unregister(listener);

      以及,修改通知方法(请注意遍历方式):

      ```
      private void noticeStudentAdded(Student student) throws RemoteException {
              students.add(student);
              /** 旧版本
              Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + mListeners.size());
              for (int i = 0; i < mListeners.size(); i++) {
                  IOnStudentAddedListener listener = mListeners.get(i);
                  listener.onStudentAdded(student);
              }
               //*/
              //RemoteCallbackList not-is a List...
              final int COUNT = mListeners.beginBroadcast();
              Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + COUNT);
              for (int i = 0; i < COUNT; i++) {
                  IOnStudentAddedListener listener = mListeners.getBroadcastItem(i);
                  if (null != listener) {
                      listener.onStudentAdded(student);
                  }
              }
              mListeners.finishBroadcast();
      
          }
      ```
      

      好奇的看了一下getBroadcastItem(),内部用数组来支持这种索引遍历方式:

      ```
      public E getBroadcastItem(int index) {
              return ((Callback)mActiveBroadcast[index]).mCallback;
          }
      ```
      
    • 修复之后的运行结果:当退出Activity时,注销成功:

      04-23 10:17:46.187 14105-14130/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:1

      04-23 10:17:47.369 14105-14105/com.dou.ipcsimple:remote W/StudentService: Service onDestroy

      04-23 10:17:51.189 14105-14130/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:0

    • 所有的修改都可以在Github的完整示例代码中找打。被注释的则是旧版本代码,也就是导致注销失败的代码。

3. 回顾Binder工作流程

  1. 我们再来看看Binder工作图解:
    image

    • Client在发起远程请求之后,会被挂起,此时如果服务端方法执行耗时过多,就会导致Client线程阻塞。如果这个Client线程是UI线程,就可能会导致ANR。
    • 我们可以在Service的getStudentList 方法中添加延时,模拟耗时过长的方法:

      public List<Student> getStudentList() throws RemoteException {
                          SystemClock.sleep(5000);
                  return students;
              }

      然后运行,发现ANR:
      这里写图片描述

    • onServiceConnectedonServiceDisconnected 也是运行在UI线程中,所以也可能会导致ANR。上面的运行结果就是证明。

  2. 解决阻塞/ANR:

    • 当客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中。同理,当远程服务端调用客户端的方法中,被调用的方法运行在客户端的Binder线程池中。
    • 当服务端的某个方法是长耗时的,那么客户端就不要在UI线程里来调用它。当然在onServiceConnectedonServiceDisconnected 方法中也不要调用这个长耗时方法。

      • 应当把长耗时方法的调用放在非UI线程中。比如另起一个Thread来运行。
    • 同理,我们也不可以在服务端主线程中调用客户端的耗时方法。例如,服务端的noticeStudentAdded 会调用客户端的回调接口方法 onStudentAdded。如果onStudentAdded 耗时较多,那么请保证服务端的noticeStudentAdded 运行在非UI线程中,否则将导致服务端无法相应。

    • 客户端的onStudentAdded方法运行在客户端的Binder线程池中,所以不能在方法里面去访问UI相关的内容。如果要访问UI,请使用Handler切换到UI线程。
  3. Binder意外死亡之后的重新连接:

    1. 为Binder设置死亡代理:linkToDeathunlinkToDeath

      private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
              @Override
              public void binderDied() {
                  if (remoteStudentManager == null) {
                      return;
                  }
                  remoteStudentManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
                  remoteStudentManager = null;
                  //重新绑定远程服务
                  Intent intent = new Intent(MainActivity.this, StudentManagerService.class);
                  bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
              }
          };

      然后在客户端绑定远程服务成功后,给binder设置死亡代理:

      service.linkToDeath(mDeathRecipient,0);
    2. onServiceDisconnected 中重新连接远程服务。

    3. 两种方法可以随便选一种来用。它们的区别在于:

      • Binder的死亡代理,在客户端Binder线程池中被回调,所以不能访问UI。
      • onServiceDisconnected 在客户端的UI线程中被回调。

4. 添加权限验证功能

AIDL中权限验证有两种方法:
1. 在服务端的onBind方法中验证,如果验证不通过就返回null,验证失败的客户端就无法绑定服务。这里我们使用permission验证方式。

  1. 首先在manifest中声明所需要的权限:

    <permission android:name="com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE" 
    android:protectionLevel="normal"/>
  2. onBind方法添加权限验证:

    public IBinder onBind(Intent intent) {
            Log.d("StudentManagerService", "Service onBind");
            int check = checkCallingOrSelfPermission("com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                Log.e("StudentManagerService", "Service onBind :" + null);
                return null;
            }
            return binder;
        }

    此时运行,客户端就无法连接到远程服务。查看Logcat,可以发现如下信息:

    com.dou.ipcsimple:remote E/StudentManagerService: Service onBind :null

  3. 在manifest中注册该权限:

    ```
    <uses-permission android:name="com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE" />
    ```
    再次运行,客户端就可以连接上远程服务了。
    

2. 在服务端的onTransact 方法中进行权限验证,如果验证失败则返回false。

除了permission验证,还可以使用Uid和Pid来验证,通过getCallingUidgetCallingPid 可以拿到客户端所属应用的Uid和Pid。

    ```
    //StudentManagerService
    private Binder binder = new IStudentManager.Stub() {
            @Override
            public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
                int check = checkCallingOrSelfPermission("com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE");
                if (check == PackageManager.PERMISSION_DENIED) {
                    return false;
                }
                String pkgName = null;
                String[] pkgs = getPackageManager().getPackagesForUid(getCallingUid());
                if (null != pkgs && pkgs.length > 0) {
                    pkgName = pkgs[0];
                }
                if (!pkgName.startsWith("com.dou")) {
                    return false;
                }
                return super.onTransact(code, data, reply, flags);
            }
            ...
            ...
            ...
        };
    ```

显然,这个服务只有声明使用权限"com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE",而且包名以com.dou 开头的客户端,才能成功连接上远程服务端。

4. 尾声

Binder通信机制说到这里就暂时告一段落,下一篇将介绍另一种IPC机制,Content Privider。

本篇文章中,完整的代码可以从Github获取:IPCSimple

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值