[TOC]
1.使用Bundle ----> 用于android四大組件間的進程間通信
android的四大組件都可使用Bundle傳遞數(shù)據(jù) 所以如果要實現(xiàn)四大組件間的進程間通信 完全可以使用Bundle來實現(xiàn) 簡單方便
2.使用文件共享 ---->用于單線程讀寫
這種方式在單線程讀寫的時候比較好用 如果有多個線程并發(fā)讀寫的話需要限制線程的同步讀寫
另外 SharePreference是個特例 它底層基于xml實現(xiàn) 但是系統(tǒng)對它的讀寫會基于緩存,也就是說再多進程模式下就變得不那么可靠了辽狈,有很大幾率丟失數(shù)據(jù)
3.使用Messenger ---->用于可存放在message中的數(shù)據(jù)的傳遞
使用這個方式可以在不同進程間傳遞message對象 這是一種輕量級的IPC方案 當傳遞的對象可以放入message中時 可以考慮用這種方式 但是msg.object最好不要放
因為不一定可以序列化
使用它的步驟如下:
假設這樣一個需求 需要在客戶端A發(fā)送消息給服務端B接受 然后服務端B再回復給客戶端A
1. 首先是客戶端A發(fā)送消息給服務端B 所以在客戶端A中 聲明一個Handler用來接受消息 并創(chuàng)建一個Messenger對象 用Handler作為參數(shù)構造 然后onBinder方法返回messenger.getBinder() 即可
public class MyServiceA extends Service {
private class MessageHandler extends Handler{ //創(chuàng)建的接受消息的handler
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 1:
Bundle bundle = msg.getData();
String str = bundle.getString("aaa");
System.out.println("----"+str);
Messenger replyTo = msg.replyTo; //此處往下是用來回復消息給客戶端A的
Message replyMsg = Message.obtain(null,2);
Bundle bundle1 = new Bundle();
bundle1.putString("bbb","remote222給主進程回復消息啦");
replyMsg.setData(bundle1);
try {
replyTo.send(replyMsg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
super.handleMessage(msg);
}
}
Messenger messenger = new Messenger(new MessageHandler());
public MyServiceA() {
}
public IBinder onBind(Intent intent) {
return messenger.getBinder();
}
}
2.在客戶端A自然是需要發(fā)送消息給服務端B的 所以需要在服務綁定完成之后 獲取到binder對象 之后用該對象構造一個Messenger對象 然后用messenger發(fā)送
消息給服務端即可 代碼如下 :
public void onServiceConnected(ComponentName name, IBinder service) {
Messenger messenger = new Messenger(service);
Message msg = Message.obtain(null,1);
Bundle bundle = new Bundle();
bundle.putString("aaa", "主進程給remote22進程發(fā)消息啦");
msg.setData(bundle);
msg.replyTo = mmessenger; //這行代碼用于客戶端A接收服務端請求 設置的消息接收者
try {
messenger.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
3.由于在服務端接收到了客戶端的消息還需要回復 所以在服務端代碼中獲取 msg中的replyTo對象 用這個對象發(fā)送消息給 客戶端即可
在客戶端需要創(chuàng)建一個handler和Messenger 將發(fā)送的msg.replyTo設置成Messenger對象 就可
4.AIDL android 接口定義語言 ---->主要用于調用遠程服務的方法的情況 還可以注冊接口
使用方法很簡單
在服務端定義aidl文件 自動生成java文件 然后在service中實現(xiàn)這個aidl 在onbind中返回這個對象
在客戶端把服務端的aidl文件完全復制過來 包名必須完全一致 在onServiceConnected方法 中 把 Ibinder對象 用asInterface方法轉化成 aidl對象
然后調用方法即可
需要注意的地方:
在aidl文件中并不是支持所有類型
僅支持如下6種類型:
基本數(shù)據(jù)類型---- int long char boolean double
String charSequence
List 只支持ArrayList CopyOnWriteArrayList也可以孟岛。。 里面元素也必須被aidl支持
Map 只支持HashMap ConCurrentHashMap也可以 里面元素也必須支持aidl
Parcelable 所有實現(xiàn)了此接口的對象
AIDL 所有的AIDL接口 因此 如果需要使用接口 必須使用AIDL接口
其中自定義的類型和AIDL對象必須顯示import進來 不管是不是在一個包中
如果AIDL文件中用到了自定義的Parcelable對象 必須創(chuàng)建同名的AIDL文件 并聲明為Parcelable類型
AIDL文件中除了基本數(shù)據(jù)類型外 其他類型必須標上方向 in out inout
AIDL接口中只支持方法 不支持聲明靜態(tài)常量
在使用aidl時 最好把所有aidl文件都放在一個包中 這樣方便復制到客戶端
其實所有的跨進程對象傳遞都是對象的序列化與反序列化 所以必須包名一致
現(xiàn)在加入有這樣一個需求 如果服務端是 圖書館添加和查看書的任務 客戶端可以查看和添加書 這時候需要添加一個功能 當服務端每添加了一本書
需要通知客戶端注冊用戶 有一本新書上架了 這個功能如何實現(xiàn)?
想想可知 這是一個觀察者模式 如果在同一進程中很容易實現(xiàn)淤刃,只需要在服務端中的代碼中維護一個集合 里面放的是注冊監(jiān)聽的用戶 然后用戶需要實現(xiàn)一個新書到來的回調接口
當有新書上架時 遍歷這個集合 調用每個注冊者的接口方法 即可實現(xiàn)
現(xiàn)在我們是跨進程通信 所以自然不能如此簡單了 但也不是很復雜 想一想 其實就是把以往的接口定義 變成了aidl接口定義 然后其他的一樣即可
但是這樣還是存在一個問題 如果注冊了listener 我們又想解除注冊 是不是在客戶端傳入listener對象 在服務端把它移除就可以呢摸恍?
其實是不可以的 因為這是跨進程的 所以對象并不是真正的傳遞 只是在另一個進程中重新創(chuàng)建了一個一樣的對象 內存地址不同 所以根本不是同一個對象
所以是不可以的 如果要解決這個問題 需要使用RemoteCallbackList 類 不要使用CopyWriteArrayList
在RemoteCallBackList中封裝了一個Map 專門用來保存所有的AIDL回調 key為IBinder value是CallBack 使用IBinder 來區(qū)別不同的對象 ,
因為跨進程傳輸時會產(chǎn)生很多個不同的對象 但這些對象的底層的Binder都是同一個對象 所以可以
在使用RemoteCallBackList時 add 變?yōu)?register remove 變?yōu)?unregister 遍歷的時候需要先 beginBroadcast 這個方法同時也獲取集合大小
獲取集合中對象使用 getBoardCastItem(i) 最后不要忘記finishBoardCast方法
還有一個情況 由于onServiceConnected方法 是在主線程執(zhí)行的 如果在這里執(zhí)行服務端的耗時代碼 會ANR 所以需要開啟一個子線程執(zhí)行
同理在服務端中 也不可以運行客戶端的耗時程序
總結起來就是 在執(zhí)行其他進程的耗時程序時 都需要開啟另外的線程防止阻塞UI線程 如果要訪問UI相關的東西 使用handler
為了程序的健壯性 有時候Binder可能意外死亡 這時候需要重連服務 有2種方法:
1.在onServiceDisconnected方法中 重連服務
2. 給Binder注冊DeathRecipient監(jiān)聽 當binder死亡時 我們可以收到回調 這時候我們可以重連遠程服務
最后有時候我們不想所有的程序都可以訪問我們的遠程服務 所以可以給服務設置權限和過濾:
1.我們在onbind中進行校驗 用某種方式 如果驗證不通過那么就直接返回null
2.我們可以在服務端的AndroidManiFest.xml中 設置所需的權限
<permission android:name="aaaaaa" android:protectionLevel="normal"/>
然后在onbind中 檢查是否有這個權限了 如果沒有那么直接返回null即可 判斷方法如下 :
int check = checkCallingOrSelfPermission("aaa");
if(check== PackageManager.PERMISSION_DENIED){
return null;
}
3.可以在onTransact方法中 進行權限驗證 如果驗證失敗直接返回false 可以采用permission方法驗證 還可以用Uid和Pid驗證 很多方法
其中聲明權限與 添加權限的方式 是 在Service所在的AndroidMinifest中 聲明權限
比如
<permission android:name="com.zhxh.android_lb.myaidlpro.book" android:protectionLevel="normal"></permission>
然后在 需要遠程調用的 app中添加 這個權限
<uses-permission android:name="com.zhxh.android_lb.myaidlpro.book"/>
這樣 就可以在 onbind中驗證權限了
至此 AIDL 大體介紹完了 以后需要在使用中提升了
aidl demo 下載 : https://github.com/zhxhcoder/IPCdemo
5.ContentProvider方式 實現(xiàn)對另一個應用進程開放provider數(shù)據(jù)的查詢
此方法使用起來也比較簡單 底層是對Binder的封裝 使之可以實現(xiàn)進程間通信 使用方法如下
1. 在需要共享數(shù)據(jù)的應用進程中建立一個ContentProvider類 重寫它的CRUD 和getType方法 在這幾個方法中調用對本應用進程數(shù)據(jù)的調用
然后在AndroidMinifest.xml文件中聲明provider
<provider
android:authorities="com.zhxh.book" //這個是用來標識provider的唯一標識 路徑uri也是這個
android:name=".BookProdiver"
android:process=":remote_provider"/> //此句為了創(chuàng)建多進程 正常不需要使用
2. 在需要獲取共享數(shù)據(jù)的應用進程中調用getContentResolver().crud方法 即可實現(xiàn)數(shù)據(jù)的查詢
需要注意的問題:
1.關于 sqlite crud的各個參數(shù)的意義
query函數(shù) 參數(shù)
Cursor query(boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit)
第一個參數(shù) distinct 英語單詞意思 獨特的 如果true 那么返回的數(shù)據(jù)都是唯一的 意思就是實現(xiàn)查詢數(shù)據(jù)的去重
第二個參數(shù) table 表名
第三個參數(shù) columns 要查詢的行的名字數(shù)組 例如 new String[]{"id","name","sex"}
第四個參數(shù) selection 選擇語句 sql語句中where后面的語句 值用璧南?代替 例如 "id=? and sex=?"
第五個參數(shù) selectionArgs 對應第四個參數(shù)的 ? 例如 new String[]{"1","男"}
第六個參數(shù) groupBy 用于分組
第七個參數(shù) having 篩選分組后的數(shù)據(jù)
第八個參數(shù) orderby 用于排序 desc/asc 升序和降序 例如 id desc / id asc
最后一個參數(shù) limit 用于限制查詢的數(shù)據(jù)的個數(shù) 默認不限制
其他幾個函數(shù) 根據(jù)query函數(shù)的參數(shù)猜想即可
2.由于每次ipc操作 都是靠uri來區(qū)別 想要獲取的數(shù)據(jù)位置 所以provider在調取數(shù)據(jù)的時候根據(jù)uri并不知道要查詢的數(shù)據(jù)是在哪個位置
所以我們可以通過 UriMatcher 這個類來給每個uri標上號 根據(jù)編號 對應適當?shù)奈恢? 例如:
public static final int BOOK_CODE = 0;
public static final int USER_CODE = 1;
public static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
matcher.addURI("book uri", "book", BOOK_CODE);
matcher.addURI("user uri", "user", USER_CODE);
}
這樣我們可以通過 下面這個樣子來獲取位置(此處是表名 其他類型也一樣)
private String getTableName(Uri uri) {
switch (matcher.match(uri)) {
case BOOK_CODE:
return "bookTable";
case USER_CODE:
return "userTable";
}
return "";
}
3.另外ContentProvider除了crud四個方法外掌逛,還支持自定義調用 通過ContentProvider 和ContentResolver的 call方法 來實現(xiàn)
ContentProviderdemo下載 :https://github.com/zhxhcoder/IPCdemo
6.Socket方法實現(xiàn)Ipc 這種方式也可以實現(xiàn) 但是不常用
需要權限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
這種方式需要一個服務端socket 和一個客戶端socket 建立連接后 通過流循環(huán)獲取消息即可
1.在服務端開啟一個serverSocket 不斷獲取客戶端連接 注意要在子線程中開啟
ServerSocket serverSocket = new ServerSocket(8688);
while(isActive) { //表示服務生存著
try {
final Socket client = serverSocket.accept(); //不斷獲取客戶端連接
System.out.println("---服務端已獲取客戶端連接");
new Thread(){
@Override
public void run() {
try {
dealWithMessageFromClient(client); //處理客戶端的消息 就是開啟一個線程循環(huán)獲取out 和 in 流 進行通信
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
} catch (IOException e) {
e.printStackTrace();
}
}
2.在客戶端開啟一個線程 使用ip和端口號連接服務端socket 連接成功后 一樣 開啟子線程 循環(huán)獲取消息 處理
Socket socket = null;
while(socket==null){ //失敗重連
try {
socket = new Socket("localhost",8688);
out = new PrintWriter(socket.getOutputStream(),true);
handler.sendEmptyMessage(1);
final Socket finalSocket = socket;
new Thread(){
@Override
public void run() {
try {
reader = new BufferedReader(new InputStreamReader(finalSocket.getInputStream()));
} catch (IOException e) {
e.printStackTrace();
}
while(!MainActivity.this.isFinishing()){ //循環(huán)獲取消息 這里必須用 循環(huán) 否則 只能獲取一條消息 服務端也一樣
try {
String msg = reader.readLine();
System.out.println("---"+msg);
if (msg!=null){
handler.sendMessage(handler.obtainMessage(2,msg));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
} catch (IOException e) {
SystemClock.sleep(1000);
e.printStackTrace();
}
}
7.Binder 連接池的使用 很好用
我們在android中進程間通信 一般都使用 AIDL實現(xiàn) 因為它強大 但是普通的使用方法每次使用AIDL 都需要開啟一個服務 如果有多個AIDL請求 那豈不是要開啟很多個服務
這明顯是不可以的 比如你讓你用戶的手機 發(fā)現(xiàn)你這一個應用程序綁定了10個服務 那是極差的 所以 我們在多個AIDL 請求的時候可以使用Binder連接池技術
只開啟一個服務 根據(jù)需要獲取的AIDL不同 轉化成需要的AIDL 接口 執(zhí)行不同的方法
實現(xiàn)的基本原理 就是在onbind中返回一個BinderPool 接口 這個接口有個方法 可以根據(jù)不同的標志位返回不同的aidl接口 這樣我們在asInTerface之后調用哪個方法
傳入標志位即可返回需要的aidl接口
說起來簡單 讓我們來實現(xiàn)一個試試吧
1.假設原來有2個AIDL接口需要實現(xiàn)(可以擴展成多個) 在服務端建立好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的實現(xiàn)類 實現(xiàn)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.客戶端實現(xiàn)一個BinderPool類 這個類主要是封裝了 AIDL的一些實現(xiàn)方法 方便調用罷了 其中 涉及到一個可以實現(xiàn)同步機制的類
CountDownLatch 這個類 當他的值 不是0的時候 執(zhí)行了await方法后會使方法一直停在await處 不進行 直到他的值變成了0 才可以繼續(xù)執(zhí)行
也就是說 當執(zhí)行了await方法 這個線程就會阻塞 等待這個數(shù)值變到0后繼續(xù)執(zhí)行
而在BinderPool中的應用場景是這樣的
private void connectService(){
countDownLatch = new CountDownLatch(1); //實現(xiàn)同步機制
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) 方法 如果不加同步機制
異步執(zhí)行 就有可能在 connectService方法 執(zhí)行完之后 執(zhí)行IBinder iBinder = binderPool.queryBinder(2);這行代碼的時候binderPool對象還
沒有被賦值 這樣就會產(chǎn)生問題 所以我們讓 connectService方法 阻塞 當BinderPool中的 binderPool對象賦值之后 讓CountDownLatch的值countDown到0
這樣 connectService方法就會繼續(xù)執(zhí)行 然后執(zhí)行下一行代碼了
BinderPool demo下載 :https://github.com/zhxhcoder/IPCdemo
最后針對這幾種IPC通信方式分析一下優(yōu)缺點
1.bundle :
簡單易用 但是只能傳輸Bundle支持的對象 常用于四大組件間進程間通信
2.文件共享:
簡單易用 但不適合在高并發(fā)的情況下 并且讀取文件需要時間 不能即時通信 常用于并發(fā)程度不高 并且實時性要求不高的情況
3.AIDL :
功能強大 支持一對多并發(fā)通信 支持即時通信 但是使用起來比其他的復雜 需要處理好多線程的同步問題 常用于一對多通信 且有RPC 需求的場合(服務端和客戶端通信)
4.Messenger :
功能一般 支持一對多串行通信 支持實時通信 但是不能很好處理高并發(fā)情況 只能傳輸Bundle支持的類型 常用于低并發(fā)的無RPC需求一對多的場合
5.ContentProvider :
在數(shù)據(jù)源訪問方面功能強大 支持一對多并發(fā)操作 可擴展call方法 可以理解為約束版的AIDL 提供CRUD操作和自定義函數(shù) 常用于一對多的數(shù)據(jù)共享場合
6.Socket :
功能強大 可以通過網(wǎng)絡傳輸字節(jié)流 支持一對多并發(fā)操作 但是實現(xiàn)起來比較麻煩 不支持直接的RPC 常用于網(wǎng)絡數(shù)據(jù)交換
總結
當僅僅是跨進程的四大組件間的傳遞數(shù)據(jù)時 使用Bundle就可以 簡單方便
當要共享一個應用程序的內部數(shù)據(jù)的時候 使用ContentProvider實現(xiàn)比較方便
當并發(fā)程度不高 也就是偶爾訪問一次那種 進程間通信 用Messenger就可以
當設計網(wǎng)絡數(shù)據(jù)的共享時 使用socket
當需求比較復雜 高并發(fā) 并且還要求實時通信 而且有RPC需求時 就得使用AIDL了
文件共享的方法用于一些緩存共享 之類的功能