1. 程式人生 > >Android中IPC的幾種方式詳細分析與優缺點分析

Android中IPC的幾種方式詳細分析與優缺點分析

Android程序間通訊(IPC:Inter-Process Communication)的幾種主要方式如下

1.使用Bundle   ----> 用於android四大元件間的程序間通訊

android的四大元件都可使用Bundle傳遞資料  所以如果要實現四大元件間的程序間通訊 完全可以使用Bundle來實現 簡單方便  

2.使用檔案共享  ---->用於單執行緒讀寫
這種方式在單執行緒讀寫的時候比較好用 如果有多個執行緒併發讀寫的話需要限制執行緒的同步讀寫  
另外 SharePreference是個特例  它底層基於xml實現  但是系統對它的讀寫會基於快取,也就是說再多程序模式下就變得不那麼可靠了,有很大機率丟失資料


3.使用Messenger   ---->用於可存放在message中的資料的傳遞

使用這個方式可以在不同程序間傳遞message物件  這是一種輕量級的IPC方案  當傳遞的物件可以放入message中時  可以考慮用這種方式  但是msg.object最好不要放
因為不一定可以序列化  
使用它的步驟如下:
假設這樣一個需求  需要在客戶端A傳送訊息給服務端B接受  然後服務端B再回復給客戶端A 

1. 首先是客戶端A傳送訊息給服務端B  所以在客戶端A中 宣告一個Handler用來接受訊息  並建立一個Messenger物件 用Handler作為引數構造  然後onBinder方法返回messenger.getBinder() 即可

  1. publicclass MyServiceA 
    extends Service {  
  2.         privateclass MessageHandler extends Handler{  //建立的接受訊息的handler
  3.             @Override
  4.             publicvoid handleMessage(Message msg) {  
  5.                 switch (msg.what){  
  6.                     case1:  
  7.                         Bundle bundle = msg.getData();  
  8.                         String str = bundle.getString("aaa"
    );  
  9.                         System.out.println("----"+str);  
  10.                         Messenger replyTo = msg.replyTo; //此處往下是用來回復訊息給客戶端A的   
  11.                         Message replyMsg = Message.obtain(null,2);  
  12.                         Bundle bundle1 = new Bundle();  
  13.                         bundle1.putString("bbb","remote222給主程序回覆訊息啦");  
  14.                         replyMsg.setData(bundle1);  
  15.                         try {  
  16.                             replyTo.send(replyMsg);  
  17.                         } catch (RemoteException e) {  
  18.                             e.printStackTrace();  
  19.                         }  
  20.                         break;  
  21.                 }  
  22.                 super.handleMessage(msg);  
  23.             }  
  24.         }  
  25.         Messenger messenger = new Messenger(new MessageHandler());  
  26.         public MyServiceA() {  
  27.         }  
  28.         public IBinder onBind(Intent intent) {  
  29.             return messenger.getBinder();  
  30.         }  
  31.     }  


2.在客戶端A自然是需要傳送訊息給服務端B的  所以需要在服務繫結完成之後  獲取到binder物件  之後用該物件構造一個Messenger物件  然後用messenger傳送
訊息給服務端即可  程式碼如下  :
  1. publicvoid onServiceConnected(ComponentName name, IBinder service) {  
  2.                 Messenger messenger = new Messenger(service);  
  3.                 Message msg = Message.obtain(null,1);  
  4.                 Bundle bundle = new Bundle();  
  5.                 bundle.putString("aaa""主程序給remote22程序發訊息啦");  
  6.                 msg.setData(bundle);  
  7.                 msg.replyTo = mmessenger; //這行程式碼用於客戶端A接收服務端請求 設定的訊息接收者 
  8.                 try {  
  9.                     messenger.send(msg);  
  10.                 } catch (RemoteException e) {  
  11.                     e.printStackTrace();  
  12.                 }  
  13.             }  


