天天看點

IPC機制---04 Android中的IPC通訊方式(D)

  • 上一篇中提到,在對我們用戶端進行解綁監聽的時候,并沒有成功,服務端列印出not found listener的log,而我們用戶端在進行綁定和解綁的時候傳遞的listener明明是同一個,為什麼會出現這種情況呢?其實這是必然的,這種解注冊的處理方式在我們平時的開發中經常使用到,但是放在多程序中卻是失效的,因為Binder會把用戶端傳遞過來的對象重新轉化并生成一個新的對象。雖然我們解綁和綁定傳遞的是同一個對象,但是通過Binderde轉換已經是兩個全新的對象了。因為對象是不能跨程序直接傳輸的,對象的跨程序傳輸本質上就是反序列化的過程,這就是為什麼AIDL中的自定義對象必須實作Parcelable接口的原因。
  • 那到底如何進行解綁呢,下面介紹一下RemoteCallbackList
    • RemoteCallbackList是系統專門提供的使用者删除跨程序listener的接口,RemoteCallbackList是一個泛型,支援管理任意的AIDL接口,因為所有的AIDL接口都繼承自IInterface接口
      public interface IBookManager extends android.os.IInterface
                 
    • 它的工作原理很簡單,在它的内部有一個Map結構專門用來儲存所有的AIDL回調,這個map的key是IBinder類型,value是CallBack類型,如下
      /*package*/ ArrayMap<IBinder, Callback> mCallbacks
                  = new ArrayMap<IBinder, Callback>();
                 
      其中Callback中封裝了真正的遠端listener。當用戶端注冊listener的時候,它會把這個listener的資訊存入mCallbacks中,其中key和value分别通過下面的方式擷取
      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;
                  }
                 
  • 是以,雖然多次跨程序傳輸的對象在服務端會生成不同的對象,但是這些新生成的對象有一個共同點,那就是他們底層的Binder對象是同一個,利用這個特性,我們就可以實作上面無法實作的功能,當用戶端解綁注冊的時候,我們隻要周遊服務端所有的listener,找出和那個解注冊listener具有相同Binder對象的伺服器端listener,并把它删掉即可。
  • 同時,RemoteCallbackList還有一個很有用的功能,那就是當用戶端程序終止後,他能自動移除用戶端所注冊的listener,另外RemoteCallbackList内部實作了線程同步的功能,是以在注冊和解注冊的時候,不需要做額外的線程同步工作。
  • 下面,示範一下如何完成解注冊
    • 在BookService中,使用RemoteCallbackList代替CopyOnWriteArrayList,如下
      private RemoteCallbackList<IONewBookArrivedListener> listenerList = new RemoteCallbackList<>();
                 
    • 修改注冊和解注冊接口的實作
      @Override
              public void registerNewBookArrivedListener(IONewBookArrivedListener listener) throws RemoteException {
                  listenerList.register(listener);
                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                      Log.i(TAG, "listenerList size = " + listenerList.getRegisteredCallbackCount());
                  }
              }
      
              @Override
              public void unRegisterNewBookArrivedListener(IONewBookArrivedListener listener) throws RemoteException {
                  listenerList.unregister(listener);
                  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                      Log.i(TAG, "listenerList size = " + listenerList.getRegisteredCallbackCount());
                  }
              }
                 
    • 修改onNewBookArrived方法,當有新書時,通知所有已注冊的listener
      private void onNewBookArrived(Book book) throws RemoteException {
              bookList.add(book);
              int N = listenerList.beginBroadcast();
              for (int i = 0; i < N; i++) {
                  IONewBookArrivedListener arrivedListener = listenerList.getBroadcastItem(i);
                  if (arrivedListener != null) {
                      arrivedListener.onNewBookArrived(book);
                  }
              }
              listenerList.finishBroadcast();
          }
                 
    • 至此,BookService修改完畢,通過在注冊和解注冊時打log檢視成功否,如下
      03-11 11:16:44.486 31043-31073/com.happy.ipc.server I/BookService: listenerList size = 1
      03-11 11:16:47.226 31043-31073/com.happy.ipc.server I/BookService: listenerList size = 0
                 
      很顯然,使用RemoteCallbackList可以實作跨程序的解注冊功能。
  • 使用RemoteCallbackList,無法像操作List一樣去操作它,雖然名字中帶List,但它卻不是一個List,周遊他的畫,需要beginBroadcase和finishBroadcase配對使用。
  • 至此,AIDL的基本用法基本介紹完了,有幾點需要說明一下
    • 用戶端調用遠端服務的方法,被調用的方法運作在服務端的Binder線程池中,同時用戶端會被挂起,如果服務端比較耗時,會導緻用戶端長時間阻塞在這裡,如果是用戶端的UI線程的話,就會導緻用戶端出現ANR錯誤,是以,當我們知道遠端方法是耗時的時候,不應該再在UI線程進行調用了。
    • 用戶端的onServiceConnected和onServiceDisconnected方法都運作在UI線程中,是以也不能在其裡面直接調用服務端的耗時方法
    • 服務端的方法本身就運作在Binder線程池中,是以不需要再在服務端另起線程執行操作了
    • 同理,當遠端服務需要調用用戶端的listener中的方法時,被調用的方法運作在用戶端的Binder線程池中,是以同樣不可以在服務端中調用用戶端的耗時方法。比如BookService中的onNewBookArrived方法,調用了用戶端的onNewBookArrived方法,如果用戶端的這個方法比較耗時的話,要確定BookService中的onNewBookArrived運作在非UI線程中,否則會導緻服務端無法相應。
    • 用戶端的IOnNewBookArrivedListener中的onNewBookArrived方法運作在用戶端的Binder線程池中,是以不能在此方法中進行UI相關操作,如果需要,借助Handler進行處理。
  • Binder是可能意外死亡的,為了提高健壯性,我們需要在服務端程序意外終止的時候重新連接配接服務,兩種方法
    • 給Binder設定DeathRecipient監聽,當Binder死亡時,我們會收到binderDied的回掉,可以重新連接配接服務,如下
      private ServiceConnection conn = new ServiceConnection() {
              @Override
              public void onServiceConnected(ComponentName name, IBinder service) {
                  mBookManager = IBookManager.Stub.asInterface(service);
                  try {
                      service.linkToDeath(deathRecipient, 0);
                  } catch (RemoteException e) {
                      e.printStackTrace();
                  }
                  try {
                      mBookManager.registerNewBookArrivedListener(listener);
                  } catch (RemoteException e) {
                      e.printStackTrace();
                  }
              }
      
              @Override
              public void onServiceDisconnected(ComponentName name) {
                  mBookManager = null;
              }
          };
      
          private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
              @Override
              public void binderDied() {
                  if (mBookManager != null) {
                      mBookManager.asBinder().unlinkToDeath(deathRecipient, 0);
                      mBookManager = null;
                  }
                  // 重新連接配接服務
              }
          };
                 
    • 在onServiceDisconnected中重新連接配接遠端服務
      @Override
              public void onServiceDisconnected(ComponentName name) {
                  mBookManager = null;
                  // 重新連接配接服務
              }
                 
    • 差別在于:onServiceDisconnected在用戶端的UI線程中被回調,而binderDied在用戶端的Binder線程中被回調,不能進行UI通路操作。
  • 最後,如何在AIDL中使用權限驗證功能。預設情況下,我們的遠端服務任何人都可以連結,但這應該不是我們願意看到的,是以,我們必須給服務加入權限驗證功能,驗證失敗,則無法調用服務中的方法。以下為兩種常用的方法
    • 在onBinder中進行驗證,驗證不通過直接傳回null,這也驗證失敗的用戶端就無法直接綁定付無,驗證方式有很多種,比如使用permission驗證,使用這種方式,首先在AndroidManifestz紅聲明所需的權限,比如 這樣,一個應用來綁定我們的服務時,會驗證這個應用的權限,達到想到的效果,此方法同樣适用于Messenger中。如果我們自己内部的應用想綁定到我們的服務中,需要在AndroidManifest中聲明permission即可,如下
      @Override
          public IBinder onBind(Intent intent) {
              int check = checkCallingOrSelfPermission("com.happ.ipc.server.ACCESS_BOOK_SERVICE");
              if (check == PackageManager.PERMISSION_DENIED) {
                  Log.i(TAG, "permission denied");
                  return null;
              }
              Log.i(TAG, "permission access");
              return mBinder;
          }
                 
      <service
                  android:name=".service.BookService"
                  android:enabled="true"
                  android:exported="true"></service>
          </application>
      
          <permission
              android:name="com.happ.ipc.server.ACCESS_BOOK_SERVICE"
              android:protectionLevel="normal" />
                 
      ps:如果服務端和用戶端是兩個工程,則在Service中無法驗證用戶端的權限,因為onBinde方法不是一個binder調用的,它運作在服務端的UI線程,是以在onBind中隻能驗證服務端的權限,這樣就木有意義了,是以推薦使用第二種。
    • 第二種方法,在服務端的onTransact方法中進行權限驗證,如果驗證失敗直接傳回false,這也服務端也不會執行AIDL中的方法,進而達到保護服務端的效果。具體的驗證方式很多,可以采用permission驗證,實作和第一種一樣,還可以采用pid和uid來做驗證,通過getCallingUid和getCallingPid可以拿到用戶端所屬應用的uid和pid,通過這種方式可以做一些驗證工作,比如包名。下面既驗證了permission,又驗證了報名。一個應用如果想遠端調用服務的方法,首先要使用我們剛才定義的權限,并且包名是"com.happy.ipc.client",否則就會調用失敗
      @Override
              public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
                  String packageName = null;
                  int callingPid = getCallingPid();
                  int callingUid = getCallingUid();
                  Log.i(TAG, "callingPid = " + callingPid + ",callingUid = " + callingUid);
                  String[] packagesForUid = BookService.this.getPackageManager().getPackagesForUid(callingUid);
                  if (packagesForUid != null && packagesForUid.length > 0) {
                      packageName = packagesForUid[0];
                  }
                  Log.i(TAG, "packageName = " + packageName);
                  if (TextUtils.isEmpty(packageName) || !"com.happy.ipc.client".equals(packageName)) {
                      return false;
                  }
                  return super.onTransact(code, data, reply, flags);
              }
                 
      檢視日志發現
      03-11 11:48:09.516 28044-28057/com.happy.ipc.server I/BookService: listenerList size = 1
      03-11 11:48:20.876 28044-28057/com.happy.ipc.server I/BookService: callingPid = 28469,callingUid = 11166
      03-11 11:48:20.876 28044-28057/com.happy.ipc.server I/BookService: packageName = com.happy.ipc.client
                 
      如果包名進行修改,比如
      if (TextUtils.isEmpty(packageName) || !"com.happy.ipc.client.test".equals(packageName)) {
                      return false;
                  }
                 
      這樣,通過client點選時間擷取書的清單和count的時候,列印如下log,成功實作權限驗證
      03-11 11:52:02.196 3507-3507/com.happy.ipc.client I/MainActivity: bookList = []
      03-11 11:52:02.826 3507-3507/com.happy.ipc.client I/MainActivity: count = 0
                 
    • 上門介紹了AIDL權限驗證的兩種方法,此外還有其他方法比如在service中指定android:permissio屬性等。
  • 至此,IPC中的AIDL基本介紹完了。