Android多進程之Binder解綁監聽的問題

Android多進程系列

接上一篇文章《Android多進程之手動編寫Binder類》中向服務端註冊監聽事件的問題,在擴展了Binder類後,咱們還須要改造對應的服務端和客戶端

客戶端和服務端的改造

服務端改造
  • 增長註冊監聽接口的功能
private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = new CopyOnWriteArrayList<IOnNewBookArrivedListener>();

private Binder mBinder = new BookManagerImpl(){
    @Override
    public List<Book> getBookList() throws RemoteException {
        Log.e(TAG, "getBookList-->"+ System.currentTimeMillis());
        return mBookList;
    }

    @Override
    public void addBook(Book book) throws RemoteException {
        Log.e(TAG, "addBook-->");
        mBookList.add(book);
    }

    @Override
    public void registerListener(IOnNewBookArrivedListener listener) {
        if (!mListenerList.contains(listener)) {
            mListenerList.add(listener);
        }else {
            Log.e(TAG, "already exists");
        }
        Log.e(TAG, "registerListener, size:"+mListenerList.size());
    }

    @Override
    public void unRegisterListener(IOnNewBookArrivedListener listener) {
        if (mListenerList.contains(listener)) {
            mListenerList.remove(listener);
            Log.e(TAG, "unRegisterListener listener succeed");
        }else {
            Log.e(TAG, "not found, can not unregister");
        }
        Log.e(TAG, "unRegisterListener, current size:"+mListenerList.size());

    }
};
複製代碼
  • 添加一個任務,定時像書籍列表中添加一本書,並觸發通知客戶端的操做
@Override
public void onCreate() {
    super.onCreate();
    Log.e(TAG, "onCreate-->"+ System.currentTimeMillis());
    mBookList.add(new Book(1, "Android"));
    mBookList.add(new Book(2, "IOS"));
    new Thread(new ServiceWorker()).start();
}

private void onNewBookArrived(Book book) throws RemoteException{
    mBookList.add(book);
    Log.e(TAG, "new book arrived, notify listeners:" + mListenerList.size());
    for (int i=0; i<mListenerList.size(); i++) {
        IOnNewBookArrivedListener listener = mListenerList.get(i);
        Log.e(TAG, "new book arrived, notify listener:" + listener);
        listener.onNewBookArrived(book);
    }
}

private class ServiceWorker implements Runnable {

    @Override
    public void run() {
        while (!mIsServiceDestoryed.get()) {
            try {
                Thread.sleep(5000);
            }catch (InterruptedException e) {
                e.printStackTrace();
            }

            int bookId = mBookList.size() + 1;
            Book newBook = new Book(bookId, "new book#" + bookId);
            try {
                onNewBookArrived(newBook);
            }catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }
}
複製代碼
  • 通知客戶端的過程就是調用每一個註冊的監聽接口的onNewBookArrived方法,也就是一個服務端調用客戶端的過程,分別是調用onNewBookArrived-->onTransact-->Proxy的onNewBookArrived-->客戶端
客戶端改造
  • 首先要聲明一個IOnNewBookArrivedListener對象,並註冊到服務端中
private IBookManager mRemoteBookManager;
private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        Log.e(TAG, "ServiceConnection-->"+ System.currentTimeMillis());
        IBookManager bookManager = BookManagerImpl.asInterface(iBinder);
        mRemoteBookManager = bookManager;
        try {
            List<Book> list = bookManager.getBookList();
            Log.e(TAG, "query book list, list type:" + list.getClass().getCanonicalName());
            Log.e(TAG, "query book list:" + list.toString());
            Book newBook = new Book(3, "Android 進階");
            bookManager.addBook(newBook);
            Log.e(TAG, "add book:" + newBook);
            List<Book> newList = bookManager.getBookList();
            Log.e(TAG, "query book list:" + newList.toString());
            bookManager.registerListener(mOnNewBookArrivedListener);

        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        mRemoteBookManager = null;
        Log.e(TAG, "binder died");
    }
};

/**
 * 這個方法運行在客戶端的binder線程池中,不能直接進行UI操做
 */
private IOnNewBookArrivedListener mOnNewBookArrivedListener = new OnNewBookArrivedListenerImpl(){
    @Override
    public void onNewBookArrived(Book book) {
        mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, book).sendToTarget();
    }
};
複製代碼
  • 而後因爲客戶端的IOnNewBookArrivedListener回調方法onNewBookArrived運行在Binder線程池中,因此須要一個Handler來切換到UI線程
