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

1.使用Bundle ----> 用于android四大組件間的進(jìn)程間通信

android的四大組件都可使用Bundle傳遞數(shù)據(jù),所以如果要實(shí)現(xiàn)四大組件間的進(jìn)程間通信,完全可以使用Bundle來實(shí)現(xiàn)簡單方便 。

2.使用文件共享 ---->用于單線程讀寫

這種方式在單線程讀寫的時(shí)候比較好用 如果有多個(gè)線程并發(fā)讀寫的話需要限制線程的同步讀寫 另外 SharePreference是個(gè)特例 它底層基于xml實(shí)現(xiàn) 但是系統(tǒng)對它的讀寫會(huì)基于緩存,也就是說再多進(jìn)程模式下就變得不那么可靠了,有很大幾率丟失數(shù)據(jù)

3.使用Messenger ---->用于可存放在message中的數(shù)據(jù)的傳遞

使用這個(gè)方式可以在不同進(jìn)程間傳遞message對象 這是一種輕量級的IPC方案 當(dāng)傳遞的對象可以放入message中時(shí) 可以考慮用這種方式 但是msg.object最好不要放因?yàn)椴灰欢梢孕蛄谢? 使用它的步驟如下:假設(shè)這樣一個(gè)需求 需要在客戶端A發(fā)送消息給服務(wù)端B接受 然后服務(wù)端B再回復(fù)給客戶端A

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

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

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

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

使用方法很簡單,在服務(wù)端定義aidl文件 自動(dòng)生成java文件,然后在service中實(shí)現(xiàn)這個(gè)aidl,在onbind中返回這個(gè)對象,在客戶端把服務(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 所有實(shí)現(xiàn)了此接口的對象。
  • AIDL 所有的AIDL接口,因此,如果需要使用接口,必須使用AIDL接口
  • 其中自定義的類型和AIDL對象必須顯示import進(jìn)來,不管是不是在一個(gè)包中。
  • 如果AIDL文件中用到了自定義的Parcelable對象,必須創(chuàng)建同名的AIDL文件,并聲明為Parcelable類型。
  • AIDL文件中除了基本數(shù)據(jù)類型外,其他類型必須標(biāo)上方向(in、out、inout)
  • AIDL接口中只支持方法 不支持聲明靜態(tài)常量。
  • 在使用aidl時(shí),最好把所有aidl文件都放在一個(gè)包中,這樣方便復(fù)制到客戶端,其實(shí)所有的跨進(jìn)程對象傳遞都是對象的序列化與反序列化,所以必須包名一致。
場景用例

現(xiàn)在加入有這樣一個(gè)需求 如果服務(wù)端是 圖書館添加和查看書的任務(wù) 客戶端可以查看和添加書 這時(shí)候需要添加一個(gè)功能 當(dāng)服務(wù)端每添加了一本書 需要通知客戶端注冊用戶 有一本新書上架了 這個(gè)功能如何實(shí)現(xiàn)?想想可知 這是一個(gè)觀察者模式 如果在同一進(jìn)程中很容易實(shí)現(xiàn),只需要在服務(wù)端中的代碼中維護(hù)一個(gè)集合 里面放的是注冊監(jiān)聽的用戶 然后用戶需要實(shí)現(xiàn)一個(gè)新書到來的回調(diào)接口當(dāng)有新書上架時(shí) 遍歷這個(gè)集合 調(diào)用每個(gè)注冊者的接口方法 即可實(shí)現(xiàn) 現(xiàn)在我們是跨進(jìn)程通信 所以自然不能如此簡單了 但也不是很復(fù)雜 想一想 其實(shí)就是把以往的接口定義 變成了aidl接口定義 然后其他的一樣即可 但是這樣還是存在一個(gè)問題 如果注冊了listener 我們又想解除注冊 是不是在客戶端傳入listener對象 在服務(wù)端把它移除就可以呢? 其實(shí)是不可以的 因?yàn)檫@是跨進(jìn)程的 所以對象并不是真正的傳遞 只是在另一個(gè)進(jìn)程中重新創(chuàng)建了一個(gè)一樣的對象 內(nèi)存地址不同 所以根本不是同一個(gè)對象所以是不可以的 如果要解決這個(gè)問題 需要使用RemoteCallbackList 類 不要使用CopyWriteArrayList 在RemoteCallBackList中封裝了一個(gè)Map 專門用來保存所有的AIDL回調(diào) key為IBinder value是CallBack 使用IBinder 來區(qū)別不同的對象 ,因?yàn)榭邕M(jìn)程傳輸時(shí)會(huì)產(chǎn)生很多個(gè)不同的對象 但這些對象的底層的Binder都是同一個(gè)對象 所以可以 在使用RemoteCallBackList時(shí) add 變?yōu)?register remove 變?yōu)?unregister 遍歷的時(shí)候需要先 beginBroadcast 這個(gè)方法同時(shí)也獲取集合大小 獲取集合中對象使用 getBoardCastItem(i) 最后不要忘記finishBoardCast方法。

