android 跨進程通信

Intent
共享文件
binder
contentprovider
網絡通信:socket

bundle

Bundle實現了Parcelable接口,activity,service,receiver三大組件可以跨進程傳輸基礎類型,序列化過的對象,及一些android支持的特殊對象

共享文件

android 基於linux,對併發讀寫沒有限制,譬如兩個線程對統一文件同時寫操作都是允許的。所以,文件共享用於簡單的文本信息傳輸,還可以通過序列化,在進程A序列化對象,在進程B反序列化得到對象來實現進程間通信

文件併發讀寫限制:試用於對數據同步要求不高的進程間通信,處理好併發讀寫的問題

sp:輕量級緩存方案,鍵值對的方式存儲,底層實現是通過xml存儲鍵值對,一般位於:/data/data/package name/shared_prefs/下。。sp屬於文件的一種,但是系統對sp的讀寫有一定的緩存策略,即在內存中有一份sp文件的緩存,因此多進程下,系統對它的讀寫變的不可靠,高併發讀寫時候,數據很可能就丟失了,因此,一般進程間通信不用sp

Messenger

demo: https://github.com/SpikeKing/wcl-messenger-demo

https://www.jianshu.com/p/56ce3d9fc00d

Messenger基於AIDL實現, 順序執行, 不支持併發

略像客戶端原生和js交互,雙向通信,需要雙向註冊信息回調

service

client

AIDL

demo:https://github.com/lijiacn/TestAIDL/tree/master

messenger:
1:串行的,對於併發不適用
2:通信,無法調用另一進程的方法

使用AIDL實現跨進程的方法調用

所有的AIDL文件大致可以分爲兩類。一類是用來定義parcelable對象,以供其他AIDL文件使用AIDL中非默認支持的數據類型的。一類是用來定義方法接口,以供系統使用來完成跨進程通信的。可以看到,兩類文件都是在“定義”些什麼,而不涉及具體的實現,這就是爲什麼它叫做“Android接口定義語言”

service:

aidl定義接口,service實現接口,clien調用接口,是現在service中,達到client調用service方法

AIDL 文件支持數據類型:

  • 基本數據類型(int,long, char, boolean, double 等)
  • String, CharSequence
  • List: 只支持ArrayList, 裏面每個元素都必須能夠被AIDL支持
  • Map: 只支持HashMap,裏面的每個元素都必須被AIDL支持,包括key和value
  • Parcelable:所有實現Parcelable接口的對象
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用

自定義對象

1:實現Parcelable接口
2:新建同名aidl文件。添加: parcelable 對象class

注意
1:aidl中除了基本數據類型,其餘都需要標明:in,out,inout
2:aidl中只能聲明方法,區別於傳統接口
3:service端中的list使用CopyOnWriteArrayList:支持併發讀寫,aidl方法是在服務端的binder線程池中執行,多個client連接時候,需要在aidl方法中處理線程同步問題,CopyOnWriteArrayList進行自動線程同步。爲啥aidl中能用ArrayList之外的list類型?aidl中所支持的是抽象的list,list只是一個接口,服務端返回的是CopyOnWriteArrayList,但是binder中會按照list的規範去訪問數據,最終形成一個新的ArrayList傳遞給客戶端,類比ConcurrentHashMap也一樣

Book.aidl與Book.java的包名應當是一樣的。這似乎理所當然的意味着這兩個文件應當是在同一個包裏面的——事實上,很多比較老的文章裏就是這樣說的,他們說最好都在 aidl 包裏同一個包下,方便移植——然而在 Android Studio 裏並不是這樣。如果這樣做的話,系統根本就找不到 Book.java 文件,從而在其他的AIDL文件裏面使用 Book 對象的時候會報 Symbol not found 的錯誤。爲什麼會這樣呢?因爲 Gradle 。大家都知道,Android Studio 是默認使用 Gradle 來構建 Android 項目的,而 Gradle 在構建項目的時候會通過 sourceSets 來配置不同文件的訪問路徑,從而加快查找速度——問題就出在這裏。Gradle 默認是將 java 代碼的訪問路徑設置在 java 包下的,這樣一來,如果 java 文件是放在 aidl 包下的話那麼理所當然系統是找不到這個 java 文件的。那應該怎麼辦呢?

