十年Android程序員:Java語言進階,必須會的-Android序列化總結

Java 程序員 Android Android超級程序員 2019-07-09

前言

公園裡,一位仙風鶴骨的老者在打太極,一招一式都仙氣十足,一個年輕人走過去:“大爺,太極這玩意兒花拳繡腿,你練它幹啥?”老者淡淡一笑:“年輕人,你還沒有領悟到太極的真諦,這樣,你用最大力氣打我試試。”於是年輕人用力打了老頭一拳,被訛了八萬六。

從段子就能看出來,今天又是一碗炒冷飯。序列化使用很簡單,但是其中的一些細節並不是所有人都清楚。在日常的應用開發中,我們可能需要讓某些對象離開內存空間,存儲到物理磁盤,以便長期保存,同時也能減少對內存的壓力,而在需要時再將其從磁盤讀取到內存,比如將某個特定的對象保存到文件中,隔一段時間後再把它讀取到內存中使用,那麼該對象就需要實現序列化操作,在java中可以使用Serializable接口實現對象的序列化,而在android中既可以使用Serializable接口實現對象序列化也可以使用Parcelable接口實現對象序列化,但是在內存操作時更傾向於實現Parcelable接口,這樣會使用傳輸效率更高效。接下來我們將分別詳細地介紹這樣兩種序列化操作。

序列化與反序列

首先來了解一下序列化與反序列化。

(1)序列化

由於存在於內存中的對象都是暫時的,無法長期駐存,為了把對象的狀態保持下來,這時需要把對象寫入到磁盤或者其他介質中,這個過程就叫做序列化。

(2)反序列化

反序列化恰恰是序列化的反向操作,也就是說,把已存在在磁盤或者其他介質中的對象,反序列化(讀取)到內存中,以便後續操作,而這個過程就叫做反序列化。

概括性來說序列化是指將對象實例的狀態存儲到存儲媒體(磁盤或者其他介質)的過程。在此過程中,先將對象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉換為字節流,然後再把字節流寫入數據流。在隨後對對象進行反序列化時,將創建出與原對象完全相同的副本。

(3)實現序列化的必要條件

一個對象要實現序列化操作,該類就必須實現了Serializable接口或者Parcelable接口,其中Serializable接口是在java中的序列化抽象類,而Parcelable接口則是android中特有的序列化接口,在某些情況下,Parcelable接口實現的序列化更為高效,關於它們的實現案例我們後續會分析,這裡只要清楚知道實現序列化操作時必須實現Serializable接口或者Parcelable接口之一即可。

(4)序列化的應用情景

主要有以下情況(但不限於以下情況)

1)內存中的對象寫入到硬盤;

2)用套接字在網絡上傳送對象;

Serializable

Serializable是java提供的一個序列化接口,它是一個空接口,專門為對象提供標準的序列化和反序列化操作,使用Serializable實現類的序列化比較簡單,只要在類聲明中實現Serializable接口即可,同時強烈建議聲明序列化標識。如下:

