IPC之AIDL进阶用法

2018-09-11  本文已影响9人  一线游骑兵

接上篇笔记 AIDL入门用法

主要包括下边四点:

  1. 跨进程的接口回调【观察者】
  2. 线程问题
  3. 断开重连问题
  4. 权限校验问题

一:跨进程的观察者
Step1. 首先定义一个通知客户端的回调接口
// OnNewBookInsertListener.aidl
package com.zhu.aidldemo;

import com.zhu.aidldemo.Book;

interface OnNewBookInsertListener {
   void onNewBookInsert(in Book book);
}

因为在AIDL文件中不支持接口,所以定义成aidl文件。

Step2. 增加IBookManager.aidl方法
// IBookManager.aidl
package com.zhu.aidldemo;
import com.zhu.aidldemo.Book;
import com.zhu.aidldemo.OnNewBookInsertListener;

interface IBookManager {
   void addBook(in Book book);
   List<Book> getBookList();
   void registerBookInsertListener(OnNewBookInsertListener listener);
   void unregisterBookInsertListener(OnNewBookInsertListener listener);
}

新增注册与注销观察者的方法。然后Make Project.

Step3. 在服务端增加相关逻辑代码。
public class BookManagerService extends Service {
    private final static String TAG = "server";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
    private CopyOnWriteArrayList<OnNewBookInsertListener> mListenerList = new CopyOnWriteArrayList<>();
    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "人类简史"));
        mBookList.add(new Book(2, "三体"));
        new Thread(new BookAutoInsertWorker()).start(); //每个5s自动向bookList中插入一条数据
    }

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public void addBook(Book book) throws RemoteException {
            Log.d(TAG, "receive a request from client, add book to list...");
            mBookList.add(book);
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            Log.d(TAG, "receive a request from client, query book list...");
            return mBookList;
        }

        @Override
        public void registerBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
            if (!mListenerList.contains(listener)) {
                Log.d(TAG, "reguister listener to list..." + listener.toString());
                mListenerList.add(listener);
            } else {
                Log.d(TAG, "listener already exists...");
            }
            Log.d(TAG, "listenerList size: " + mListenerList.size());
        }

        @Override
        public void unregisterBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
            if (mListenerList.contains(listener)) {
                Log.d(TAG, "remove listener from list..." + listener.toString());
                mListenerList.remove(listener);
            } else {
                Log.d(TAG, "listener not exists...");
            }
            Log.d(TAG, "listenerList size: " + mListenerList.size());
        }
    };

    private class BookAutoInsertWorker implements Runnable {

        @Override
        public void run() {
            while (!mIsServiceDestoryed.get()) {
                try {
                    Thread.sleep(5000);
                    int bookId = mBookList.size() + 1;
                    Book newBook = new Book(bookId, "New book#" + bookId);
                    insertBookAndNotifyUser(newBook);
                } catch (Exception e) {
                    Log.e(TAG, e.getMessage());
                }
            }
        }
    }

    private void insertBookAndNotifyUser(Book newBook) {
        try {
            mBookList.add(newBook);
            Log.d(TAG, "add a new book: " + newBook.toString() + " , size: " + mBookList.size());
            for (int i = 0; i < mListenerList.size(); i++) {
                OnNewBookInsertListener onNewBookInsertListener = mListenerList.get(i);
                Log.d(TAG, "notify user > " + onNewBookInsertListener.toString());
                onNewBookInsertListener.onNewBookInsert(newBook);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }


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

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestoryed.set(true);
    }
}

新增逻辑如下:

Step4. 客户端注册并监听相关方法。
class MainActivity : AppCompatActivity() {
    val TAG = "client"

    val MSG_NEW_BOOK_INSERTED = 1
    private var mHandler = object : Handler() {
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            when (msg?.what) {
                MSG_NEW_BOOK_INSERTED -> {
                    Log.d(TAG, "receive a notify from server: " + msg.obj.toString())
                }
                else -> super.handleMessage(msg)
            }
        }
    }

    private var onNewBookInsertListener = object : OnNewBookInsertListener.Stub() {

        override fun onNewBookInsert(book: Book?) {
            mHandler.obtainMessage(MSG_NEW_BOOK_INSERTED, book).sendToTarget()
        }
    }

    private var mBookManager: IBookManager? = null
    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            var bookManager = IBookManager.Stub.asInterface(service)
            mBookManager = bookManager
            try {
                var bookList = bookManager.bookList
                Log.d(TAG, "init book list on server: " + bookList.toString())
                bookManager.addBook(Book(3, "未来简史"))
                Log.d(TAG, "insert a book from client...")
                Log.d(TAG, "book list on server after insert : " + bookManager.bookList.toString())
                bookManager.registerBookInsertListener(onNewBookInsertListener)
            } catch (e: Exception) {
                println(e.message)
            }
        }

        override fun onServiceDisconnected(name: ComponentName) {
            Log.d(TAG, "onServiceDisconnected...")
            mBookManager = null
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var intent = Intent(this, BookManagerService::class.java)
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    override fun onDestroy() {
        super.onDestroy()
        if (mBookManager != null && mBookManager?.asBinder()?.isBinderAlive!!) {
            try {
                Log.d(TAG, "unregister listener....")
                mBookManager?.unregisterBookInsertListener(onNewBookInsertListener)
            } catch (e: Exception) {
                Log.e(TAG, e.message)
            }

        }
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null)
        }
        unbindService(serviceConnection)
    }
}

新增逻辑如下:

  1. 获取服务端初始图书集合
  2. 调用服务端方法新增一条数据
  3. 注册成为一个观察者
  4. 当收到新增图书通知时打印相关信息
  5. 页面退出时注销观察者
结果:

客户端日志:


