段子
公園里,一位仙風鶴骨的老者在打太極,一招一式都仙氣十足,一個年輕人走過去:“大爺,太極這玩意兒花拳繡腿,你練它干啥?”老者淡淡一笑:“年輕人,你還沒有領悟到太極的真諦,這樣,你用最大力氣打我試試?!庇谑悄贻p人用力打了老頭一拳,被訛了八萬六。
前言
序列化使用很簡單,但是其中的一些細節并不是所有人都清楚。在日常的應用開發中,我們可能需要讓某些對象離開內存空間,存儲到物理磁盤,以便長期保存,同時也能減少對內存的壓力,而在需要時再將其從磁盤讀取到內存,比如將某個特定的對象保存到文件中,隔一段時間后再把它讀取到內存中使用,那么該對象就需要實現序列化操作,在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 studio后,我們創建一個User類,如下:
public class User {
public int id;
public int age;
public String name;
}
然后使用剛剛安裝的插件協助我們生成實現Parcelable接口的代碼,window快捷鍵:Alt+Insert,Mac快捷鍵:cmd+n,如下:
最后結果如下:
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 檢測即可,如下:
然后新建User類實現Serializable接口,右側會提示添加serialVersionUID,如下:
鼠標放在類名上,Alt+Enter(Mac:cmd+Enter),快捷代碼提示,生成serialVersionUID即可:
最終生成結果:
public class User implements Serializable {
private static final long serialVersionUID = 6748592377066215128L;
public int id;
public int age;
public String name;
}
總結
以上就是Android序列化的全部內容,很簡單,但是也有細節。我有一個想法,就是后面專門寫一些表面很簡單但是細節可能不清楚的知識點,我們不要始終把目光聚集在大框架上、高端前沿技術什么的,偶爾研究研究基礎的東西也不錯。
我的博客即將搬運同步至騰訊云+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1i1t9pd69ux2t