public class User implements Serializable {
private static final long serialVersionUID = -2083503801443301445L;
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

如上述代碼所示,User類實現的Serializable接口並聲明瞭序列化標識serialVersionUID,該ID由編輯器生成,當然也可以自定義,如1L,5L,不過還是建議使用編輯器生成唯一標識符。那麼serialVersionUID有什麼作用呢?實際上我們不聲明serialVersionUID也是可以的,因為在序列化過程中會自動生成一個serialVersionUID來標識序列化對象。既然如此,那我們還需不需要要指定呢?原因是serialVersionUID是用來輔助序列化和反序列化過程的,原則上序列化後的對象中serialVersionUID只有和當前類的serialVersionUID相同才能夠正常被反序列化,也就是說序列化與反序列化的serialVersionUID必須相同才能夠使序列化操作成功。具體過程是這樣的:序列化操作的時候系統會把當前類的serialVersionUID寫入到序列化文件中,當反序列化時系統會去檢測文件中的serialVersionUID,判斷它是否與當前類的serialVersionUID一致,如果一致就說明序列化類的版本與當前類版本是一樣的,可以反序列化成功,否則失敗。報出如下UID錯誤:

Exception in thread "main" java.io.InvalidClassException: com.zejian.test.Client; 
local class incompatible: stream classdesc serialVersionUID = -2083503801443301445,
local class serialVersionUID = -4083503801443301445

因此強烈建議指定serialVersionUID,這樣的話即使微小的變化也不會導致crash的出現,如果不指定的話只要這個文件多一個空格,系統自動生成的UID就會截然不同的,反序列化也就會失敗。ok~,瞭解這麼多,下面來看一個如何進行對象序列化和反序列化的列子:

public class Demo {
public static void main(String[] args) throws Exception {
// 構造對象
User user = new User();
user.setId(1000);
user.setName("韓梅梅");
// 把對象序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/serializable/user.txt"));
oos.writeObject(user);
oos.close();
// 反序列化到內存
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/serializable/user.txt"));
User userBack = (User) ois.readObject();
System.out.println("read serializable user:id=" + userBack.getId() + ", name=" + userBack.getName());
ois.close();
}
}

輸出結果:

read serializable user:id=1000, name=韓梅梅

從代碼可以看出只需要ObjectOutputStream和ObjectInputStream就可以實現對象的序列化和反序列化操作,通過流對象把user對象寫到文件中,並在需要時恢復userBack對象,但是兩者並不是同一個對象了,反序列化後的對象是新創建的。這裡有兩點特別注意的是如果反序列類的成員變量的類型或者類名,發生了變化,那麼即使serialVersionUID相同也無法正常反序列化成功。其次是靜態成員變量屬於類不屬於對象,不會參與序列化過程,使用transient關鍵字標記的成員變量也不參與序列化過程。

另外,系統的默認序列化過程是可以改變的,通過實現如下4個方法,即可以控制系統的默認序列化和反序列過程:

public class User implements Serializable {
private static final long serialVersionUID = -4083503801443301445L;
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 序列化時,
* 首先系統會先調用writeReplace方法,在這個階段,
* 可以進行自己操作,將需要進行序列化的對象換成我們指定的對象.
* 一般很少重寫該方法
*/
private Object writeReplace() throws ObjectStreamException {
System.out.println("writeReplace invoked");
return this;
}
/**
*接著系統將調用writeObject方法,
* 來將對象中的屬性一個個進行序列化,
* 我們可以在這個方法中控制住哪些屬性需要序列化.
* 這裡只序列化name屬性
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("writeObject invoked");
out.writeObject(this.name == null ? "默認值" : this.name);
}
/**
* 反序列化時,系統會調用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,
* 反序列化回來.然後通過readResolve方法,我們也可以指定系統返回給我們特定的對象
* 可以不是writeReplace序列化時的對象,可以指定其他對象.
*/
private void readObject(java.io.ObjectInputStream in) throws IOException,
ClassNotFoundException {
System.out.println("readObject invoked");
this.name = (String) in.readObject();
System.out.println("got name:" + name);
}
/**
* 通過readResolve方法,我們也可以指定系統返回給我們特定的對象
* 可以不是writeReplace序列化時的對象,可以指定其他對象.
* 一般很少重寫該方法
*/
private Object readResolve() throws ObjectStreamException {
System.out.println("readResolve invoked");
return this;
}
}

通過上面的4個方法,我們就可以隨意控制序列化的過程了,由於在大部分情況下我們都沒必要重寫這4個方法,因此這裡我們也不過介紹了,只要知道有這麼一回事就行。ok~,對於Serializable的介紹就先到這裡。

Parcelable

鑑於Serializable在內存序列化上開銷比較大,而內存資源屬於android系統中的稀有資源(android系統分配給每個應用的內存開銷都是有限的),為此android中提供了Parcelable接口來實現序列化操作,Parcelable的性能比Serializable好,在內存開銷方面較小,所以在內存間數據傳輸時推薦使用Parcelable,如通過Intent在activity間傳輸數據,而Parcelable的缺點就使用起來比較麻煩,下面給出一個Parcelable接口的實現案例,大家感受一下:

public class User implements Parcelable {
public int id;
public String name;
public User friend;
/**
* 當前對象的內容描述,一般返回0即可
*/
@Override
public int describeContents() {
return 0;
}
/**
* 將當前對象寫入序列化結構中
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeString(this.name);
dest.writeParcelable(this.friend, 0);
}
public NewClient() {}
/**
* 從序列化後的對象中創建原始對象
*/
protected NewClient(Parcel in) {
this.id = in.readInt();
this.name = in.readString();
//friend是另一個序列化對象,此方法序列需要傳遞當前線程的上下文類加載器,否則會報無法找到類的錯誤
this.friend=in.readParcelable(Thread.currentThread().getContextClassLoader());
}
/**
* public static final一個都不能少,內部對象CREATOR的名稱也不能改變,必須全部大寫。
* 重寫接口中的兩個方法:
* createFromParcel(Parcel in) 實現從Parcel容器中讀取傳遞數據值,封裝成Parcelable對象返回邏輯層,
* newArray(int size) 創建一個類型為T,長度為size的數組,供外部類反序列化本類數組使用。
*/
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
/**
* 從序列化後的對象中創建原始對象
*/
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}
/**
* 創建指定長度的原始對象數組
*/
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}