3.由於在服務端接收到了客戶端的訊息還需要回復  所以在服務端程式碼中獲取 msg中的replyTo物件  用這個物件傳送訊息給 客戶端即可 
 在客戶端需要建立一個handler和Messenger  將傳送的msg.replyTo設定成Messenger物件  就可  


4.AIDL android 介面定義語言  ---->主要用於呼叫遠端服務的方法的情況 還可以註冊介面 

使用方法很簡單  
在服務端定義aidl檔案 自動生成java檔案  然後在service中實現這個aidl  在onbind中返回這個物件  
在客戶端把服務端的aidl檔案完全複製過來  包名必須完全一致   在onServiceConnected方法 中 把  Ibinder物件 用asInterface方法轉化成 aidl物件
然後呼叫方法即可  

需要注意的地方: 
在aidl檔案中並不是支援所有型別 
僅支援如下6種類型:
基本資料型別---- int long  char  boolean double 
String  charSequence
List  只支援ArrayList  CopyOnWriteArrayList也可以。。  裡面元素也必須被aidl支援
Map   只支援HashMap   ConCurrentHashMap也可以  裡面元素也必須支援aidl
Parcelable  所有實現了此介面的物件 
AIDL  所有的AIDL介面   因此 如果需要使用介面 必須使用AIDL介面

其中自定義的型別和AIDL物件必須顯示import進來 不管是不是在一個包中  
如果AIDL檔案中用到了自定義的Parcelable物件  必須建立同名的AIDL檔案 並宣告為Parcelable型別
AIDL檔案中除了基本資料型別外 其他型別必須標上方向  in  out  inout  
AIDL介面中只支援方法  不支援宣告靜態常量
在使用aidl時  最好把所有aidl檔案都放在一個包中  這樣方便複製到客戶端 
其實所有的跨程序物件傳遞都是物件的序列化與反序列化  所以必須包名一致

現在加入有這樣一個需求 如果服務端是 圖書館新增和檢視書的任務   客戶端可以檢視和新增書   這時候需要新增一個功能  當服務端每添加了一本書 
需要通知客戶端註冊使用者  有一本新書上架了   這個功能如何實現?
想想可知  這是一個觀察者模式  如果在同一程序中很容易實現,只需要在服務端中的程式碼中維護一個集合 裡面放的是註冊監聽的使用者  然後使用者需要實現一個新書到來的回撥介面
當有新書上架時 遍歷這個集合  呼叫每個註冊者的介面方法  即可實現  
現在我們是跨程序通訊   所以自然不能如此簡單了  但也不是很複雜 想一想  其實就是把以往的介面定義 變成了aidl介面定義  然後其他的一樣即可  
但是這樣還是存在一個問題  如果註冊了listener  我們又想解除註冊  是不是在客戶端傳入listener物件 在服務端把它移除就可以呢? 
其實是不可以的   因為這是跨程序的  所以物件並不是真正的傳遞  只是在另一個程序中重新建立了一個一樣的物件  記憶體地址不同 所以根本不是同一個物件
所以是不可以的   如果要解決這個問題  需要使用RemoteCallbackList 類  不要使用CopyWriteArrayList  
在RemoteCallBackList中封裝了一個Map 專門用來儲存所有的AIDL回撥  key為IBinder  value是CallBack   使用IBinder 來區別不同的物件  ,
因為跨程序傳輸時會產生很多個不同的物件  但這些物件的底層的Binder都是同一個物件  所以可以  
在使用RemoteCallBackList時 add 變為 register  remove 變為 unregister  遍歷的時候需要先 beginBroadcast  這個方法同時也獲取集合大小 
獲取集合中物件使用 getBoardCastItem(i)  最後不要忘記finishBoardCast方法   

還有一個情況  由於onServiceConnected方法 是在主執行緒執行的  如果在這裡執行服務端的耗時程式碼  會ANR  所以需要開啟一個子執行緒執行  
同理在服務端中 也不可以執行客戶端的耗時程式  
總結起來就是 在執行其他程序的耗時程式時  都需要開啟另外的執行緒防止阻塞UI執行緒  如果要訪問UI相關的東西  使用handler  

