Android_IPC機制知識梳理
IPC機制知識梳理
寫在前面
本篇部落格大部分是對Android中IPC知識的一個梳理,部落格內容編寫邏輯參考了任玉剛前輩的《Android開發藝術探索》一書中第二章節,部落格中的demo原始碼是基於android studio 2.2.3所寫,本內容中有部分原始碼分析,適合具有較好android基礎知識的開發人員閱讀。此外,因個人能力有限,如有錯誤遺漏之處歡迎指正。
簡介
IPC是Inter-Process Communication的縮寫,含義為程序間通訊或跨程序通訊,是指兩個程序之間程序資料交換的過程。
程序和執行緒的解釋:
在Android系統中程序是指一個應用,一個程序可以包含多個執行緒,也可以只有一個執行緒即UI執行緒也叫主執行緒其主要功能是更新介面元素,相應的子執行緒中一般是用來執行大量耗時任務。
Linux上可以通過命名管道、共享記憶體、訊號量等來實現程序間通訊,Android雖然是基於Linux核心的移動作業系統,但是他的程序間通訊方式與其並不完全相同,Android有自己的程序通訊方式,如可以輕鬆實現程序間通訊的Binder和不僅可以實現同一個裝置兩個應用程序間通訊還可以實現兩個終端之間的通訊的Socket。
程序間通訊應用場景:
- 某些模組由於特殊原因需要執行在單獨程序中
- 為了加大一個應用可使用的記憶體,才有多程序來獲取多份記憶體空間
- 當前應用需要向其他應用獲取資料
多程序模式
先來熱熱身,看下同一個應用開啟多程序模式的方式及執行機制。
1. 開啟方式
manifest檔案中配置4大元件的process屬性,預設程序名稱是當前應用包名。
命名方式:“ 包名:程序名稱 ”——私有程序(包名可省略),“ 包名.程序名稱 ”——全域性程序。
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".SecondActivity" android:process=":secondProcessName"/> <activity android:name=".ThirdActivity" android:process="cn.mime.multipleprocessstudy.thirdProcessName"/> </application>
除了可以在Studio的Monitor選項卡中看到多個程序執行,還可以通過命令檢視:adb shell ——> ps | grep (應用包名)
C:\Users\wangshan>adb shell
[email protected]:/ $ ps | grep cn.mime.multipleprocessstudy
u0_a302 744 530 1497292 75148 SyS_epoll_ 0000000000 S cn.mime.multipleprocessstudy
u0_a302 1471 530 1477016 64480 SyS_epoll_ 0000000000 S cn.mime.multipleprocessstudy:secondProcessName
u0_a302 1571 530 1477016 64568 SyS_epoll_ 0000000000 S cn.mime.multipleprocessstudy.thirdProcessName
2. 執行機制
多程序的開啟方式固然簡單,但是帶來各種各樣的問題,問題的根源要從其執行機制說起:
Android為每個程序都分配了一個獨立的虛擬機器,不同的虛擬機器在記憶體分配上有不同的地址空間,所以執行在不同程序的四大元件不能通過共享記憶體來共享資料。
一般使用多程序會有下面幾方面的問題:
- 靜態成員和單例模式完全失效
- 執行緒同步機制完全失效
- SharedPerferences的可靠性下降
- Application會多次建立
基礎概念介紹
雖然多程序的使用與會帶來與常規開發時不同的各種問題,但是我們仍舊可以通過特有的程序間通訊方式來解決上面的問題。
在此之前我們先來了解下IPC中的一些基礎概念:Serializable、Parcelable和Binder。
Serializable和Parcelable介面可以完成物件的序列化過程,當通過Intent和Binder傳輸資料時就需要使用Serializable和Parcelable,當需要把物件持久化到儲存裝置上或者通過網路傳輸時需要使用Serialzable來完成物件的持久化。
1. Serializable
java提供,實現簡單(實現 Serializable 介面,生成serialVersionUID),可以通過ObjectOutputStream序列化到檔案中,通過ObjectInputStream完成反序列化; serialVersionUID作用是在反序列化時判斷類成員變數的數量及型別有沒有發生變化。
需要注意的是:靜態成員變數屬於類不屬於物件,所以不會參與序列化過程;用transient關鍵字標記的成員變數也不參與序列化過程。
tip:在android studio中可通過設定來輔助生成serialVersionUID,步驟如下:
- File -> Settings… -> Editor -> Inspections -> Serialization issues -> Serializable class without ‘serialVersionUID‘(選中)-> apply
- 進入實現了Serializable中的類,選中類名,Alt+Enter彈出提示,然後直接匯入完成
2. Parcelable
Android提供,實現相對複雜(實現Parcelable 介面,寫 構造方法(Parcel in)、describeContents()、writeToParcel(Pacel dest,int flags)、Parcelable.Creator CREATOR)
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public Book userBook;
protected User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
//由於book是另一個可序列化物件,所以它的反序列化過程需要傳遞當前執行緒的上下文類載入器,否則會報ClassNotFound錯誤。
userBook = in.readParcelable(Book.class.getClassLoader());
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
dest.writeParcelable(userBook, flags);
}
}
Serializable 使用簡單,開銷大;Parcelable 實現相對複雜,效率高;建議記憶體序列化使用Parcelable,序列化到儲存裝置或網路傳輸用Serialzable。
tip:在Android Studio中只需要先將你的成員變數宣告好之後,一路Alt + Enter就可以輕鬆實現Parcelable中的所有方法。
測試(100個WalletBankCardBean物件序列化反序列化過程的記憶體開銷及耗時):
- Parcelable 30.37MB ——> 30.73MB 0.36MB 37ms ; 30.42MB ——> 30.77MB 0.35MB 35ms ; 30.49MB ——> 30.85MB 0.36MB 36ms
- Serializable 30.59MB ——> 31.97MB 1.38MB 76ms ; 30.64MB ——> 32.01MB 1.37MB 71ms ; 30.65MB ——> 32.02MB 1.37MB 70ms
3. Binder
Android開發中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及程序間通訊,無法觸及Binder核心,而Messenger的底層就是AIDL,所以我們這裡可以通過AIDL來分析Binder的工作機制。
首先,回顧下Android Studio中的aidl檔案目錄結構,這裡與最早的Eclipse工程目錄不同:
tip:建議將aidl檔案及其中所使用的Java Bean檔案均放在aidl目錄下,方便客戶端和服務端一起拷貝使用,以免遺漏,
如果aidl目錄中有非aidl類且外部有使用,則需要在專案的build.gradle中做“sourceSets”路徑配置:
sourceSets{
main{
java.srcDirs = ['src/main/java','src/main/aidl']
}
}
其中studio根據.aidl檔案自動生成的對應java檔案的目錄位置也發生了變化,在build\generated\source\aidl\debug\cn\mime\aidldemo\aidl目錄下。
下面就來分析下.aidl對應的.java檔案:
package cn.mime.aidldemo.aidl;
public interface IBookManager extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements cn.mime.aidldemo.aidl.IBookManager {
//Binder的唯一標識,一般用當前Binder的類名全路徑表示
private static final java.lang.String DESCRIPTOR = "cn.mime.aidldemo.aidl.IBookManager";
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* 將服務端的Binder物件轉換成客戶端所需的AIDL介面型別的物件
* 如果客戶端和服務端位於同一程序,那麼返回的就是服務端的Stub物件本身
* 否則返回的是系統封裝後的Stub.Proxy物件
* @param obj onServiceConnected()方法中返回的服務端Binder
* @return 客戶端所需的AIDL介面型別的物件
*/
public static cn.mime.aidldemo.aidl.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof cn.mime.aidldemo.aidl.IBookManager))) {
return ((cn.mime.aidldemo.aidl.IBookManager) iin);
}
return new cn.mime.aidldemo.aidl.IBookManager.Stub.Proxy(obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
/**
* 執行在服務端的Binder執行緒池中,當客戶端發起跨程序請求時,遠端請求會通過系統底層封裝後交由此方法來處理。
* 1.服務端通過code確定客戶端請求的目標方法
* 2.從data中取出目標方法所需要的引數(如果目標方法有引數的話)
* 3.執行目標方法
* 4.目標方法執行完畢後,向reply中寫入返回值(如果目標方法有返回值的話)
* 注意:如此方法返回false則客戶端的請求會失敗,所以我們可以在服務端中重寫該方法,
* 然後利用這個特性來做許可權驗證。
*/
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList: {
data.enforceInterface(DESCRIPTOR);
java.util.List<cn.mime.aidldemo.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
data.enforceInterface(DESCRIPTOR);
cn.mime.aidldemo.aidl.Book _arg0;
if ((0 != data.readInt())) {
_arg0 = cn.mime.aidldemo.aidl.Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
/**
* 上面Stub的asInterface()中,如果客戶端和服務端不是位於同一程序,就會將此物件返回給客戶端呼叫
*/
private static class Proxy implements cn.mime.aidldemo.aidl.IBookManager {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
/**
* 執行在客戶端
* 1.建立該方法需要的輸入型Parcel物件 _data
* 2.建立該方法需要的輸出型Parcel物件 _reply
* 3.把該方法的引數資訊寫入 _data 中(如果有引數的話)
* 4.呼叫transact()方法來發起RPC(遠端過程呼叫)請求,同時當前執行緒掛起
* 5.服務端的onTransact()方法被呼叫,直到RPC過程返回,當前執行緒繼續執行
* 6.從 _reply 中讀取RPC過程返回的結果(如果有返回值的話)
*/
@Override
public java.util.List<cn.mime.aidldemo.aidl.Book> getBookList() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<cn.mime.aidldemo.aidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(cn.mime.aidldemo.aidl.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public void addBook(cn.mime.aidldemo.aidl.Book book) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book != null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
}
public java.util.List<cn.mime.aidldemo.aidl.Book> getBookList() throws android.os.RemoteException;
public void addBook(cn.mime.aidldemo.aidl.Book book) throws android.os.RemoteException;
}
通過上面的分析我們需要注意:
- 當客戶端發起請求時,當前執行緒會被掛起,直至服務端程序返回資料,所以如果遠端方法很耗時,一定要放在子執行緒中呼叫。
- 服務端的Binder方法執行在Binder執行緒池中,所以Binder方法不管是否耗時都應該採用同步的方式實現。
總結下Binder工作機制:
可以大致歸結為三個物件(Client物件、Service物件及連線兩個物件的Binder物件)和兩個方法(在Client中呼叫的transact()方法和執行在Service中的onTransact()方法)
呼叫順序大致是:
Client發起遠端請求 ——> Binder寫入引數data ——> 呼叫transact(),同時Client執行緒被掛起 ——> Service呼叫Binder執行緒池中的onTransact() ——>寫入結果reply,通過Binder返回資料後喚醒Client執行緒
tip:相關demo原始碼見https://github.com/wanglei8100/Android_IPC 中的aidlclient和aidlservice
使用方式
跨程序通訊方式有很多種,如可以通過Intent中附加extras來傳遞資訊、通過共享檔案的方式來共享資料、通過Binder、ContentProvider、Socket,不同方式適合不同的場景,下面來一一說明。
1. Bundle
Bundle適用與不同程序中的四大元件間的程序通訊,一般是單向通訊,即在某個程序中開啟另外一個程序中的元件時通過Intent傳遞。
Bundle支援的資料型別有基本型別、實現了Parcelable或Serialzable介面的物件、List、Size等等具體可見Bundle類。
2. 共享檔案
通過將資料序列化到外部儲存裝置上,然後再從外部裝置上進行反序列化來達到通過共享檔案進行程序間通訊。
因為會發生併發讀寫問題,所以這種方式適合在對資料同步要求不高的程序間進行通訊。
3. Messenger
Messenger譯為信使,通過它可以在不同程序中傳遞Message物件,在Message中放入我們需要傳遞的資料。Messenger是一種輕量級IPC方案,它的底層實現是AIDL,它對AIDL做了封裝使用起來更簡單。由於它一次處理一個請求,因此在服務端我們不用考慮執行緒同步問題。
關於Messenger的使用和AIDL類似,設計的類有傳遞資料的Messenger,資料載體Message,接收處理資料的Handler,其工作機制流程可以理解為:
- 繫結服務:將Service返回的Binder轉換成IMessenger的同時得到封裝好的Messenger
- Client發起請求:通過Messenger將封裝好資料的Message傳送到Service,其底層呼叫的是IMessenger的send()方法
- Service在Handler的handleMessage()方法中接收到請求並處理
- 如果需要Service的相應則發起請求時要在Client建立屬於自己的Messenger並通過message.replyTo傳遞給Service
- Service將返回資訊封裝到Message中並通過message.replyTo攜帶過來的Client的Messenger傳送相應資料
- Client在Handler的handleMessage()方法中接收到返回資訊並處理
tip:相關demo原始碼見https://github.com/wanglei8100/Android_IPC 中的messengerclient和messengerservice
4. AIDL
上面講到的Messenger的底層實現也是AIDL,使得呼叫更加簡單,但是由於Messenger是以序列的方式處理客戶端發來的訊息,如果有大量的併發請求使用Messenger是不合適的,而且Messenger的作用主要是為了傳遞訊息,很多時候我們可能需要跨程序呼叫服務端的方法,所以這時我們可以使用AIDL來實現。
AIDL的使用在上面講Binder時就有提過,這裡簡單回顧總結下使用流程,可以分為服務端和客戶端兩塊:
- Service:首先建立一個Service來監聽客戶端的連線請求,然後建立一個AIDL檔案,將暴露給客戶端的介面在這個AIDL檔案中宣告,最後在Service中實現這個AIDL介面即可
- Client:首先繫結服務端的Service,繫結成功後將服務端返回的Binder物件轉化成AIDL介面所屬的型別,最後就可以呼叫AIDL中的方法了
AIDL檔案支援的資料型別:
- 8種基本型別(byte、short、int、long、char、boolean、float、double)
- String和CharSequence
- List:只支援ArrayList,切裡面的每個元素必須都能夠被AIDL支援
- Map:只支援HashMap,切裡面的每個元素必須都能夠被AIDL支援,包括key和Value
- Parcelable:所有實現Parcelable介面的物件
- AIDL:所有的AIDL介面本身也可以在AIDL檔案中使用
相關demo原始碼見https://github.com/wanglei8100/Android_IPC 中的aidlclient和aidlservice。
關於AIDL的使用需要特殊注意的有幾點:
demo中的Book.aidl 檔案
// Book.aidl package cn.mime.aidldemo.aidl; // Declare any non-default types here with import statements parcelable Book;
AIDL中每個實現了Parcelable介面的類都需要按照上面這種方式去建立相應的AIDL檔案並宣告那個類為parcelable
在demo中的IBookManger.adil中有兩處需要加以說明
// IBookManager.aidl package cn.mime.aidldemo.aidl; // Declare any non-default types here with import statements import cn.mime.aidldemo.aidl.Book; interface IBookManager { List<Book> getBookList(); void addBook(in Book book); }
- import cn.mime.aidldemo.aidl.Book; Book類的匯入,我們知道在java類編寫規範時,同一個包名下的類檔案引用是不需要導包的,但是AIDL檔案編寫規則是需要手動匯入的,否則編譯時就會報 ProcessException。
- void addBook(in Book book); 在AIDL中除了基本資料型別,其他型別的引數必須標上定向tag:in、out或inout。
- in 服務端接收客戶端流向的完整物件,但是服務端的修改不影響客戶端該物件;
- out 服務端接收客戶端流向物件的空物件,但服務端的修改影響客戶端該物件;
- inout 服務端接收客戶端流向的完整物件,且服務端的修改影響客戶端該物件;
- 定向tag在aidl方法介面的方法中修飾序列化物件,用out/inout修飾的序列化物件必須寫readFromParcel(Parcel dest)方法
在原始碼的Service中我們可以看到用到的List是CopyOnWriteArrayList它不僅實現了List介面,而且支援併發讀/寫,這是因為考慮到AIDL方法是在服務端的Binder執行緒池中執行的,因此當有多個客戶端同時連線時會存在多個執行緒同時訪問的情形,所以我們要在AIDL方法中處理執行緒同步。雖然這裡使用是CopyOnWriteArrayList但是Binder中會按照List的規範去訪問資料並最終形成一個ArrayList傳遞給客戶端,這也印證了我們上面提到的AIDL支援的資料型別中List只有ArrayList。類似的還有ConcurrentHashMap。
- AIDL中只支援方法,不支援宣告靜態常量
上面講的是AIDL的基礎用法,所寫的demo案例也都是客戶端向服務發出遠端呼叫,然後服務端給出響應。但是,有時候還有一種業務場景就是客戶端需要監聽服務端的某個動作,然後做響應的操作。對於這種場景我們需要做進一步操作了,我們在上面demo的基礎上新建一對案列aidlclient_v2和aidlservice_v2進行分析。
首先,新建一個AIDL檔案IOnNewBookArrivedListener.aidl:
// IOnNewBookArrivedListener.aidl
package cn.mime.aidlservice_v2;
// Declare any non-default types here with import statements
import cn.mime.aidlservice_v2.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book newBook);
}
此aidl是服務端對客戶端提供的用來監聽介面,在案例demo中的作用是服務端如果有新書到來就會呼叫此介面中的方法通知客戶端。
其次,改造IBookManager.aidl,新增註冊監聽及取消監聽的方法:
// IBookManager.aidl
package cn.mime.aidlservice_v2;
// Declare any non-default types here with import statements
import cn.mime.aidlservice_v2.Book;
import cn.mime.aidlservice_v2.IOnNewBookArrivedListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
這裡需要注意的是IOnNewBookArrivedListener是aidl檔案,且與IBookManager.aidl位於同級目錄下也要手動匯入。
然後,就是服務端Service中的邏輯改造了:
- 實現IBookManager介面中新增的兩個方法,註冊監聽,取消監聽
在onCreate中建立一個執行緒,每隔五秒鐘建立一本新書並通過監聽通知客戶端
public class BookManagerService extends Service { private static final String TAG = "BookManagerService"; private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(true); private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>(); private RemoteCallbackList mRemoteListenerList = new RemoteCallbackList(); private IBookManager.Stub mBookManager = new IBookManager.Stub() { @Override public List<Book> getBookList() throws RemoteException { Log.d(TAG, "mBookManager getBookList() called mBookList : " + mBookList.toString()); return mBookList; } @Override public void addBook(Book book) throws RemoteException { Log.d(TAG, "mBookManager addBook() called book : " + book.toString()); if (!checkBookExist(book)) { mBookList.add(book); } } @Override public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException { mRemoteListenerList.register(listener); } @Override public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException { mRemoteListenerList.unregister(listener); } }; private boolean checkBookExist(Book book) { boolean isExist = false; for (int i = 0; i < mBookList.size(); i++) { if (book.bookId == mBookList.get(i).bookId && book.bookName.equals(mBookList.get(i).bookName)) { isExist = true; break; } } return isExist; } @Override public void onCreate() { super.onCreate(); new Thread(new Runnable() { @Override public void run() { while (mIsServiceDestroyed.get()) { onNewBookArrived(new Book(mBookList.size() + 1, "武俠小說" + mBookList.size() + 1)); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } private void onNewBookArrived(Book book) { int broadcastNumber = mRemoteListenerList.beginBroadcast(); for (int i = 0; i < broadcastNumber; i++) { IOnNewBookArrivedListener listener = (IOnNewBookArrivedListener) mRemoteListenerList.getBroadcastItem(i); if (listener != null) { try { listener.onNewBookArrived(book); } catch (RemoteException e) { e.printStackTrace(); } } } mRemoteListenerList.finishBroadcast(); } @Override public IBinder onBind(Intent intent) { Book firstBook = intent.getParcelableExtra("first_book"); mBookList.add(firstBook); return mBookManager; } @Override public void onDestroy() { mIsServiceDestroyed.set(false); super.onDestroy(); } }
這裡需要注意的是RemoteCallbackList這個類,它是系統專門提供用於註冊/刪除跨程序listener的介面,它是一個泛型,支援管理任意的AIDL介面。
public class RemoteCallbackList<E extends IInterface> {
為什麼要引用它來進行註冊/刪除跨程序listener的介面?因為Binder會把客戶端傳遞過來的監聽物件反序列化後變成一個新的物件,所以無論是在註冊還是取消註冊時傳遞過來的監聽物件均不是同一個物件,所以傳統的List並不能完成註冊/刪除跨程序listener的介面。而RemoteCallbackList的特性則可以,它的內部有一個Map結構專門用來儲存所有的AIDL回撥,這個Map的key是IBinder型別,value是CallBack型別:
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();
而CallBack中封裝了真正的遠端listener,雖然多次跨程序傳輸客戶端的同一個物件會在服務端生成不同的物件,但是它們底層的Binder物件是同一個,而RemoteCallbackList的Key儲存的就是相同的Binder,所以它可以在取消監聽時通過Binder準確取消所監聽的物件,此外它還會在客戶端程序終止後,自動移除客戶端所註冊的Listener,並且它內部自動實現了執行緒同步的功能,所以我們使用它來進行註冊/解註冊時不需要做額外的執行緒同步工作。
RemoteCallbackList常用方法:
- register(listener) :註冊監聽
- unregister(listener):取消監聽
- int beginBroadcast():準備開始呼叫監聽方法,和finishBroadcast()配對使用,返回監聽的數量
- Listenr getBroadcastItem(index):獲取監聽物件,必須在beginBroadcast()方法後呼叫
- finishBroadcast():結束監聽呼叫,和beginBroadcast()配對使用
最後,客戶端改造,客戶端改造較為簡單:
建立IOnNewBookArrivedListener
private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub(){ @Override public void onNewBookArrived(Book newBook) throws RemoteException { Message message = Message.obtain(); message.obj = newBook; mHandler.sendMessage(message); } };
註冊遠端監聽
if (checkRemoteService()){ try { mRemoteService.registerListener(mOnNewBookArrivedListener); } catch (RemoteException e) { e.printStackTrace(); } }
處理監聽回撥
private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { Book newBook = (Book) msg.obj; Toast.makeText(MainActivity.this,newBook.toString(),Toast.LENGTH_SHORT).show(); } };
取消遠端監聽
if (checkRemoteService()){ try { mRemoteService.unregisterListener(mOnNewBookArrivedListener); } catch (RemoteException e) { e.printStackTrace(); } }
這裡需要注意的是因為遠端監聽的回撥是執行在客戶端的Binder執行緒池中,所以不能有直接更新UI的操作。另外,無論是註冊還是取消註冊的操作之前都要判斷遠端服務的連結是否存在,否則會報警告:
private boolean checkRemoteService() { return mRemoteService !=null&&mRemoteService.asBinder().isBinderAlive(); }
在上面的業務場景的基礎上我們還需要做最後一步,許可權驗證,因為預設情況下我們的遠端服務任何人都可以連線,這並不是我們想看到的,所以我們需要給服務加入許可權驗證功能,只有驗證通過才可以呼叫我們服務中的方法。在AIDL中進行許可權驗證發方法有很多,這裡介紹常用的兩種方法。
在onBind中進行驗證,驗證不通過直接返回null,則驗證不通過客戶端就無法繫結服務。驗證方式可以有很多種,比如通過intent取出客戶端和服務端協商好的唯一性keySecret進行校驗,也可以通過自定義permission驗證,如果通過自定義permission驗證,我們需要在服務端的AndroidMenifest中宣告所需許可權,比如:
<permission android:name="cn.mime.aidlservice_v3.permission.ACCESS_BOOK_SERVICE" android:protectionLevel="normal"/>
然後在服務端的onBind中校驗此許可權:
@Override
public IBinder onBind(Intent intent) {
int checkStatus = checkCallingOrSelfPermission("cn.mime.aidlservice_v3.permission.ACCESS_BOOK_SERVICE");
if (checkStatus == PackageManager.PERMISSION_DENIED){
return null;
}
return mBookManager;
}
則呼叫我們服務的客戶端需要在AndroidMenifest中使用permission即可:
<uses-permission android:name="cn.mime.aidlservice_v3.permission.ACCESS_BOOK_SERVICE" />
在服務端的onTransact方法中進行許可權驗證,如果驗證失敗直接返回false,這樣服務端就不會終止執行AIDL中個的方法,從而達到保護服務端的效果。具體驗證方式除和上面一樣的外,還可以採用Uid和Pid來驗證,比如:
@Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { int callingUid = getCallingUid(); String[] packages = getPackageManager().getPackagesForUid(callingUid); if (packages!=null&&packages.length>0){ if (!packages[0].startsWith("cn.mime.aidlclient")){ return false; } } return super.onTransact(code, data, reply, flags); }
tip:關於許可權驗證的demo原始碼見https://github.com/wanglei8100/Android_IPC 中的aidlclient_v3和aidlservice_v3
5. ContentProvider
ContentProvider是Android中提供的專門用於不同應用間進行資料共享的方式,所以它天生適合程序間通訊,底層實現也是Binder。下面是自定義Provider時的一些注意事項:
- 服務端定義ContentProvider時新建類extends ContentProvider然後重寫onCreate()、insert()、delete()、update()、query()和getType()方法。onCreate()是執行在主執行緒,其它CRUD方法執行在Binder執行緒
- 然後在Manifest中註冊provider,注意兩個屬性:android:authorities是指定provider唯一屬性的,android: permission可選是指定訪問此provider必須要申請的相應許可權,也可以單獨指定android:readPermission或android:writePermission
- 可以使用SQLiteOpneHelper結合資料庫完成ContentProvider對資料的操作
- 可以使用UriMatcher來匹配查詢的Uri,UriMatcher.addURI()初始化新增支援的URI,UriMatcher.match()匹配查詢的uri。
- 在insert,delete和update方法中因為資料發生了變化要呼叫ContentResolver方法的notifyChange()
- 要觀察一個ContentProvider中的資料改變情況,可以通過ContentResolver的registerContentObserver方法來註冊觀察者,通過unregisterContentObserver方法來解除觀察者。
- 客戶端使用定義好的android:authorities屬性值指定Uri,然後利用ContentResolver進行CRUD操作,如果服務端的provider註冊時有宣告許可權,客戶端必須在manifest中申請相應許可權
6. Socket
Socket也稱為“套接字”,是網路通訊中的概念,它分為流失套接字和使用者資料報套接字兩種,分別對應於網路的傳輸控制層的TCP和UDP協議。
TCP協議:面向連線的協議,提供穩定的雙向通訊功能,連線的建立需要經過“三次握手”才能完成,提供了超時重傳機制,穩定性很高。
UDP協議:無連線的,提供不穩定的單向通訊功能,效能上效率更好,但是不能保證資料一定能正確傳輸,特別是在網路擁塞的情況下。
Socket不僅僅可以實現程序間的通訊,而且還可以實現裝置間的通訊,不過需要裝置之間的IP地址互相可見。雖然如此,但是使用Socket的業務場景一般是在客戶端和服務端通訊比較頻繁,需要建立長時間穩定的連線的時候,所以在Android跨程序通訊的實際運用場景較少。
Binder連線池
需求場景,當應用中有多個模組都需要使用AIDL,並且各個模組之間沒有耦合,則是不是我們要在服務端把每個模組對應的建立一個Service,顯然這樣是不可取的,因為Service本身就是一種系統資源,太多的Service使得應用看起來很重量級。這時我們可以建立一個統一的Aidl的管理類,然後統一在一個service中去管理各個模組。我們可以簡單稱之為Binder連線池,舉個例子:假設服務端有分享和支付兩個模組,這兩個模組相互獨立,現在需要兩個模組都對外提供呼叫介面。此時我們可以在服務端這樣設計:
建立分享和支付aidl檔案
interface IShareContent { boolean share(String content); } interface IPayMoney { boolean pay(String orderNo); }
建立分享和支付的對應實現類
public class ShareContentImpl extends IShareContent.Stub { @Override public boolean share(String content) throws RemoteException { boolean status = !TextUtils.isEmpty(content); Log.d(TAG,"share status : "+status); return status; } } public class PayMoneyImpl extends IPayMoney.Stub { @Override public boolean pay(String orderNo) throws RemoteException { boolean status = !TextUtils.isEmpty(orderNo); Log.d(TAG,"pay status : "+status); return status; } }
建立管理分享和支付功能的BinderPool及其實現類
interface IBinderPool { IBinder getPayMoneyBinder(); IBinder getShareContentBinder(); } public class BinderPoolImpl extends IBinderPool.Stub { @Override public IBinder getPayMoneyBinder() throws RemoteException { return new PayMoneyImpl(); } @Override public IBinder getShareContentBinder() throws RemoteException { return new ShareContentImpl(); } }
在Service中onBinder方法中將BinderPool返回
public class BinderPoolService extends Service { @Override public IBinder onBind(Intent intent) { return new BinderPoolImpl(); } }
在客戶端對應的aidl檔案只需要匯入IBinderPool和使用的業務模組的aidl,然後在繫結服務成功時多了一次轉換:
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG,"onServiceConnected connected success !");
IBinderPool binderPool = IBinderPool.Stub.asInterface(service);
try {
mRemoteService = IShareContent.Stub.asInterface(binderPool.getShareContentBinder());
} catch (RemoteException e) {
e.printStackTrace();
}
}
tip:具體demo原始碼見https://github.com/wanglei8100/Android_IPC 中的binderpoolservice和binderpool_shareclient
IPC方式適用場景總結
- Bundle:四大元件間的程序間通訊,簡單易用,但是隻能傳輸Bundle支援的資料型別
- 共享檔案:無併發訪問情形,交換簡單的資料實時性不高的場景,簡單易用,但是不適合高併發場景,且無法做到程序間的即時通訊
- AIDL:一對多通訊且有RPC(遠端過程呼叫)需求,功能強大,支援一對多併發通訊,支援實時通訊,但是使用稍複雜,需注意處理好執行緒同步
- Messenger:低併發的一對多即時通訊,無RPC需求,或者無需返回結果的RPC需求,功能一般,支援一對多序列通訊,支援試試通訊,但不能很好處理高併發情形,不支援RPC,資料通過Message傳輸,因此只能傳輸Bundle支援的資料型別
- ContentProvider:一對多的程序間的資料共享,在資料訪問方面功能強大,支援一對多併發資料共享,可以通過Call方法擴充套件其他操作,但是可以理解為受約束的AIDL,主要提供資料來源的CRUD操作
- Socket:網路資料互動,功能強大,可以通過網路傳輸位元組流,支援一對多併發實時通訊,但是實現細節稍微有點麻煩,不支援直接的RPC