從代碼可知,在序列化的過程中需要實現的功能有序列化和反序列以及內容描述。其中writeToParcel方法實現序列化功能,其內部是通過Parcel的一系列write方法來完成的,接著通過CREATOR內部對象來實現反序列化,其內部通過createFromParcel方法來創建序列化對象並通過newArray方法創建數組,最終利用Parcel的一系列read方法完成反序列化,最後由describeContents完成內容描述功能,該方法一般返回0,僅當對象中存在文件描述符時返回1。同時由於User是另一個序列化對象,因此在反序列化方法中需要傳遞當前線程的上下文類加載器,否則會報無法找到類的錯誤。

簡單用一句話概括來說就是通過writeToParcel將我們的對象映射成Parcel對象,再通過createFromParcel將Parcel對象映射成我們的對象。也可以將Parcel看成是一個類似Serliazable的讀寫流,通過writeToParcel把對象寫到流裡面,在通過createFromParcel從流裡讀取對象,這個過程需要我們自己來實現並且寫的順序和讀的順序必須一致。ok~,到此Parcelable接口的序列化實現基本介紹完。

那麼在哪裡會使用到Parcelable對象呢?其實通過Intent傳遞複雜類型(如自定義引用類型數據)的數據時就需要使用Parcelable對象,如下是日常應用中Intent關於Parcelable對象的一些操作方法,引用類型必須實現Parcelable接口才能通過Intent傳遞,而基本數據類型,String類型則可直接通過Intent傳遞而且Intent本身也實現了Parcelable接口,所以可以輕鬆地在組件間進行傳輸。

方法名稱含義putExtra(String name, Parcelable value)設置自定義類型並實現Parcelable的對象putExtra(String name, Parcelable[] value)設置自定義類型並實現Parcelable的對象數組putParcelableArrayListExtra(String name, ArrayList value)設置List數組,其元素必須是實現了Parcelable接口的數據

除了以上的Intent外系統還為我們提供了其他實現Parcelable接口的類,再如Bundle、Bitmap,它們都是可以直接序列化的,因此我們可以方便地使用它們在組件間進行數據傳遞,當然Bundle本身也是一個類似鍵值對的容器,也可存儲Parcelable實現類,其API方法跟Intent基本相似,由於這些屬於android基礎知識點,這裡我們就不過多介紹了。

Parcelable 與 Serializable 區別

(1)兩者的實現差異

Serializable的實現,只需要實現Serializable接口即可。這只是給對象打了一個標記(UID),系統會自動將其序列化。而Parcelabel的實現,不僅需要實現Parcelabel接口,還需要在類中添加一個靜態成員變量CREATOR,這個變量需要實現 Parcelable.Creator 接口,並實現讀寫的抽象方法。

(2)兩者的設計初衷

Serializable的設計初衷是為了序列化對象到本地文件、數據庫、網絡流、RMI以便數據傳輸,當然這種傳輸可以是程序內的也可以是兩個程序間的。而Android的Parcelable的設計初衷是由於Serializable效率過低,消耗大,而android中數據傳遞主要是在內存環境中(內存屬於android中的稀有資源),因此Parcelable的出現為了滿足數據在內存中低開銷而且高效地傳遞問題。

(3)兩者效率選擇

Serializable使用IO讀寫存儲在硬盤上。序列化過程使用了反射技術,並且期間產生臨時對象,優點代碼少,在將對象序列化到存儲設置中或將對象序列化後通過網絡傳輸時建議選擇Serializable。

