Android中實現(xiàn)IPC的幾種方式詳細(xì)分析及比較

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方案 當(dāng)傳遞的對象可以放入message中時 可以考慮用這種方式 但是msg.object最好不要放因為不一定可以序列化 使用它的步驟如下:假設(shè)這樣一個需求 需要在客戶端A發(fā)送消息給服務(wù)端B接受 然后服務(wù)端B再回復(fù)給客戶端A

  • 首先是客戶端A發(fā)送消息給服務(wù)端B 所以在客戶端A中 聲明一個Handler用來接受消息 并創(chuàng)建一個Messenger對象 用Handler作為參數(shù)構(gòu)造 然后onBinder方法返回messenger.getBinder() 即可

  • 在客戶端A自然是需要發(fā)送消息給服務(wù)端B的 所以需要在服務(wù)綁定完成之后 獲取到binder對象 之后用該對象構(gòu)造一個Messenger對象 然后用messenger發(fā)送消息給服務(wù)端即可

  • 由于在服務(wù)端接收到了客戶端的消息還需要回復(fù) 所以在服務(wù)端代碼中獲取 msg中的replyTo對象 用這個對象發(fā)送消息給 客戶端即可 在客戶端需要創(chuàng)建一個handler和Messenger 將發(fā)送的msg.replyTo設(shè)置成Messenger對象

4.AIDL android 接口定義語言 ---->主要用于調(diào)用遠(yuǎn)程服務(wù)的方法的情況 還可以注冊接口

使用方法很簡單俭茧,在服務(wù)端定義aidl文件 自動生成java文件串远,然后在service中實現(xiàn)這個aidl,在onbind中返回這個對象托享,在客戶端把服務(wù)端的aidl文件完全復(fù)制過來其兴,包名必須完全一致顶瞒,在onServiceConnected方法 中 把Ibinder對象用asInterface方法轉(zhuǎn)化成 aidl對象然后調(diào)用方法即可。

需要注意的地方:

在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ù)類型外,其他類型必須標(biāo)上方向(in常柄、out鲤氢、inout)
  • AIDL接口中只支持方法 不支持聲明靜態(tài)常量。
  • 在使用aidl時西潘,最好把所有aidl文件都放在一個包中卷玉,這樣方便復(fù)制到客戶端,其實所有的跨進程對象傳遞都是對象的序列化與反序列化喷市,所以必須包名一致相种。
場景用例

現(xiàn)在加入有這樣一個需求 如果服務(wù)端是 圖書館添加和查看書的任務(wù) 客戶端可以查看和添加書 這時候需要添加一個功能 當(dāng)服務(wù)端每添加了一本書 需要通知客戶端注冊用戶 有一本新書上架了 這個功能如何實現(xiàn)?想想可知 這是一個觀察者模式 如果在同一進程中很容易實現(xiàn)品姓,只需要在服務(wù)端中的代碼中維護一個集合 里面放的是注冊監(jiān)聽的用戶 然后用戶需要實現(xiàn)一個新書到來的回調(diào)接口當(dāng)有新書上架時 遍歷這個集合 調(diào)用每個注冊者的接口方法 即可實現(xiàn) 現(xiàn)在我們是跨進程通信 所以自然不能如此簡單了 但也不是很復(fù)雜 想一想 其實就是把以往的接口定義 變成了aidl接口定義 然后其他的一樣即可 但是這樣還是存在一個問題 如果注冊了listener 我們又想解除注冊 是不是在客戶端傳入listener對象 在服務(wù)端把它移除就可以呢寝并? 其實是不可以的 因為這是跨進程的 所以對象并不是真正的傳遞 只是在另一個進程中重新創(chuàng)建了一個一樣的對象 內(nèi)存地址不同 所以根本不是同一個對象所以是不可以的 如果要解決這個問題 需要使用RemoteCallbackList 類 不要使用CopyWriteArrayList 在RemoteCallBackList中封裝了一個Map 專門用來保存所有的AIDL回調(diào) 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í)行服務(wù)端的耗時代碼 會ANR 所以需要開啟一個子線程執(zhí)行 同理在服務(wù)端中 也不可以運行客戶端的耗時程序 總結(jié)起來就是 在執(zhí)行其他進程的耗時程序時 都需要開啟另外的線程防止阻塞UI線程 如果要訪問UI相關(guān)的東西 使用handler

為了程序的健壯性 有時候Binder可能意外死亡 這時候需要重連服務(wù) 有2種方法:

  • 1.在onServiceDisconnected方法中衬潦,重連服務(wù)。
  • 2.給Binder注冊DeathRecipient監(jiān)聽植酥,當(dāng)binder死亡時镀岛,我們可以收到回調(diào) 這時候我們可以重連遠(yuǎn)程服務(wù)。

最后有時候我們不想所有的程序都可以訪問我們的遠(yuǎn)程服務(wù) 所以可以給服務(wù)設(shè)置權(quán)限和過濾:

第一種方法:在onBind中進行驗證(permission驗證)

首先在AndroidMenifest中聲明所需權(quán)限