又要 java文件和 aidl 文件的包名是一樣的,又要能找到這個 java 文件——那麼仔細想一下的話,其實解決方法是很顯而易見的。首先我們可以把問題轉化成:如何在保證兩個文件包名一樣的情況下,讓系統能夠找到我們的 java 文件?這樣一來思路就很明確了:要麼讓系統來 aidl 包裏面來找 java 文件,要麼把 java 文件放到系統能找到的地方去,也即放到 java 包裏面去。接下來我詳細的講一下這兩種方式具體應該怎麼做:

1:新建aidl文件,添加方法或變量,編譯,同名接口內出現對應方法
2:新建service。binder橋接,使用接口.stub 實例

1:修改 build.gradle 文件:在 android{} 中間加上下面的內容:

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}

2:把 java 文件放到 java 包下去:把 Book.java 放到 java 包裏任意一個包下,保持其包名不變,與 Book.aidl 一致。只要它的包名不變,Book.aidl 就能找到 Book.java ,而只要 Book.java 在 java 包下,那麼系統也是能找到它的。但是這樣做的話也有一個問題,就是在移植相關 .aidl 文件和 .java 文件的時候沒那麼方便,不能直接把整個 aidl 文件夾拿過去完事兒了,還要單獨將 .java 文件放到 java 文件夾裏去。

client:

1:connection

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMyAidlInterface mIMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
            Log.i("TAG", "onServiceConnected: " + mIMyAidlInterface);
            try {
                mIMyAidlInterface.hello("World!!!");
            }  catch (RemoteException e) { e.printStackTrace(); } }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.i("TAG", "onServiceDisconnected: " + name);
        }
    };

2:action

Intent intent = new Intent();           
//你定義的service的action
intent.setAction("example.admin.com.testaidl.MyService"); 
//這裏你需要設置你應用的包名
intent.setPackage("example.admin.com.testaidl"); 
bindService(intent,mConnection, Context.BIND_AUTO_CREATE);

場景:service數據變動,通知client。觀察者模式

1:新建client端用於回調的AIDL接口:onNewBookArrived
2:新建服務端使用的AIDL文件:addBook,getBook,register(上面接口實例),unRegister
3:新建service,建list維護client回調接口,在service收到新書時候,通過onNewBookArrived逐個回調給client

難點

上面的場景,在進程unRegister監聽時候,跟註冊傳的同一個對象,缺反註冊失敗,爲什麼?跨進層,是沒法傳輸對象的,內存空間都不一樣,傳到service的對象,都生成了新的對象。。這時候就要用到系統專門提供的RemoteCallBackList:專門用於刪除跨進層listener的接口。它是個泛型,支持管理任意的AIDL接口,因爲所有的AIDL都繼承自IInterFace,看下面就明白了:

public class RemoteCallbackList<E extends IInterface>

RemoteCallbackList

工作原理:

內部有個Map結構專門用來保存所有的AIDL回調,這個map的key是IBinder類型,value是CallBack類型,其中CallBack中封裝了真正的listener,當客戶端註冊listener時候,它會把這個listener的信息存入mCallBack中,其中key和value獲取方式如下:

IBinder key = listener.asBinder();
Callback value = new Callback(listener, cookie);

所以,雖說到service都是新對象,但是他們底層Binder對象是同一個,所以,解註冊時候,遍歷service所有的listener,找出那個和解註冊listener具有相同Binder對象的服務端listener並把它刪除即可。

優勢:

1:客戶端進程終止後,它能自動移除客戶端所註冊的listener

2:RemoteCallbackList內部自動實現線程同步工作。

注意:

1:不能像list一樣去操作它,它並不是一個list

2:遍歷RemoteCallbackList,必須要成對出現:beginBroadcast(),finishBroadcast()。哪怕僅僅是獲取其中元素個數

3:service方法本身是在binder線程池中執行,所以,沒有特殊必要不用再service開闢線程執行任務

4:client端,connection方法都是主線程,也不要直接調用service端耗時方法

解決4.5ANR問題: client端調用service方法放到非ui線程即可

new Thread(new Runnable() {
            @Override
            public void run() {
                //todo service方法調用
            }
        });

5:service調用client接口也是運行在binder線程池中,所以,涉及到client中操作ui的。需要注意

6:binder可能會意外死亡,大多是應爲service進程意外停止

service端:
1:維護client監聽的數組CopyOnWriteArrayList替換爲RemoteCallBackList

2:數組操作修改一下:add 》 register,remvove 》unregister