為了程式的健壯性  有時候Binder可能意外死亡  這時候需要重連服務  有2種方法:
1.在onServiceDisconnected方法中  重連服務  
2. 給Binder註冊DeathRecipient監聽  當binder死亡時 我們可以收到回撥  這時候我們可以重連遠端服務

最後有時候我們不想所有的程式都可以訪問我們的遠端服務  所以可以給服務設定許可權和過濾:
1.我們在onbind中進行校驗 用某種方式 如果驗證不通過那麼就直接返回null 
2.我們可以在服務端的AndroidMiniFest.xml中  設定所需的許可權  <permission android:name="aaaaaa" android:protectionLevel="normal"/>
然後在onbind中 檢查是否有這個許可權了  如果沒有那麼直接返回null即可  判斷方法如下  :
  1. int check = checkCallingOrSelfPermission("aaa");  
  2.             if(check== PackageManager.PERMISSION_DENIED){  
  3.                 returnnull;  
  4.             }  


3.可以在onTransact方法中 進行許可權驗證  如果驗證失敗直接返回false  可以採用permission方法驗證  還可以用Uid和Pid驗證  很多方法  

其中宣告許可權與 新增許可權的方式 是   在Service所在的AndroidMinifest中 宣告許可權 
比如    <permission android:name="com.yangsheng.ydzd_lb.myaidlpro.book" android:protectionLevel="normal"></permission>
然後在 需要遠端呼叫的 app中新增 這個許可權 <uses-permission android:name="com.yangsheng.ydzd_lb.myaidlpro.book"/>  
這樣 就可以在  onbind中驗證許可權了

至此 AIDL  大體介紹完了   以後需要在使用中提升了


5.ContentProvider方式  實現對另一個應用程序開放provider資料的查詢
此方法使用起來也比較簡單  底層是對Binder的封裝 使之可以實現程序間通訊  使用方法如下  
1. 在需要共享資料的應用程序中建立一個ContentProvider類 重寫它的CRUD 和getType方法  在這幾個方法中呼叫對本應用程序資料的呼叫 
  然後在AndroidMinifest.xml檔案中宣告provider  
  
  1. <provider   
  2.             android:authorities="com.yangsheng.book"//這個是用來標識provider的唯一標識  路徑uri也是這個
  3.             android:name=".BookProdiver"
  4.             android:process=":remote_provider"/>   //此句為了建立多程序  正常不需要使用



2. 在需要獲取共享資料的應用程序中呼叫getContentResolver().crud方法  即可實現資料的查詢  


需要注意的問題:
1.關於 sqlite crud的各個引數的意義 
query函式 引數 
Cursor query(boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit)
第一個引數 distinct 英語單詞意思 獨特的   如果true 那麼返回的資料都是唯一的  意思就是實現查詢資料的去重 
第二個引數 table  表名
第三個引數 columns  要查詢的行的名字陣列  例如  new String[]{"id","name","sex"} 
第四個引數 selection 選擇語句  sql語句中where後面的語句  值用?代替  例如  "id=? and sex=?"
第五個引數 selectionArgs  對應第四個引數的 ?  例如  new String[]{"1","男"}
第六個引數 groupBy 用於分組  
第七個引數 having  篩選分組後的資料 
第八個引數 orderby 用於排序  desc/asc  升序和降序  例如  id desc / id asc 
最後一個引數 limit  用於限制查詢的資料的個數  預設不限制
其他幾個函式 根據query函式的引數猜想即可