<permission android:name="com.example.test1.permission.ACCESS_BOOK_SERVICE"android:protectionLevel="normal" />
<uses-permission android:name="com.example.test1.permission.ACCESS_BOOK_SERVICE" />

驗證

@Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
        Log.d(TAG, "onbind check=" + check);
        if (check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return mBinder;
    }
第二種方法:在onTransact中進行驗證(包名驗證)
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
            // 權(quán)限驗證
            int check = checkCallingOrSelfPermission("com.example.test1.permission.ACCESS_BOOK_SERVICE");
            L.d("check:"+check);
            if(check==PackageManager.PERMISSION_DENIED){
                L.d("Binder 權(quán)限驗證失敗");
                return false;
            }
            // 包名驗證
            String packageName=null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if(packages!=null && packages.length>0){
                packageName = packages[0];
            }
            if(!packageName.startsWith("com.example")){
                L.d("包名驗證失敗");
                return false;

            }
            return super.onTransact(code, data, reply, flags);
        };

5.ContentProvider方式 實現(xiàn)對另一個應(yīng)用進程開放provider數(shù)據(jù)的查詢

此方法使用起來也比較簡單 底層是對Binder的封裝 使之可以實現(xiàn)進程間通信友驮,使用方法如下:

  1. 在需要共享數(shù)據(jù)的應(yīng)用進程中建立一個ContentProvider類漂羊,重寫它的CRUD 和getType方法,在這幾個方法中調(diào)用對本應(yīng)用進程數(shù)據(jù)的調(diào)用喊儡,然后在AndroidMinifest.xml文件中聲明provider
 <provider 
        android:authorities="com.yangsheng.book"  //這個是用來標(biāo)識provider的唯一標(biāo)識  路徑uri也是這個
        android:name=".BookProdiver"
        android:process=":remote_provider"/>   //此句為了創(chuàng)建多進程  正常不需要使用
  1. 在需要獲取共享數(shù)據(jù)的應(yīng)用進程中調(diào)用getContentResolver().crud方法 即可實現(xiàn)數(shù)據(jù)的查詢

需要注意的問題:
1.關(guān)于 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,要查詢的行的名字?jǐn)?shù)組 例如 new String[]{"id","name","sex"}
第四個參數(shù) selection 選擇語句 sql語句中where后面的語句 值用算柳?代替 例如 "id=? and sex=?"
第五個參數(shù) selectionArgs 對應(yīng)第四個參數(shù)的 ? 例如 new String[]{"1","男"}
第六個參數(shù) groupBy 用于分組
第七個參數(shù) having 篩選分組后的數(shù)據(jù)
第八個參數(shù) orderby 用于排序 desc/asc 升序和降序 例如 id desc / id asc
最后一個參數(shù) limit 用于限制查詢的數(shù)據(jù)的個數(shù) 默認(rèn)不限制其他幾個函數(shù) 根據(jù)query函數(shù)的參數(shù)猜想即可低淡。

2.由于每次ipc操作 都是靠uri來區(qū)別 想要獲取的數(shù)據(jù)位置 所以provider在調(diào)取數(shù)據(jù)的時候根據(jù)uri并不知道要查詢的數(shù)據(jù)是在哪個位置,所以我們可以通過 UriMatcher 這個類來給每個uri標(biāo)上號 根據(jù)編號 對應(yīng)適當(dāng)?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四個方法外,還支持自定義調(diào)用 通過ContentProvider 和ContentResolver的 call方法 來實現(xiàn)

6.Socket方法實現(xiàn)Ipc 這種方式也可以實現(xiàn) 但是不常用

需要權(quán)限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
這種方式需要一個服務(wù)端socket 和一個客戶端socket 建立連接后 通過流循環(huán)獲取消息即可蔗蹋。
1.在服務(wù)端開啟一個serverSocket 不斷獲取客戶端連接 注意要在子線程中開啟