private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;
private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message message) {
        switch (message.what) {
            case MESSAGE_NEW_BOOK_ARRIVED:
                Log.e(TAG, "receive new book:" + message.obj);
                break;
            default:
                super.handleMessage(message);
        }
    }

};
複製代碼
  • 最後須要在客戶端退出時解除服務端的註冊監聽
@Override
protected void onDestroy() {
    if (mRemoteBookManager != null && mRemoteBookManager.asBinder().isBinderAlive()) {
        try {
            Log.e(TAG, "unRegister listener:" + mOnNewBookArrivedListener);
            mRemoteBookManager.unRegisterListener(mOnNewBookArrivedListener);
        }catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    unbindService(mConnection);
    super.onDestroy();
}
複製代碼
改造結果

每隔5秒添加一本書

  • 經過上圖咱們能夠看到,咱們已經成功實現了預期的功能,而且服務端通知客戶端的調用過程也如咱們上面所說的那樣bash

  • 接下去咱們退出應用,這樣能夠測試解綁監聽的功能 微信

    解除綁定失敗

  • 從上圖咱們能夠看到,服務端調用解綁失敗了,提示找不到接口,這是咋回事呢?cookie

利用Binder進行進程間通訊,Binder會把客戶端傳遞的參數AIDL接口和Parcelable對象,從新轉化並生成一個新的對象。由於對象是不能跨進程傳輸的,對象的跨進程傳輸本質上就是序列化和反序列化的過程。因此上述狀況服務端根本就沒有客戶端的那個對象,那確定找不到會解綁失敗。那咋辦呢?

RemoteCallBackList

RemoteCallBackList是啥
public class RemoteCallbackList<E extends IInterface> {
    /*package*/ ArrayMap<IBinder, Callback> mCallbacks
            = new ArrayMap<IBinder, Callback>();
    ...
}
複製代碼
  • RemoteCallBackList的內部有一個Map結構用來保存全部的AIDL回調,這個Map的key是IBinder類型,value是CallBack類型
public boolean register(E callback, Object cookie) {
    synchronized (mCallbacks) {
        if (mKilled) {
            return false;
        }
        IBinder binder = callback.asBinder();
        try {
            Callback cb = new Callback(callback, cookie);
            binder.linkToDeath(cb, 0);
            mCallbacks.put(binder, cb);
            return true;
        } catch (RemoteException e) {
            return false;
        }
    }
}
複製代碼
  • 每次有新的接口來,就調用register方法註冊,添加到mCallbacks中
  • 經過上面的源碼咱們能夠看到,因爲客戶端跨進程傳輸的對象的底層的Binder對象都是同一個,因此咱們能夠經過這一點,當須要解除註冊監聽時,就能夠經過遍歷RemoteCallBackList找到與解綁客戶端Binder對象相同的listener並刪除便可
  • 並且咱們能夠看到RemoteCallBackList中實現了線程同步,咱們在利用它進行註冊和解註冊時不須要處理同步問題。
  • 特別的是,客戶端進程終止後,RemoteCallBackList可以自動解除客戶端所註冊的listener
用RemoteCallBackList實現實現解綁註冊
private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<IOnNewBookArrivedListener>();
private Binder mBinder = new BookManagerImpl(){
    @Override
    public List<Book> getBookList() throws RemoteException {
        Log.e(TAG, "getBookList-->"+ System.currentTimeMillis());
        return mBookList;
    }

    @Override
    public void addBook(Book book) throws RemoteException {
        Log.e(TAG, "addBook-->");
        mBookList.add(book);
    }

    @Override
    public void registerListener(IOnNewBookArrivedListener listener) {
        //註冊接口
        mListenerList.register(listener);
    }

    @Override
    public void unRegisterListener(IOnNewBookArrivedListener listener) {
        //解註冊接口
        mListenerList.unregister(listener);
    }
};

//通知客戶端
private void onNewBookArrived(Book book) throws RemoteException{
        mBookList.add(book);
        final int N = mListenerList.beginBroadcast();
        for (int i=0; i<N; i++) {
            IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
            if (listener != null) {
                try {
                    listener.onNewBookArrived(book);
                }catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }
複製代碼
  • 經過RemoteCallBackList的改造,咱們就能夠成功解註冊客戶端的listener了
注意點
  • RemoteCallBackList並非一個List,咱們沒法像操做list同樣操做它,好比調用size方法
  • 遍歷RemoteCallBackList必須按照上面代碼中的方式進行,beginBroadcast方法和finishBroadcast方法必須配對使用

歡迎關注個人微信公衆號,和我一塊兒學習一塊兒成長!
複製代碼

AntDream
相關文章
相關標籤/搜索