2.由於每次ipc操作 都是靠uri來區別 想要獲取的資料位置  所以provider在調取資料的時候根據uri並不知道要查詢的資料是在哪個位置
 所以我們可以通過 UriMatcher 這個類來給每個uri標上號 根據編號 對應適當的位置   例如:
  1. publicstaticfinalint BOOK_CODE = 0;  
  2.             publicstaticfinalint USER_CODE = 1;  
  3.             publicstatic UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);  
  4.             static {  
  5.                 matcher.addURI("book uri""book", BOOK_CODE);  
  6.                 matcher.addURI("user uri""user", USER_CODE);  
  7.             }  
  8.             這樣我們可以通過 下面這個樣子來獲取位置(此處是表名 其他型別也一樣)  
  9.             private String getTableName(Uri uri) {  
  10.                 switch (matcher.match(uri)) {  
  11.                     case BOOK_CODE:  
  12.                         return"bookTable";  
  13.                     case USER_CODE:  
  14.                         return"userTable";  
  15.                 }  
  16.                 return"";  
  17.             }  



3.另外ContentProvider除了crud四個方法外,還支援自定義呼叫  通過ContentProvider 和ContentResolver的 call方法  來實現


6.Socket方法實現Ipc   這種方式也可以實現 但是不常用  
需要許可權  
<uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
這種方式需要一個服務端socket 和一個客戶端socket  建立連線後 通過流迴圈獲取訊息即可  
1.在服務端開啟一個serverSocket 不斷獲取客戶端連線  注意要在子執行緒中開啟 

  1. ServerSocket serverSocket = new ServerSocket(8688);  
  2.         while(isActive) { //表示服務生存著
  3.                 try {  
  4.                     final Socket client = serverSocket.accept();  //不斷獲取客戶端連線
  5.                     System.out.println("---服務端已獲取客戶端連線");  
  6.                     new Thread(){  
  7.                         @Override
  8.                         publicvoid run() {  
  9.                             try {  
  10.                                 dealWithMessageFromClient(client);  //處理客戶端的訊息 就是開啟一個執行緒迴圈獲取out 和 in  流 進行通訊
  11.                             } catch (IOException e) {  
  12.                                 e.printStackTrace();  
  13.                             }  
  14.                         }  
  15.                     }.start();  
  16.                 } catch (IOException e) {  
  17.                     e.printStackTrace();  
  18.                 }  
  19.             }  


2.在客戶端開啟一個執行緒 使用ip和埠號連線服務端socket  連線成功後 一樣 開啟子執行緒 迴圈獲取訊息 處理

                      
  1. Socket socket = null;  
  2.                 while(socket==null){  //失敗重連
  3.                     try {  
  4.                         socket = new Socket("localhost",8688);  
  5.                         out = new PrintWriter(socket.getOutputStream(),true);  
  6.                         handler.sendEmptyMessage(1);  
  7.                         final Socket finalSocket = socket;  
  8.                         new Thread(){  
  9.                             @Override
  10.                             publicvoid run() {  
  11.                                 try {  
  12.                                     reader = new BufferedReader(new InputStreamReader(finalSocket.getInputStream()));  
  13.                                 } catch (IOException e) {  
  14.                                     e.printStackTrace();  
  15.                                 }  
  16.                                 while(!MainActivity.this.isFinishing()){  //迴圈獲取訊息  這裡必須用 迴圈 否則 只能獲取一條訊息 服務端也一樣
  17.                                     try {  
  18.                                         String msg = reader.readLine();  
  19.                                         System.out.println("---"+msg);  
  20.                                         if (msg!=null){  
  21.                                             handler.sendMessage(handler.obtainMessage(2,msg));  
  22.                                         }  
  23.                                     } catch (IOException e) {  
  24.                                         e.printStackTrace();  
  25.                                     }  
  26.                                 }  
  27.                             }  
  28.                         }.start();  
  29.                     } catch (IOException e) {  
  30.                         SystemClock.sleep(1000);  
  31.                         e.printStackTrace();  
  32.                     }  
  33.                 }  



