本節主要介紹IPC中的一些基礎概念,主要包含三方面內容:Serializable接口、Parcelable接口以及Binder,只有熟悉這方面的內容後,我們才能更好地理解跨進程通信地各種方式。Serializable和Parcelable接口可以完成對象的序列化過程,當我們需要通過Intent和Binder傳輸數據時,既需要用到Serializable或Parcelable。還有的時候我們需要把對象持久化到存儲設備上或者通過網絡傳輸給其他客戶端,這個時候也需要使用Serializable來完成對象的持久化,下面先介紹如何使用Serializable來完成對象的序列化。
1.Serializable接口
Serializable是java所提供的一個序列化的接口,它是一個空接口,爲對象提供標準的序列化和反序列化操作,使用Serializable來實現序列化相當簡單,只需要在類聲明中指定一個類似下面的標識即可自動實現默認的序列化過程。
private static final long serialVersionUID = 2965076648418851219L;
在Android中也提供了新的序列化方式那就是Parcelable接口,使用Parcelable來實現對象的序列化,其過程要稍微複雜些,本節先介紹Serializable接口。上面提到,想讓一個對象實現序列化,只需要這個類實現Serializable接口,並聲明一個serialVersionUID即可,實際上甚至這個serialVersionUID也不是必需的,我們不聲明這個serialVersionUID同樣也可以實現序列化,但是這將會對反序列化過程產生影響,具體什麼影響後面再介紹。User類就是一個實現了Serializable接口的類,他是可以被序列化和反序列化的,如下所示。
public class User implements Serializable {
private static final long serialVersionUID = 2965076648418851219L;
public int userId;
public String userName;
public boolean isMale;
}
通過Serializable方式來實現對象的序列化,實現起來非常簡單,幾乎所有工作都被系統自動完成了。如何進行對象的序列化和反序列化也非常簡單,只需要採用ObjectOutputStream和ObjectInputStream即可輕鬆實現。下面舉個例子。
//序列化過程
User user = new User(0,"jake",true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化過程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();
上述代碼演示了採用Serializable方式序列化對象的典型過程,很簡單,只需要把實現了Serializable接口的user對象寫到文件中就可以快速恢復了,恢復後的對象newUser和user的內容完全一樣,但是兩者並不是同一個對象。
剛剛開始提到,即使不指定serialVersionUID也可以實現序列化,那到底要不要指定呢?如果指定的化,serialVersionUID後面那一長串數字又是什麼含義呢?我們要明白系統既然提供了這個serialVersionUID,那麼它必須有用的。這個serialVersionUID是用來輔助序列化和反序列化過程的,原則上序列化後的數據中的serialVersionUID只有和當前類的serialVersionUID相同才能夠正常地被反序列化。serialVersionUID地詳細工作機制是這樣的:序列化的時候系統會把當前類的serialVersionUID寫入到序列化文件中(也可能是其他的中介),當反序列化的時候,系統會去檢測文件中的serialVersionUI看它是否和當前類的serialVersionUID一致,如果一致就說明序列化的類的版本和當前類的版本是相同的,這個時候可以成功反序列化;否則就說明當前類和序列化的類相比發送了某些變換,比如成員變量的數量、類型可能發送了改變,這個時候是無法正常反序列化的,因此會報錯誤。
一般來說,我們應該手動指定serialVersionUID的值,比如1L,也可以讓編輯工具根據當前類的結果去自動生成它的hash值,這樣序列化和反序列化時兩者的serialVersionUID時相同的,因此可以正常進行反序列化。如果不手動指定serialVersionUID的值,反序列化時當前類有所改變,比如增加或者刪除了某些成員變量,那麼系統就會重新計算當前類的hash值並把它賦值給serialVersionUI這個時候當前類的serialVersionUID就和序列化的數據中的serialVersionUID不一致,於是反序列化失敗,程序就會出現crash。所以我們可以很明顯的感覺到serialVersionUID的作用,當我們手動指定了它以後,就可以再很大程度上避免反序列化過程的失敗。比如當版本升級後,我們可能刪除 了某個成員變量也可能增加了一些新的成員變量,這個時候我們的反向序列化過程仍然能夠成功,程序仍然能夠最大限度地恢復數據,相反如果不指定地化,程序就會掛掉。當然我們還要考慮另外一種清空,如果類結果發送了非常規性地改變,比如修改了類名,修改了成員變量地類型,這個時候儘管serialVersionUID嚴重通過了,但是反序列化成功還是會失敗,因爲類結構有了毀滅性地改變,根本無法從老版本地數據中還原出一個新的類結構的對象。
根據上面的分析,我們可以知道,給serialVersionUID指定爲1L或者採用編碼工具根據當前類結構去生成的hash值,這兩種並沒有本質的區別,效果完全一樣。一下兩點需要特別注意,首先靜態成員變量屬於類不屬於對象,所以不會參與序列化過程;其次transient關鍵字標記的成員變量不參與序列化過程。
另外,系統的默認序列化過程也是可以改變的,通過如下兩個方法即可重寫系統默認的序列化和反序列化過程,具體怎麼去重寫這兩個方法就是很簡單的事情了,這裏就不做詳細的介紹 了。
private void writeObject(java.io.ObjectOutputStream out) throws IOException{
//write "this" to "out"
}
private void readObject(java.io.ObjectInputStream in)throws IOException,ClassNotFoundException{
//populate the fields of "this" from the data in "in"
}
2.Parcelable接口
上一節我們介紹了通過Serializable方式來實現序列化的方法,本節介紹另一種序列化方式Parcelable。Parcelable也是一個接口,只要實現這個接口,一個類的對象就可以實現序列化並通過Intent和Binder傳遞。下面的示例時一個典型的用法。
public class User implements Parcelable {
private static final long serialVersionUID = 2965076648418851219L;
public int userId;
public String userName;
public boolean isMale;
public Book book;
public User(int userId, String userName, boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeInt(isMale ? 1 : 0);
dest.writeParcelable(book,0);
}
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];
}
};
private User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readInt() == 1;
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
}
這裏先說明一下Parcel,Parcel內部包裝了可序列化的數據,可以在Binder中自由傳輸。從上述代碼可以看出,在序列化過程中需要實現的功能有序列化、反序列化和內容描述。序列化功能由writeToParcel方法來完成,最終是通過Parcel中的一系列Write方法來完成的;反序列化功能由CREATEOR來完成,其內部表明瞭如何創建序列化對象和數組,並通過Parcel的一系列read方法來完成反序列化過程;內容描述功能由describeContents方法來完成,幾乎所有情況下這個方法都應該返回0,僅噹噹前對象中存在文件描述符時,此方法返回1.需要注意的是,在User(Parcel in)方法中,由於book是另一個可序列化對象,所以它的反序列化過程需要傳遞當前線程的上下文類加載器,否則會報無法找到類的錯誤。詳細的方法說明請參看下錶。
方法 | 功能 | 標記 |
createFromParcel(Parcel in) | 從序列化後的對象中創建原始對象 | |
newArray(int size) | 創建指定長度的原始對象數組 | |
User(Parcel in) | 從序列化後的對象創建原始對象 | |
writeToParcel(Parcel out, int flags) | 將當前對象寫入序列化結構中,其中flags標識有兩種值:0或1(參見右側標記)。爲1時標識當前對象需要作爲返回值返回,不能立即釋放資源,幾乎所有情況都爲0 | PARCELABLE_WRITR_RETURN_VALUE |
describeContents | 返回當前對象的內容描述,如果含有文件描述符,返回1(參見右側標記位),否則返回0,幾乎所有情況都返回0 | CONTENT_FILE_DESCRIPTOR |
系統意見爲我們提供了許多實現Parcelable接口的類,它們都是可以直接序列化的,比如Intent、Bundle、Bitmap等,同時List和Map也可以序列化,前提是它們裏面的每個元素都是可序列化的。
既然Parcelable和Serializable都能實現序列化並且都可用於Intent間的數據傳遞,那麼二者該如何選取呢?Serializable是java中的序列化接口,其使用起來簡單但是開銷很大,序列化和反序列化過程需要大量I/O操作。而Parcelable是Android中的序列化方式,因此更適合用在Android平臺上,它的缺點就是使用起來稍微麻煩點,但它的效率很高,這是Android推薦的序列化方式,因此我們要首選Parcelable。Parcelable主要用在內存序列化上,通過Parcelable將對象序列化到存儲設備中或者將對象序列化後通過網絡傳輸也都是可以的,但是這個過程會稍顯複雜,因此在這兩種情況下建議大家使用Serializable。以上就是Parcelable和Serializable的區別。
3.Binder
Binder是一個很深入的話題,筆者也看過一些別人寫的Binder相關的文章,發現很少有人能把它介紹清楚,不是深入代碼細節不能自拔,就是長篇大論不知所云,看完後都是暈暈的感覺,所以,本節筆者不打算深入探討Binder的底層細節,因爲Binder太複雜了。本節的側重點是介紹Binder的使用以及上層原理,爲接下來的幾節內容做鋪墊的。
直觀來說,Binder是Android中的一個類,它實現了IBinder接口。從IPC角度來說,Binder是Android中的一種跨進程通信方式,Binder還可以理解爲一種虛擬的物理設備,它的設備驅動是/dev/binder,該通信方式在Linux中沒有;從Android Framework角度來說,Binder是ServiceManager連接各種Manager(ActivityManager、WindowManager,等等)和相應的ManagerService的橋樑;從Android應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService的時候,服務端會返回一個包含了服務端業務調用的Binder對象,通過這個Binder對象,客戶端就可以獲取服務端提供的服務或者數據,這裏的服務包括普通服務和基於AIDL的服務。
Android開發中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及進程間通信,所以比較簡單,無法觸及Binder的核心,而Messenger的底層其實是AIDL,所以這裏選用AIDL來分析Binder的工作機制。爲了分析Binder的工作機制,我們要新建一個AIDL示例,SDK會自動爲我們生產AIDL所對應的Binder類,然後我們就可以分析Binder的工作過程。還是採用本章開始時的例子將Book實現Parcelable接口。
package test.chenj.com.chapter2;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Created by 72312 on 2018/3/19.
*/
public class Book implements Parcelable{
private int bookId;
private String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
protected Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
}
右擊,新建AIDL文件,輸入名稱IBookManager,然後確定。此時不要忘記導入包Book。
// IBookManager.aidl
package test.chenj.com.chapter2;
// Declare any non-default types here with import statements
import test.chenj.com.chapter2.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
並且用到的Model類必須要實現Parcelable接口,名稱爲Book.aidl,如下,。
package test.chenj.com.chapter2;
parcelable Book;
上面三個文件中,Book.java時一個表示圖書信息的類,它實現了Parcelable接口。Book.aidl是Book類在AIDL中的聲明。IBookManager.aidl是我們自定義的一個接口,裏面有兩個方法:getBookList和addBook,其中getBookList用於從遠程服務端獲取圖書列表,而addBook用於汪圖書列表中添加一本書,當然這兩個方法主要是示例用,不一定要有實際意義。我們可以看到,IBookManager需要導入Book類,這就是AIDL的特殊之處。下面我們先看一下系統爲IBookManager.aidl生產的Binder類,在as工程目錄下的\app\build\generated\source\aidl\debug\test\chenj\com\chapter2中有一個IBookManager.java的類,這就是我們要找的類。接下來我們需要根據這個系統生成的Binder類來分析Binder的工作原理,代碼如下:
/* * This file is auto-generated. DO NOT MODIFY. * Original file: G:\\android_KFYS_project\\chapter2\\app\\src\\main\\aidl\\test\\chenj\\com\\chapter2\\IBookManager.aidl */ package test.chenj.com.chapter2; public interface IBookManager extends android.os.IInterface { public java.util.List<test.chenj.com.chapter2.Book> getBookList() throws android.os.RemoteException; public void addBook(test.chenj.com.chapter2.Book book) throws android.os.RemoteException; /** Local-side IPC implementation stub class. */ public static abstract class Stub extends android.os.Binder implements test.chenj.com.chapter2.IBookManager { private static final java.lang.String DESCRIPTOR = "test.chenj.com.chapter2.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); /** Construct the stub at attach it to the interface. */ public Stub() { this.attachInterface(this, DESCRIPTOR); } /** * Cast an IBinder object into an test.chenj.com.chapter2.IBookManager interface, * generating a proxy if needed. */ public static test.chenj.com.chapter2.IBookManager asInterface( android.os.IBinder obj) { if ((obj == null)) { return null; } android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof test.chenj.com.chapter2.IBookManager))) { return ((test.chenj.com.chapter2.IBookManager) iin); } return new test.chenj.com.chapter2.IBookManager.Stub.Proxy(obj); } @Override public android.os.IBinder asBinder() { return this; } @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<test.chenj.com.chapter2.Book> _result = this.getBookList(); reply.writeNoException(); reply.writeTypedList(_result); return true; } case TRANSACTION_addBook: { data.enforceInterface(DESCRIPTOR); test.chenj.com.chapter2.Book _arg0; if ((0 != data.readInt())) { _arg0 = test.chenj.com.chapter2.Book.CREATOR.createFromParcel(data); } else { _arg0 = null; } this.addBook(_arg0); reply.writeNoException(); return true; } } return super.onTransact(code, data, reply, flags); } private static class Proxy implements test.chenj.com.chapter2.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; } @Override public java.util.List<test.chenj.com.chapter2.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<test.chenj.com.chapter2.Book> _result; try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0); _reply.readException(); _result = _reply.createTypedArrayList(test.chenj.com.chapter2.Book.CREATOR); } finally { _reply.recycle(); _data.recycle(); } return _result; } @Override public void addBook(test.chenj.com.chapter2.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(); } } } } }
上述代碼是系統生成的,爲了方便查看,格式化了一下代碼。可以看到根據IBookManager .aidl系統爲我們生成了IBookManager.java類,它繼承了IInterface這個接口,同事它自己也還是個接口,所有可以在Binder中傳輸的接口都需要繼承IInterface接口。這個類剛開始看起來邏輯混亂,但實際上還是很清晰的,通過它我們可以清楚地瞭解到Binder地工作機制。這個類的結構其實很簡單,首先,它聲明瞭兩個方法getBookList和addBook,顯然這就是我們在IBookManager.aidl中所聲明的方法,同事它還剩嗎了兩個整型的id,分別用於標識這兩個方法,這兩個id用於標識在transact過程中客戶端所請求的到底是哪個方法。接着,它聲明瞭一個內部類Stub,這個Stub就是一個Binder類,當客戶端和服務端都位同一進程時,方法調用不會走跨進程的transact過程,而兩者位於不同進程時,方法調用需要走transact過程,這個邏輯由Stub內部代理類Proxy來完成。這麼看來IBookManager這個接口的確很簡單,但是我們也應該認識到,這個接口的很小實現就是它的內部類Stub和Stub的內部代理類Proxy,下面詳細介紹針對這兩個類的每個方法的含義。
DESCRIPTOR
Binder的唯一標識符,一般用當前Binder的類名錶示,比如本例中的"test.chenj.com.chapter2.IBookManager".
Binder的唯一標識符,一般用當前Binder的類名錶示,比如本例中的"test.chenj.com.chapter2.IBookManager".
asInterface(android.os.IBinder obj)
用於將服務端的Binder對象轉換成客戶端所需要的AIDL接口類型的對象,這種轉化過程時區分進程的,如果位於統一進程,那麼此方法返回的就是服務端的Stub對象本身,否則返回封裝好的Stub.proxy對象。
asBinder
此方法用於返回返回當前Binder對象。
onTransact
這個方法於系在服務端中的Binder線程池中,當客戶端發起跨進程請求時,遠程請求會通過系統底層封裝後交由此方法來處理。該方法的原型爲public boolean onTransact(int code, android.os.Parcel data,android.os.Parcel reply, int flags)。服務器端通過code可以確定客戶端所所請求的目標的方法是什麼,接着從data中取出目標方法所需要的參數,就像reply中寫入返回值(如果目標中有返回值的話)。需要注意的是,如果此方法返回false,那麼客戶端的請求會失敗,因此我們可以利用這個特性來做權限驗證,畢竟我們也不希望隨便一個進程都能遠程調用我們的服務。
Proxy #getBookList
這個方法運行在客戶端,當客戶端調用此方法時,它的內部實現時這樣的:首先創建該方法所需要的輸入性Parcel對象_data、輸出型Parcel對象和返回對象List;然後把該方法寫入data中(如果有參數的話);接着調用transact方法來發起RPC(遠程過程調用)請求,同事當前線程掛起;然後服務端的ontransact方法會被調用,知道RPC過程返回後,當前線程繼續執行,並從_reply中取出RPC過程的返回結果;最後_reply中的數據返回。
Proxy #addBook
這個方法運行在客戶端,它的執行過程和getBookList是一樣的,addBook沒有返回值,所以它不需要從_reply中取出返回值。
通過上面的分析,相信已經瞭解了Binder的工作機制,但是有兩點還是需要額外說明一下:首先,當客戶端發起遠程請求時,由於當前線程會被掛起至服務端進程返回數據,所以如果一個遠程方法時很耗時的,那麼不能在UI線程中發起此遠程請求;其次,由於服務端的Binder方法運行在Binder的線程池中,所以Binder方法不管是否耗時都應該採用同步的方式取實現,因爲它已經運行在一個線程中了。爲了更好的說明Binder,下面給出一個Binder的工作機制圖,如下所示。
從上述分析過程來看,我們完全可以不提供AIDL文件即可實現Binder,之所以提供AIDL文件,就是爲了方便系統爲我們生成代碼。系統根據AIDL文件生成java文件的格式是固定的,我們可以拋開AIDL文件直接寫一個Binder出來,接下來我們就介紹如何手動寫一個Binder。還是上面的例子,但是這次我們不提供AIDL文件。操靠上面系統自動生成的IBookManager.java這個類的代碼,可以發現這個類是相當有規律的,根據它的特點,我們完全可以自己寫一個和他一模一樣的類出來,然後這個不借助AIDL文件的Binder就完成了。但是我們發現系統生成的類看起來結構不清晰,我們想試着對他進行結構上的調整,可以發現這個類主要由兩部分構成,首先它本身就是一個Binder的接口(繼承了IInterface),其次它的內部有個Stub類這個類就是Binder。還記得我們怎麼寫Binder的服務端嗎?代碼如下所示。
public class MyService extends Service {
private List<Book> mBookList;
public MyService() {
mBookList = new ArrayList<>();
mBookList.add(new Book(1,"第一行代碼"));
mBookList.add(new Book(2,"羣英傳"));
mBookList.add(new Book(3,"開發藝術"));
}
private final IBookManager.Stub mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
synchronized (mBookList) {
return mBookList;
}
}
@Override
public void addBook(Book book) throws RemoteException {
synchronized (mBookList) {
if (!mBookList.contains(book)){
mBookList.add(book);
}
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
首先我們會實現一個穿件Stub對象並在內部實現IBookManager的接口方法,然後再Service的onBind中返回這個Stub對象。因此,從這一點來看,我們完全可以把Stub類提取出來直接作爲一個獨立的Binder類來實現,這樣IBookManager中就只剩接口本身了,通過這種分離式的方式可以讓它的結構變得清晰。
接下來,我們介紹Binder的兩個很重要的方法linkToDeath和unlinkToDeath。我們知道,Binder運行再服務端程序,如果服務端進程由於某種原因異常終止,這個時候我們到服務端的Binder連接斷裂(稱之爲Binder死亡),會導致我們的遠程調用失敗。更爲關鍵的是,如果我們不知道Binder連接已經斷裂,那麼客戶端的功能就會收到影響。爲了解決這個問題,Binder中提供了兩個配對的方法linkToDeath和unlinkToDeath,通過linkToDeath我們可以爲Binder設置一個死亡代理,當Binder死亡時,我們就會收到通知,這個時候我們就可以重啓發起連接請求從而恢復連接。那麼到底如何給Binder設置死亡代理呢?也很簡單。
首先聲明一個DeathRecipient對象。DeathRecipient時一個接口,其內部只有一個方法binderDied,我們需要實現這個方法,當Binder死亡的時候,系統就會回調binderDied方法,然後我們就可以移除之前綁定的binder代理並重新綁定遠程服務。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { if (mBookManager == null) { return; } mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0); mBookManager = null; //這裏重新綁定Service } };
其次再客戶端綁定遠程服務成功後,給biner設置死亡代理:
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
其中linkToDeath的第二個參數是個標記位,我們直接設爲0即可。經過上面兩個步驟,就給我們的Binder設置了死亡代理,當Binder死亡的時候,我們就可以收到通知了。另外,通過Binder方法isBinderAlive也可以判斷Binder是否是我。