ServerSocket serverSocket = new ServerSocket(8688);
        while(isActive) { //表示服務(wù)生存著
                try {
                    final Socket client = serverSocket.accept();  //不斷獲取客戶端連接
                    System.out.println("---服務(wù)端已獲取客戶端連接");
                    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和端口號連接服務(wù)端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) 否則 只能獲取一條消息 服務(wù)端也一樣
                                    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 連接池的使用 很好用

有一種情況何荚,假如有多個業(yè)務(wù)模塊需要通過AIDL進程間通信,如果按照之前AIDL的實現(xiàn)方式猪杭,我們就需要創(chuàng)建對應(yīng)的多個Service餐塘。顯然這樣是不可取的,不僅耗費系統(tǒng)資源皂吮,而且讓應(yīng)用看上去很重量級戒傻。我們可以通過Binder連接池的方法解決以上問題。
實現(xiàn)步驟:

  1. 首先蜂筹,為每個業(yè)務(wù)模塊創(chuàng)建AIDL接口并實現(xiàn)此接口及其業(yè)務(wù)方法需纳。
  2. 創(chuàng)建IBinderPool的AIDL接口,定義IBinder queryBinder(int BinderCode)方法艺挪。外部通過調(diào)用此方法傳入對應(yīng)的code值來獲取對應(yīng)的Binder對象不翩。
  3. 創(chuàng)建BinderPoolService,通過new BinderPool.BinderPoolImpl實例化Binder對象闺属,通過onBind方法返回出去慌盯。
  4. 創(chuàng)建BinderPool類,單例模式掂器,在構(gòu)造方法中綁定Service,在onServiceConnected方法獲取到BinderPoolImpl對象俱箱,這個BinderPoolImpl類是BinderPool的內(nèi)部類国瓮,并實現(xiàn)了IBinderPool的業(yè)務(wù)方法。BinderPool類中向外暴露了queryBinder方法狞谱,這個方法其實調(diào)用的是BinderPoolImpl對象的queryBinder方法乃摹。

代碼連接:https://github.com/huivs12/IPCDemo2BinderPool.git

最后 總結(jié)了這么多IPC通信方式 那我們該如何選擇合適的IPC方式呢 針對這幾種IPC通信方式分析一下優(yōu)缺點
1.bundle :簡單易用 但是只能傳輸Bundle支持的對象 常用于四大組件間進程間通信
2.文件共享:簡單易用 但不適合在高并發(fā)的情況下 并且讀取文件需要時間 不能即時通信 常用于并發(fā)程度不高 并且實時性要求不高的情況
3.AIDL :功能強大 支持一對多并發(fā)通信 支持即時通信 但是使用起來比其他的復(fù)雜 需要處理好多線程的同步問題 常用于一對多通信 且有RPC 需求的場合(服務(wù)端和客戶端通信)
4.Messenger :功能一般 支持一對多串行通信 支持實時通信 但是不能很好處理高并發(fā)情況 只能傳輸Bundle支持的類型 常用于低并發(fā)的無RPC需求一對多的場合
5.ContentProvider :在數(shù)據(jù)源訪問方面功能強大 支持一對多并發(fā)操作 可擴展call方法 可以理解為約束版的AIDL 提供CRUD操作和自定義函數(shù) 常用于一對多的數(shù)據(jù)共享場合
6.Socket :功能強大 可以通過網(wǎng)絡(luò)傳輸字節(jié)流 支持一對多并發(fā)操作 但是實現(xiàn)起來比較麻煩 不支持直接的RPC 常用于網(wǎng)絡(luò)數(shù)據(jù)交換。

總結(jié)起來
當(dāng)僅僅是跨進程的四大組件間的傳遞數(shù)據(jù)時 使用Bundle就可以 簡單方便 當(dāng)要共享一個應(yīng)用程序的內(nèi)部數(shù)據(jù)的時候 使用ContentProvider實現(xiàn)比較方便 當(dāng)并發(fā)程度不高 也就是偶爾訪問一次那種 進程間通信 用Messenger就可以 當(dāng)設(shè)計網(wǎng)絡(luò)數(shù)據(jù)的共享時 使用socket 當(dāng)需求比較復(fù)雜 高并發(fā) 并且還要求實時通信 而且有RPC需求時 就得使用AIDL了 文件共享的方法用于一些緩存共享 之類的功能

最后附上自制的一個ipc通訊的demo跟衅,實現(xiàn)了AIDL和Messenger方式孵睬。
地址:http://git.oschina.net/elensliu/IPC_demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市伶跷,隨后出現(xiàn)的幾起案子掰读,更是在濱河造成了極大的恐慌,老刑警劉巖叭莫,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹈集,死亡現(xiàn)場離奇詭異,居然都是意外死亡雇初,警方通過查閱死者的電腦和手機拢肆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人郭怪,你說我怎么就攤上這事支示。” “怎么了鄙才?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵悼院,是天一觀的道長。 經(jīng)常有香客問我咒循,道長据途,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任叙甸,我火速辦了婚禮颖医,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘裆蒸。我一直安慰自己熔萧,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布僚祷。 她就那樣靜靜地躺著佛致,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辙谜。 梳的紋絲不亂的頭發(fā)上俺榆,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機與錄音装哆,去河邊找鬼罐脊。 笑死,一個胖子當(dāng)著我的面吹牛蜕琴,可吹牛的內(nèi)容都是我干的萍桌。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼凌简,長吁一口氣:“原來是場噩夢啊……” “哼上炎!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起雏搂,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤藕施,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后畔派,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铅碍,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年线椰,在試婚紗的時候發(fā)現(xiàn)自己被綠了胞谈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖烦绳,靈堂內(nèi)的尸體忽然破棺而出卿捎,到底是詐尸還是另有隱情,我是刑警寧澤径密,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布午阵,位于F島的核電站,受9級特大地震影響享扔,放射性物質(zhì)發(fā)生泄漏底桂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一惧眠、第九天 我趴在偏房一處隱蔽的房頂上張望籽懦。 院中可真熱鬧,春花似錦氛魁、人聲如沸暮顺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捶码。三九已至,卻和暖如春或链,著一層夾襖步出監(jiān)牢的瞬間惫恼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工株扛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尤筐,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓洞就,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掀淘。 傳聞我的和親對象是個殘疾皇子旬蟋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容