Parcelable是直接在內存中讀寫,我們知道內存的讀寫速度肯定優於硬盤讀寫速度,所以Parcelable序列化方式性能上要優於Serializable方式很多。所以Android應用程序在內存間數據傳輸時推薦使用Parcelable,如activity間傳輸數據和AIDL數據傳遞。大多數情況下使用Serializable也是沒什麼問題的,但是針對Android應用程序在內存間數據傳輸還是建議大家使用Parcelable方式實現序列化,畢竟性能好很多,其實也沒多麻煩。

Parcelable也不是不可以在網絡中傳輸,只不過實現和操作過程過於麻煩並且為了防止android版本不同而導致Parcelable可能不同的情況,因此在序列化到存儲設備或者網絡傳輸方面還是儘量選擇Serializable接口。

AndroidStudio中的快捷生成方式

(1)AndroidStudio快捷生成Parcelable代碼

在程序開發過程中,我們實現Parcelable接口的代碼都是類似的,如果我們每次實現一個Parcelable接口類,就得去編寫一次重複的代碼,這顯然是不可取的,不過幸運的是,android studio 提供了自動實現Parcelable接口的方法的插件,相當實現,我們只需要打開Setting,找到plugin插件,然後搜索Parcelable插件,最後找到android Parcelable code generator 安裝即可:


十年Android程序員:Java語言進階,必須會的-Android序列化總結


重啟android studio後,我們創建一個User類,如下:

public class User {
public int id;
public int age;
public String name;
}

然後使用剛剛安裝的插件協助我們生成實現Parcelable接口的代碼,window快捷鍵:Alt+Insert,Mac快捷鍵:cmd+n,如下:


十年Android程序員:Java語言進階,必須會的-Android序列化總結


最後結果如下:

public class User implements Parcelable {
public int id;
public int age;
public String name;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeInt(this.age);
dest.writeString(this.name);
}
public User() {
}
protected User(Parcel in) {
this.id = in.readInt();
this.age = in.readInt();
this.name = in.readString();
}
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}

(2)AndroidStudio快捷生成Serializable的UID

在正常情況下,AS是默認關閉serialVersionUID生成提示的,我們需要打開setting,找到檢測(Inspections選項),開啟 Serializable class without serialVersionUID 檢測即可,如下:


十年Android程序員:Java語言進階,必須會的-Android序列化總結


然後新建User類實現Serializable接口,右側會提示添加serialVersionUID,如下:


十年Android程序員:Java語言進階,必須會的-Android序列化總結


鼠標放在類名上,Alt+Enter(Mac:cmd+Enter),快捷代碼提示,生成serialVersionUID即可:


十年Android程序員:Java語言進階,必須會的-Android序列化總結


最終生成結果:

public class User implements Serializable {
private static final long serialVersionUID = 6748592377066215128L;
public int id;
public int age;
public String name;
}

總結

以上就是Android序列化的全部內容,很簡單,但是也有細節。我有一個想法,就是後面專門寫一些表面很簡單但是細節可能不清楚的知識點,我們不要始終把目光聚集在大框架上、高端前沿技術什麼的,偶爾研究研究基礎的東西也不錯。

最後

如果你看到了這裡,覺得文章寫得不錯就給個讚唄?如果你覺得那裡值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。

最後針對Android程序員,小編這邊給大家整理了一些資料,其中分享內容包括不限於【高級UI、性能優化、移動架構師、NDK、混合式開發(ReactNative+Weex)微信小程序、Flutter等全方面的Android進階實踐技術】希望能幫助到大家,也節省大家在網上搜索資料的時間來學習,也可以分享動態給身邊好友一起學習!

十年Android程序員:Java語言進階,必須會的-Android序列化總結

十年Android程序員:Java語言進階,必須會的-Android序列化總結

為什麼某些人會一直比你優秀,是因為他本身就很優秀還一直在持續努力變得更優秀,而你是不是還在滿足於現狀內心在竊喜!希望讀到這的您能點個小贊關注下我,以後還會更新技術乾貨,謝謝您的支持!

轉發分享+關注,私信回覆【資料】獲取更多知識點

Android架構師之路很漫長,一起共勉吧!

相關推薦

推薦中...