3:通知監聽者(client)。

    interface CustomListener extends android.os.IInterface {
        void method();
    }

    private RemoteCallbackList<CustomListener> callbackList = new RemoteCallbackList<>();

    private void method() {
        final int n = callbackList.beginBroadcast();
        for (int i = 0; i < n; i++) {
            CustomListener listener = callbackList.getBroadcastItem(i);
            if (null != listener) {
                try {
                    listener.method();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        callbackList.finishBroadcast();
    }

binder意外死亡,重連服務

1:給Binder設置DeathRecipient監聽,當Binder死亡時候,我們會收到binderDied方法的回調,在binderDied中我們可以重新連接遠程服務

2:onServiceDisconnected中重連

區別

onServiceDisconnected在UI線程被回調,binderDied在客戶端的Binder線程池中被回調

如何在AIDL中使用權限驗證功能

service默認情況下,都是可以連接的。所以,需要在service添加權限驗證,來過濾能訪問的client

onBind中驗證,驗證不通過就返回null,這樣client就沒法綁定服務

驗證方式:

  • permission:onBind中checkCallingOrSelfPermission。。這種方式也適用於Messenger

1:聲明所需權限:?
2:client申請權限 ?
3:service 驗證權限

服務端的onTransact方法中進行權限驗證,如果驗證失敗,就返回false,這樣服務端就會終止AIDL中的方法從而達到保護服務端的效果

驗證方式:?????????????

  • permission:
  • Uid和Pid驗證

Binder連接池

ContentProvider

ContentProvider 系統提供的專門用於不同應用間進行數據共享的方式,這一點,天生適合進程間通信。

和Messenger一樣,底層實現也是Binder

雖然系統已經封裝的很好,使用起來相對簡單,但是還是有一些注意問題:

1:CRUD
2:防止SQL注入
3:權限控制

自定義ContentProvider:
1:繼承自ContentProvider
2:實現6個抽象函數

oncreate:創建,一些初始化工作

getType:返回一個Uri請求對應的MIME類型,比如圖片,視頻等,如果不關注這個類型,直接返回null或“/

剩下4個方法,對應CRUD

根據binder運行原理:6個方法都運行在ContentProvider進程中,oncreate由系統回調並運行在主線程中,其他5個有外界回調並運行在binder線程池中

ContentProvider主要以表格的形式來組織數據,並且可以包含多個表,表具有行和列,行代表一條記錄,列對應一條記錄中的一個字段。類比db很像。。。ContentProvider還支持文件數據,如圖片,視頻等。android 系統的MediaStore就是文件類型的ContentProvider。ContentProvider對底層的數據存儲方式沒有要求,可以使用sqlite數據庫,可以是普通文件,也可以是內存中的一個對象來進行數據存儲

Socket

套接字,是網絡通信中的概念,:流式套接字,用戶數據報套接字。分別對應於網絡的傳輸控制層中的TCP和UDP。

TCP是面向連接的協議,提供穩定的雙向通信功能,TCP的連接需要經過三次握手才能完成,爲了穩定的數據傳輸功能,其本身提供了超時重傳機制,因此具有很高的穩定性。

UDP是無連接的,提供不穩定的單向通信功能,當然UDP也能實現雙向通信功能。性能上,UDP具有更好的效率,其缺點是不保證數據一定能夠正確傳輸,尤其是在網絡擁塞的時候。

Socket本身可以支持傳輸任意字節流

注意點:

1:聲明權限

2:不要放在主線程訪問網絡。1):程序無法在4.0及以上中運行,會拋出異常:android.os.NetworkOnMainThreadException。2):網絡請求很可能是耗時的,影響效率

選擇合適的IPC方式

名稱 優點 缺點 適用場景
Bundle 簡單易用 只能傳輸Bundle支持的數據類型 四大組件間的進程通信
文件共享 簡單易用 不適合高併發場景,並且無法做到進程間的及時通信 無法併發訪問,交換簡單的數據實時性不高的場景
AIDL 功能強大,支持一對多併發通信,支持實時通信 使用稍微複雜,需要處理好線程同步 一對多通信且有RPC需求
Messenger 功能一般,支持一對多串行通信,支持實時通信 不能很好處理高併發情形,不支持RPC,數據通過Message進行傳輸,因此只能傳輸Bundle支持的數據類型 低併發的一對多即時通信,無RPC需求,或者無須要返回結果的RPC需求
ContentProvider 在數據源訪問方面功能強大,支持一對多併發數據共享,可通過Call方法擴展其他操作 可以理解爲受約束的AIDL,主要提供數據源的CRUD操作 一對多的進程間數據共享
Socket 功能強大,可以通過網絡傳輸字節流,支持一對多併發實時通信 實現細節稍微有點繁瑣,不支持直接的RPC 網絡數據交換
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章