上次講到ipC機制的幾種通信方式,但是沒有講完,還剩下幾種方式沒有講,今天繼續。
在上次講到Service的時候,說過AIDL,但是那種只是非常簡單的方式,今天將會詳細的講述AIDL。
aidl支持的數據類型:
基本數據類型(int,long,char,boolean,double等)String和CharSequenceList:只支持ArrayList,里面的每個元素都必須能夠被AIDL支持。Map:只支持HashMap,里面的每個元素都必須能夠被AIDL支持Parcelable:所有實現了Parcelable接口的對象。AIDL:所有的AIDL接口本身也可以在AIDL中使用。注意:
自定義的Parcelable對象和AIDL對象必須要顯示import進來,不管他們是否在同一個包中。
package cn.demo.zx_aidl_learn;import cn.demo.zx_aidl_learn.domain.Book;interface IService { List<Book>getBookList(); void addBook(in Book b);}如果AIDL文件中用到了自定義的Parcelable對象,那么必須新建一個和它同名的AIDL文件,并在其中聲明為parcelable類型。
案例:
服務端:
public class BookManagerService extends Service { /** * 使用CopyOnWriteArrayList的原因: * 因為CopyOnWriteArrayList支持并發讀/寫,AIDL方法是在服務端的Binder的線程池中執行的, * 因此當多個客戶端同時連接的時候,會存在多個線程同時訪問的情形,所以我們要在AIDL方法中 * 處理線程同步,而我們這里直接使用CopyOnWriteArrayList來進行自動的線程同步。 * * AIDL中能夠使用的List只有ArrayList,但是這里為什么還使用它呢? * 因為AIDL中所支持的是抽象的List,而List只是一個接口,因此雖然服務端返回的是CopyOnWriteArrayList, * 但是在Binder中會按照List的規范去訪問數據并最終返回一個新的ArrayList傳遞給客戶端。還有就是類似的是 * ConcurrentHashMap. */ CopyOnWriteArrayList<Book>mBookList = new CopyOnWriteArrayList<>(); public BookManagerService() { } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { super.onCreate(); mBookList.add(new Book(1,"android")); mBookList.add(new Book(2,"ios學習")); } PRivate Binder mBinder = new IService.Stub(){ @Override public List<Book> getBookList() throws RemoteException { return mBookList; } @Override public void addBook(Book b) throws RemoteException { mBookList.add(b); } };}客戶端:
public class MainActivity extends AppCompatActivity { private IService mService = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Intent intent = new Intent(this,BookManagerService.class); startService(intent); bindService(intent,conn,BIND_AUTO_CREATE); } public void findBook(View v){ /** * 注意:服務端的方法有可能是耗時的方法,那么下面的調用就有可能早能ANR異常,需要注意。 */ try { List<Book> bookList = mService.getBookList(); if(bookList!=null){ for(Book b : bookList){ System.out.println("書籍::"+b.toString()); } } } catch (RemoteException e) { e.printStackTrace(); } } private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = IService.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { } };}清單文件AndroidMenifest:
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/A上述案例只是講述客戶端從服務端獲取圖書信息,如果是在服務端每添加一本書籍就向客戶端發送消息,那么怎么實現呢?這里就會用到觀察者模式。
創建一個aidl接口,每個用戶都需要實現這個接口并且向圖書館申請新書的提醒功能。
新創建的aidl文件:
import cn.demo.zx_aidl_learn.domain.Book;interface IOnNewBookArrivedListener { void onNewBookArrived(in Book newBook);}import cn.demo.zx_aidl_learn.domain.Book;import cn.demo.zx_aidl_learn.IOnNewBookArrivedListener;interface IService { List<Book>getBookList(); void addBook(in Book b); void registerListener(in IOnNewBookArrivedListener listener); void unregisterListener(in IOnNewBookArrivedListener listener);}在MainActivity中創建一個IOnNewBookArrivedListener對象,然后通過注冊的方式將IOnNewBookArrivedListener對象傳遞給服務端:
private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = IService.Stub.asInterface(service); try { /** * 注冊對新書的監控 */ mService.registerListener(listener); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { }};private IOnNewBookArrivedListener listener = new IOnNewBookArrivedListener.Stub() { @Override public void onNewBookArrived(Book newBook) throws RemoteException { System.out.println("新到的圖書::"+newBook.toString()); mHandler.obtainMessage(0,newBook).sendToTarget(); }};服務端通過獲取傳入的IOnNewBookArrivedListener對象來調用客戶端中的方法來通知客戶端:
/** * 發送新書已到的消息到客戶端 * 怎樣通知呢? * 通過客戶端在主持監聽的時候傳入的IOnNewBookArrivedListener對象來通知客戶端 * @param newBook */private void sendNewBookArrivedToClient(Book newBook){ mBookList.add(newBook); for(int i=0;i<mListenerList.size();i++){ IOnNewBookArrivedListener listener = mListenerList.get(i); try { listener.onNewBookArrived(newBook); } catch (RemoteException e) { e.printStackTrace(); } }}詳細功能實現請查看源碼
當我們實現了注冊功能之后,怎樣取消注冊功能呢?
@Overrideprotected void onDestroy() { super.onDestroy(); if(mService!=null || mService.asBinder().isBinderAlive()){ try { mService.unregisterListener(listener); } catch (RemoteException e) { e.printStackTrace(); } } unbindService(conn);}我們通過上述方法無法取消注冊。這是為什么呢?
這是因為在客戶端傳入的IOnNewBookArrivedListener對象是同一個對象,但是在服務端卻又變成了另外的一個對象了。因為對象無法跨進程通信,服務端接收的IOnNewBookArrivedListener對象是通過序列化之后得到對象,所以無法取消注冊,這種情況怎么處理呢?
這里我們就會用到RemoteCallbackList類,它是系統專門提供的用于刪除跨進程listener的接口,它是一個泛型,支持管理任意的AIDL接口,因為它繼承了IInterface接口,aidl接口都繼承IInterface。
它的工作原理非常簡單,就是在它的內部有一個專門用于保存所有的AIDL回調的Map集合:
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();其中Callback中封裝了真正的遠程listener,當客戶端注冊listener的時候,它會把這個listener的信息存入mCallback中,其中key和value分別通過下面的方式獲得:
private final class Callback implements IBinder.DeathRecipient。。。。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; } }}到這里就應該知道,雖然在客戶端傳入的同一對象在服務端會生成了不同的對象,但是它的底層的Binder對象卻沒有變化,利用這個特性就可以實現上面的功能了。當客戶端取消注冊的時候,我們會便利所有的listener接口,找到與客戶端傳入的listener具有相同Binder對象的服務端listener,然后把它刪除即可。
服務端的代碼:
RemoteCallbackList<IOnNewBookArrivedListener>mListenerList = new RemoteCallbackList<>();@Override public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {// if(!mListenerList.contains(listener)){// mListenerList.add(listener);// } mListenerList.register(listener); } @Override public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {// if(mListenerList.contains(listener)){// mListenerList.remove(listener);// } mListenerList.unregister(listener); }/** * 發送新書已到的消息到客戶端 * 怎樣通知呢? * 通過客戶端在主持監聽的時候傳入的IOnNewBookArrivedListener對象來通知客戶端 * @param newBook */private void sendNewBookArrivedToClient(Book newBook){ mBookList.add(newBook);// for(int i=0;i<mListenerList.size();i++){ // IOnNewBookArrivedListener listener = mListenerList.get(i); // try { // listener.onNewBookArrived(newBook); // } catch (RemoteException e) { // e.printStackTrace(); // } // } int N = mListenerList.beginBroadcast(); for(int i=0;i<N;i++){ IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i); try { listener.onNewBookArrived(newBook); } catch (RemoteException e) { e.printStackTrace(); } } mListenerList.finishBroadcast();}關于死亡代理,以前講過,這里在說明一下,當客戶端在與服務端進行跨進程通信的時候,服務端因為異常而被銷毀,但是服務端卻不知道,那么怎樣解決這個問題呢,這里就講到了重新連接服務的兩種方法,第一種就是死亡代理:
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { mService.asBinder().unlinkToDeath(mDeathRecipient,0); mService = null; //重新連接 bindService(intent,conn,BIND_AUTO_CREATE); }};private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mService = IService.Stub.asInterface(service); try { service.linkToDeath(mDeathRecipient,0); } catch (RemoteException e) { e.printStackTrace(); } try { /** * 注冊對新書的監控 */ mService.registerListener(listener); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { }};還有一種方法就是在onServiceDisconnected中重新連接綁定服務。
我們不想讓所有的客戶端訪問我們的服務端,我們希望具有某些權限的客戶端才能訪問,怎樣實現?
第一種方法,在onBind中進行驗證。驗證不通過就返回null,造成無法連接服務端。這種驗證很多,這里介紹其中一種permission權限驗證,在服務端定義一個權限。
AndroidMenifest:
<permission android:name="cn.demo.zx_aidl_learn.YANZHENG"/>服務端:
@Overridepublic IBinder onBind(Intent intent) { int check = checkCallingOrSelfPermission("cn.demo.zx_aidl_learn.YANZHENG"); if(check== PackageManager.PERMISSION_DENIED){ return null; } return mBinder;}如果想要訪問這個服務端的話,那么客戶端就需要設置權限:
<uses-permission android:name="cn.demo.zx_aidl_learn.YANZHENG"/>另一種方法,這種方法就是在服務端的onTransact方法中進行權限驗證,如果驗證失敗返回false。驗證的方法也是很多,可以通過permission驗證,也可以通過包名驗證。permission驗證的方法與上述onBind方法中差不多,唯一的區別就是返回false。包名驗證就通過getCallinngUid和getCallingPid獲取客戶端的uid和pid從而獲取包名,進行驗證。
String[] packageNames = getPackageManager().getPacagesForUid(getCallingUid());if(!packageNames[0].startsWith("cn.demo")){ return false;}當客戶端訪問服務端的方法時候,客戶端會等待跨進程通信,如果服務端中的方法是一個耗時的方法的話,那么客戶端就需要在子線程中去訪問,如果在UI線程中,容易造成ANR異常。服務端訪問客戶端的方法也是一樣的。
源碼下載:
https://github.com/zhangxun1900/my_ipc_learn/
新聞熱點
疑難解答