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.aidlpackage 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 getStudentList();    //新增的接口,用于注册和注销观察者    void registerListener(OnNewStudentAddedListener listener);    void unregisterListener(OnNewStudentAddedListener listener);}
    3. 服务端StudentManagerService 修改:

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

        private CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>();    private Binder binder = new IStudentManager.Stub() {        @Override        public void addStudent(Student student) throws RemoteException {            students.add(student);        }        @Override        public List 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来保存所有的AIDL回调。对于本例中的3个listener对象(客户端1个,服务端2个),它们的低层Binder都是同一个。利用RemoteCallbackList 这个特性,就可以修复上面注销的BUG。
      • 使用RemoteCallbackList 替代CopyOnWriteArrayList, 然后修改注册/注销方法的实现:

        private RemoteCallbackList 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工作图解:

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

      public List 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中注册该权限:

    ``````再次运行,客户端就可以连接上远程服务了。

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。

更多相关文章

  1. Android中对媒体的使用
  2. Android中数据存储——SharedPreferences存储数据
  3. Android(安卓)AsyncTask实现异步任务的执行
  4. Androidstudio开发button按钮的操作以及项目开发大致过程
  5. React Native Linking与 Android原生页面路由跳转问题
  6. Android中SQLite应用详解(sql访问数据库)
  7. 关于Android软件盘的显示与隐藏的问题
  8. 浅谈Java中Collections.sort对List排序的两种方法
  9. Python list sort方法的具体使用

随机推荐

  1. 【鼠】安卓学习杂记(六)——Android相对布
  2. Android swap分区作用及swapper软件设置
  3. 解决Mono for android的xml编辑器无法代
  4. reactnative ~ android 模块通讯混合跳转
  5. Android API中文文档TextView
  6. J2ME项目移植到Android平台六大注意事项
  7. android LinearLayout和RelativeLayout实
  8. 浅析android平板市场的今天。
  9. Android中的APK,TASK,PROCESS,USERID之间
  10. Android(安卓)Bitmap 缩放 旋转 水印 裁