還有一個(gè)情況 由于onServiceConnected方法 是在主線程執(zhí)行的 如果在這里執(zhí)行服務(wù)端的耗時(shí)代碼 會(huì)ANR 所以需要開啟一個(gè)子線程執(zhí)行 同理在服務(wù)端中 也不可以運(yùn)行客戶端的耗時(shí)程序 總結(jié)起來就是 在執(zhí)行其他進(jìn)程的耗時(shí)程序時(shí) 都需要開啟另外的線程防止阻塞UI線程 如果要訪問UI相關(guān)的東西 使用handler

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

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

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

第一種方法:在onBind中進(jìn)行驗(yàn)證(permission驗(yàn)證)

首先在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" />

驗(yàn)證

@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中進(jìn)行驗(yàn)證(包名驗(yàn)證)
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
            // 權(quán)限驗(yà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)限驗(yàn)證失敗");
                return false;
            }
            // 包名驗(yàn)證
            String packageName=null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if(packages!=null && packages.length>0){
                packageName = packages[0];
            }
            if(!packageName.startsWith("com.example")){
                L.d("包名驗(yàn)證失敗");
                return false;

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

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

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

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

需要注意的問題:
1.關(guān)于 sqlite crud的各個(gè)參數(shù)的意義,query函數(shù),參數(shù)
Cursor query(boolean distinct, String table, String[] columns,String selection, String[] selectionArgs, String groupBy,String having, String orderBy, String limit)
第一個(gè)參數(shù) distinct 英語單詞意思,獨(dú)特的,如果true 那么返回的數(shù)據(jù)都是唯一的,意思就是實(shí)現(xiàn)查詢數(shù)據(jù)的去重。
第二個(gè)參數(shù) table,表名
第三個(gè)參數(shù) columns,要查詢的行的名字?jǐn)?shù)組 例如 new String[]{"id","name","sex"}
第四個(gè)參數(shù) selection 選擇語句 sql語句中where后面的語句 值用?代替 例如 "id=? and sex=?"
第五個(gè)參數(shù) selectionArgs 對應(yīng)第四個(gè)參數(shù)的 ? 例如 new String[]{"1","男"}
第六個(gè)參數(shù) groupBy 用于分組
第七個(gè)參數(shù) having 篩選分組后的數(shù)據(jù)
第八個(gè)參數(shù) orderby 用于排序 desc/asc 升序和降序 例如 id desc / id asc
最后一個(gè)參數(shù) limit 用于限制查詢的數(shù)據(jù)的個(gè)數(shù) 默認(rèn)不限制其他幾個(gè)函數(shù) 根據(jù)query函數(shù)的參數(shù)猜想即可。

2.由于每次ipc操作 都是靠uri來區(qū)別 想要獲取的數(shù)據(jù)位置 所以provider在調(diào)取數(shù)據(jù)的時(shí)候根據(jù)uri并不知道要查詢的數(shù)據(jù)是在哪個(gè)位置,所以我們可以通過 UriMatcher 這個(gè)類來給每個(gè)uri標(biāo)上號(hào) 根據(jù)編號(hào) 對應(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);
            }
            這樣我們可以通過 下面這個(gè)樣子來獲取位置(此處是表名 其他類型也一樣)
            private String getTableName(Uri uri) {
                switch (matcher.match(uri)) {
                    case BOOK_CODE:
                        return "bookTable";
                    case USER_CODE:
                        return "userTable";
                }
                return "";
            }

3.另外ContentProvider除了crud四個(gè)方法外,還支持自定義調(diào)用 通過ContentProvider 和ContentResolver的 call方法 來實(shí)現(xiàn)

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

需要權(quán)限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
這種方式需要一個(gè)服務(wù)端socket 和一個(gè)客戶端socket 建立連接后 通過流循環(huán)獲取消息即可。
1.在服務(wù)端開啟一個(gè)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);  //處理客戶端的消息 就是開啟一個(gè)線程循環(huán)獲取out 和 in  流 進(jìn)行通信
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }.start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

2.在客戶端開啟一個(gè)線程 使用ip和端口號(hào)連接服務(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 連接池的使用 很好用

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

  1. 首先,為每個(gè)業(yè)務(wù)模塊創(chuàng)建AIDL接口并實(shí)現(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實(shí)例化Binder對象,通過onBind方法返回出去。
  4. 創(chuàng)建BinderPool類,單例模式,在構(gòu)造方法中綁定Service,在onServiceConnected方法獲取到BinderPoolImpl對象,這個(gè)BinderPoolImpl類是BinderPool的內(nèi)部類,并實(shí)現(xiàn)了IBinderPool的業(yè)務(wù)方法。BinderPool類中向外暴露了queryBinder方法,這個(gè)方法其實(shí)調(diào)用的是BinderPoolImpl對象的queryBinder方法。

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容