7.Binder 連線池的使用  很好用
我們在android中程序間通訊 一般都使用 AIDL實現 因為它強大  但是普通的使用方法每次使用AIDL 都需要開啟一個服務  如果有多個AIDL請求 那豈不是要開啟很多個服務 
這明顯是不可以的  比如你讓你使用者的手機 發現你這一個應用程式綁定了10個服務  那是極差的  所以 我們在多個AIDL 請求的時候可以使用Binder連線池技術 
只開啟一個服務  根據需要獲取的AIDL不同 轉化成需要的AIDL 介面 執行不同的方法  
實現的基本原理  就是在onbind中返回一個BinderPool 介面 這個介面有個方法 可以根據不同的標誌位返回不同的aidl介面  這樣我們在asInTerface之後呼叫哪個方法
傳入標誌位即可返回需要的aidl介面 
說起來簡單 讓我們來實現一個試試吧
1.假設原來有2個AIDL介面需要實現(可以擴充套件成多個)  在服務端建立好AIDL檔案  並且建立一個IBinderPool aidl介面  只有一個查詢binder的方法 用於查詢需要的binder
interface IBinderPool {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
IBinder queryBinder(int code);  //此方法返回Ibinder  用於轉化成需要的AIDL介面
}
2.在服務端 onbind方法中返回 IBinderPool的實現類  實現query方法 按照傳入的code 返回需要的ibinder
@Override
public IBinder onBind(Intent intent) {
return iBinderPool;
}


private Binder iBinderPool = new IBinderPool.Stub() {


@Override
public IBinder queryBinder(int code) throws RemoteException {
switch (code) {
case 1:
return new IBookManger.Stub() {


@Override
public void getBook() throws RemoteException {
System.out.println("--->book");
}
};
case 2:
return new IPersonManager.Stub() {


@Override
public void getPerson() throws RemoteException {
System.out.println("---->person");
}
};


}
return null;
}
};
3.客戶端實現一個BinderPool類  這個類主要是封裝了 AIDL的一些實現方法 方便呼叫罷了 其中 涉及到一個可以實現同步機制的類
CountDownLatch  這個類 當他的值 不是0的時候  執行了await方法後會使方法一直停在await處  不進行 直到他的值變成了0 才可以繼續執行  
也就是說 當執行了await方法  這個執行緒就會阻塞 等待這個數值變到0後繼續執行  
而在BinderPool中的應用場景是這樣的   
 private void connectService(){
countDownLatch = new CountDownLatch(1);  //實現同步機制 
Intent intent = new Intent();
intent.setClass(ctx,MyService.class);
ctx.bindService(intent,connection,Context.BIND_AUTO_CREATE);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
首先為什麼在這裡要使用同步機制  我們要搞清楚 讓我們看這個方法呼叫的時機 :
binderPool = BinderPool.getInstance(MainActivity.this);  //connectService方法 是在這個方法中呼叫的  
                IBinder iBinder = binderPool.queryBinder(2);
                iPersonManager = IPersonManager.Stub.asInterface(iBinder);
                try {
                    iPersonManager.getPerson();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
因為我們最終的目的是在bind服務  連線到遠端服務之後獲取到 binderPool物件呼叫它的 binderPool.queryBinder(2) 方法  如果不加同步機制
非同步執行  就有可能在 connectService方法 執行完之後  執行IBinder iBinder = binderPool.queryBinder(2);這行程式碼的時候binderPool物件還
沒有被賦值  這樣就會產生問題  所以我們讓 connectService方法 阻塞  當BinderPool中的 binderPool物件賦值之後 讓CountDownLatch的值countDown到0

這樣 connectService方法就會繼續執行 然後執行下一行程式碼了 


總結起來
當僅僅是跨程序的四大元件間的傳遞資料時 使用Bundle就可以  簡單方便  
當要共享一個應用程式的內部資料的時候  使用ContentProvider實現比較方便  
當併發程度不高  也就是偶爾訪問一次那種 程序間通訊 用Messenger就可以  
當設計網路資料的共享時  使用socket 
當需求比較複雜  高併發 並且還要求實時通訊 而且有RPC需求時  就得使用AIDL了 
檔案共享的方法用於一些快取共享 之類的功能