image.png

服务端日志:


image.png
存在的问题:

有日志可以观察到,在客户端页面退出时,调用服务端的注销观察者方法,并没有注销成功,而且客户端在退出时,依然能再次受到一条通知。猜测每次传入过去的onNewBookInsertListener都是一个新的对象。
对象是不能跨进程传输的,对象的跨进程传输本质上是一中反序列化,因此Binder回把客户端传过来的对象重新转化并生成一个新的对象。因此无法像单进程一样操作list集合。

解决的方法:

使用RemoteCallbackList。对BookManagerService中的mListenerList做如下修改:

  1. CopyOnWriteArrayList改为RemoteCallbackList
    private RemoteCallbackList<OnNewBookInsertListener> mListenerList = new RemoteCallbackList<>();
  1. 适配注册观察者与反注册观察者方法:
        @Override
        public void registerBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
            boolean result = mListenerList.register(listener);
            if (result) {
                Log.d(TAG, "reguister listener to list success..." + listener.toString());
            } else {
                Log.d(TAG, "register listener failed...");
            }
            int count = mListenerList.beginBroadcast();
            Log.d(TAG,"listener size : "+count);
            mListenerList.finishBroadcast();
        }

        @Override
        public void unregisterBookInsertListener(OnNewBookInsertListener listener) throws RemoteException {
            boolean result = mListenerList.unregister(listener);
            if (result) {
                Log.d(TAG, "unregister listener success..." + listener.toString());
            } else {
                Log.d(TAG, "unregister listener failed...");
            }
            int count = mListenerList.beginBroadcast();
            Log.d(TAG,"listener size : "+count);
            mListenerList.finishBroadcast();
        }
  1. 修改通知客户端的方法:
     private void insertBookAndNotifyUser(Book newBook) {
        try {
            mBookList.add(newBook);
            final int N = mListenerList.beginBroadcast();
            Log.d(TAG, "add a new book: " + newBook.toString() + " , size: " + mBookList.size());
            for (int i = 0; i < N; i++) {
                OnNewBookInsertListener onNewBookInsertListener = mListenerList.getBroadcastItem(i);
                if (onNewBookInsertListener != null) {
                    Log.d(TAG, "notify user > " + onNewBookInsertListener.toString());
                    onNewBookInsertListener.onNewBookInsert(newBook);
                }
            }
            mListenerList.finishBroadcast();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
  1. 结果:
    客户端日志:


    image.png

服务端日志:


image.png
  1. RemoteCallbackList说明:

二:线程问题
1. IPC时服务端的耗时操作导致的ANR

由于客户端的onServiceConnected方法实在UI线程中执行的,所有当在该方法内部调用服务端比较耗时的方法时,容易导致客户端ANR,因此在该方法中最好开启线程调用服务端的方法。
打印结果如下:

image.png
当在服务端的addBook 以及 getBookList方法中睡眠6s后客户端多次调用回发生ANR:
image.png image.png

解决方法:在onServiceConnected方法开启新的线程并在线程内部调用服务端方法,如需操作UI,可通过Handler或runOnUiThread等方法实现。
另外,由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,因此不需再开启新线程

2. IPC时客户端耗时操作导致的问题

同样,如果再服务端调用客户端的方法时,如果客户端的方法中进行了耗时的操作,比如在该demo中,当服务端新增一条数据时,会通过onNewBookInsertListener.onNewBookInsert(newBook);回调客户端的方法。该种情况请确保客户端的回调不是在UI线程中接受即可。否则也会造成客户端的ANR。

image.png
该方法运行在客户端的Binder线程池中,因此不能在该方法中进行UI操作。
三:服务意外断开重连问题

当服务端进程意外停止,Binder会意外死亡,此时需要客户端重新进行连接服务。

1. 方法一:该Binder设置DeathRecipient监听

首先实现IBinder.DeathRecipient接口,并重写binderDied方法。

image.png
在接受到binder终止后重新绑定远程服务
之后将该回调设置给binder:
  var bookManager = IBookManager.Stub.asInterface(service)
  mBookManager = bookManager
  mBookManager?.asBinder()?.linkToDeath(this@MainActivity, 0)

测试: 使用 adb shell kill pid 杀死远程服务进程【adb shell ps】,然后客户端打印log如下:查看进程信息

image.png
四:IPC权限校验

为了防止任何人都可以连接远程服务,因此有时需要在客户端连接时进行权限校验。

方法1:在onBind中进行验证

首先在客户端的清单文件中声明调用权限:

    <permission
        android:name="com.zhu.aidldemo.permission.ACCESS_BOOK_MANAGE_SERVICE"
        android:protectionLevel="normal" />

    <uses-permission android:name="com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE" />

然后在服务端的onBind方法中进行校验:

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE");
        if (check == PackageManager.PERMISSION_DENIED) {
            Log.d(TAG, "permission denied....");
            return null;
        }
        Log.d(TAG, "permission granted....");
        return mBinder;
    }

结果:


image.png

反例【更改onBind中的权限内容】:


image.png
方法2:在onTransact方法中进行权限校验

重写服务端创建Binder中的onTransact方法:

        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            int check = checkCallingOrSelfPermission("com.zhu.aidldemo.permission.ACCESS_BOOK_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                Log.d(TAG, "permission denied....");
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (packages != null && packages.length > 0) {
                packageName = packages[0];
                Log.d(TAG, "pacakgeName: " + packageName);
            }
            if (!packageName.equals("com.zhu.aidldemo")) {
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

双重校验:先校验是否有权限,然后校验用户uid的包名。

测试结果:

正例:


image.png

反例【修改服务端校验的包名】:


image.png

完。

上一篇下一篇

猜你喜欢

热点阅读