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工作图解:
    Android跨进程通信-IPC初探(三) - 使用AIDL_第1张图片

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

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

      然后运行,发现ANR:
      Android跨进程通信-IPC初探(三) - 使用AIDL_第2张图片

    • 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 LayoutInflater inflate方法效率
  2. 下载Android Sdk源码方法
  3. Android 自定义属性时TypedArray的使用方法
  4. Android TV 智能电视/盒子 APP 开发焦点控制 两种方法实例
  5. android v7包 正常导入使用方法
  6. Could not find *****/adb.exe!的解决方法(android sdk太新了?**#
  7. 转帖并消化:Android中一种使用AttributeSet自定义控件的方法

随机推荐

  1. Android内存泄漏监测(MAT)及解决办法
  2. Android(安卓)笔记:Android将图像转换成流
  3. Android(安卓)处理启动页(Splash)白屏问
  4. Eclipse 卡死在 Android(安卓)SDK Conten
  5. Android横竖屏切换
  6. Android终于公布源代码
  7. Google Android(安卓)应用程序结构
  8. android中炫酷划屏事件及sqlite全部操作D
  9. Android动态部署五:如何从插件apk中启动Se
  10. Dragger android 的Activity切换动画大全