段子
公園里,一位仙風(fēng)鶴骨的老者在打太極泣洞,一招一式都仙氣十足忧风,一個(gè)年輕人走過(guò)去:“大爺,太極這玩意兒花拳繡腿球凰,你練它干啥狮腿?”老者淡淡一笑:“年輕人,你還沒(méi)有領(lǐng)悟到太極的真諦呕诉,這樣蚤霞,你用最大力氣打我試試∫宥ぃ”于是年輕人用力打了老頭一拳,被訛了八萬(wàn)六规肴。
前言
序列化使用很簡(jiǎn)單捶闸,但是其中的一些細(xì)節(jié)并不是所有人都清楚。在日常的應(yīng)用開(kāi)發(fā)中拖刃,我們可能需要讓某些對(duì)象離開(kāi)內(nèi)存空間删壮,存儲(chǔ)到物理磁盤,以便長(zhǎng)期保存兑牡,同時(shí)也能減少對(duì)內(nèi)存的壓力央碟,而在需要時(shí)再將其從磁盤讀取到內(nèi)存,比如將某個(gè)特定的對(duì)象保存到文件中均函,隔一段時(shí)間后再把它讀取到內(nèi)存中使用亿虽,那么該對(duì)象就需要實(shí)現(xiàn)序列化操作,在java中可以使用Serializable接口實(shí)現(xiàn)對(duì)象的序列化洛勉,而在android中既可以使用Serializable接口實(shí)現(xiàn)對(duì)象序列化也可以使用Parcelable接口實(shí)現(xiàn)對(duì)象序列化粘秆,但是在內(nèi)存操作時(shí)更傾向于實(shí)現(xiàn)Parcelable接口,這樣會(huì)使用傳輸效率更高效收毫。接下來(lái)我們將分別詳細(xì)地介紹這樣兩種序列化操作攻走。
序列化與反序列
首先來(lái)了解一下序列化與反序列化。
(1)序列化
由于存在于內(nèi)存中的對(duì)象都是暫時(shí)的此再,無(wú)法長(zhǎng)期駐存昔搂,為了把對(duì)象的狀態(tài)保持下來(lái),這時(shí)需要把對(duì)象寫入到磁盤或者其他介質(zhì)中输拇,這個(gè)過(guò)程就叫做序列化摘符。
(2)反序列化
反序列化恰恰是序列化的反向操作,也就是說(shuō)淳附,把已存在在磁盤或者其他介質(zhì)中的對(duì)象议慰,反序列化(讀取)到內(nèi)存中奴曙,以便后續(xù)操作别凹,而這個(gè)過(guò)程就叫做反序列化。
概括性來(lái)說(shuō)序列化是指將對(duì)象實(shí)例的狀態(tài)存儲(chǔ)到存儲(chǔ)媒體(磁盤或者其他介質(zhì))的過(guò)程洽糟。在此過(guò)程中炉菲,先將對(duì)象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉(zhuǎn)換為字節(jié)流,然后再把字節(jié)流寫入數(shù)據(jù)流坤溃。在隨后對(duì)對(duì)象進(jìn)行反序列化時(shí)拍霜,將創(chuàng)建出與原對(duì)象完全相同的副本。
(3)實(shí)現(xiàn)序列化的必要條件
一個(gè)對(duì)象要實(shí)現(xiàn)序列化操作薪介,該類就必須實(shí)現(xiàn)了Serializable接口或者Parcelable接口祠饺,其中Serializable接口是在java中的序列化抽象類,而Parcelable接口則是android中特有的序列化接口汁政,在某些情況下道偷,Parcelable接口實(shí)現(xiàn)的序列化更為高效,關(guān)于它們的實(shí)現(xiàn)案例我們后續(xù)會(huì)分析记劈,這里只要清楚知道實(shí)現(xiàn)序列化操作時(shí)必須實(shí)現(xiàn)Serializable接口或者Parcelable接口之一即可勺鸦。
(4)序列化的應(yīng)用情景
主要有以下情況(但不限于以下情況)
1)內(nèi)存中的對(duì)象寫入到硬盤;
2)用套接字在網(wǎng)絡(luò)上傳送對(duì)象目木;
Serializable
Serializable是java提供的一個(gè)序列化接口换途,它是一個(gè)空接口,專門為對(duì)象提供標(biāo)準(zhǔn)的序列化和反序列化操作,使用Serializable實(shí)現(xiàn)類的序列化比較簡(jiǎn)單军拟,只要在類聲明中實(shí)現(xiàn)Serializable接口即可剃执,同時(shí)強(qiáng)烈建議聲明序列化標(biāo)識(shí)。如下:
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類實(shí)現(xiàn)的Serializable接口并聲明了序列化標(biāo)識(shí)serialVersionUID忠蝗,該ID由編輯器生成,當(dāng)然也可以自定義漓拾,如1L阁最,5L,不過(guò)還是建議使用編輯器生成唯一標(biāo)識(shí)符骇两。那么serialVersionUID有什么作用呢速种?實(shí)際上我們不聲明serialVersionUID也是可以的,因?yàn)樵谛蛄谢^(guò)程中會(huì)自動(dòng)生成一個(gè)serialVersionUID來(lái)標(biāo)識(shí)序列化對(duì)象低千。既然如此配阵,那我們還需不需要要指定呢?原因是serialVersionUID是用來(lái)輔助序列化和反序列化過(guò)程的示血,原則上序列化后的對(duì)象中serialVersionUID只有和當(dāng)前類的serialVersionUID相同才能夠正常被反序列化棋傍,也就是說(shuō)序列化與反序列化的serialVersionUID必須相同才能夠使序列化操作成功。具體過(guò)程是這樣的:序列化操作的時(shí)候系統(tǒng)會(huì)把當(dāng)前類的serialVersionUID寫入到序列化文件中难审,當(dāng)反序列化時(shí)系統(tǒng)會(huì)去檢測(cè)文件中的serialVersionUID瘫拣,判斷它是否與當(dāng)前類的serialVersionUID一致,如果一致就說(shuō)明序列化類的版本與當(dāng)前類版本是一樣的告喊,可以反序列化成功麸拄,否則失敗。報(bào)出如下UID錯(cuò)誤:
Exception in thread "main" java.io.InvalidClassException: com.zejian.test.Client;
local class incompatible: stream classdesc serialVersionUID = -2083503801443301445,
local class serialVersionUID = -4083503801443301445
因此強(qiáng)烈建議指定serialVersionUID黔姜,這樣的話即使微小的變化也不會(huì)導(dǎo)致crash的出現(xiàn)拢切,如果不指定的話只要這個(gè)文件多一個(gè)空格,系統(tǒng)自動(dòng)生成的UID就會(huì)截然不同的秆吵,反序列化也就會(huì)失敗淮椰。ok~,了解這么多纳寂,下面來(lái)看一個(gè)如何進(jìn)行對(duì)象序列化和反序列化的列子:
public class Demo {
public static void main(String[] args) throws Exception {
// 構(gòu)造對(duì)象
User user = new User();
user.setId(1000);
user.setName("韓梅梅");
// 把對(duì)象序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/serializable/user.txt"));
oos.writeObject(user);
oos.close();
// 反序列化到內(nèi)存
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();
}
}
輸出結(jié)果:
read serializable user:id=1000, name=韓梅梅
從代碼可以看出只需要ObjectOutputStream和ObjectInputStream就可以實(shí)現(xiàn)對(duì)象的序列化和反序列化操作实苞,通過(guò)流對(duì)象把user對(duì)象寫到文件中,并在需要時(shí)恢復(fù)userBack對(duì)象烈疚,但是兩者并不是同一個(gè)對(duì)象了,反序列化后的對(duì)象是新創(chuàng)建的聪轿。這里有兩點(diǎn)特別注意的是如果反序列類的成員變量的類型或者類名爷肝,發(fā)生了變化,那么即使serialVersionUID相同也無(wú)法正常反序列化成功。其次是靜態(tài)成員變量屬于類不屬于對(duì)象灯抛,不會(huì)參與序列化過(guò)程金赦,使用transient關(guān)鍵字標(biāo)記的成員變量也不參與序列化過(guò)程。
另外对嚼,系統(tǒng)的默認(rèn)序列化過(guò)程是可以改變的夹抗,通過(guò)實(shí)現(xiàn)如下4個(gè)方法,即可以控制系統(tǒng)的默認(rèn)序列化和反序列過(guò)程:
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;
}
/**
* 序列化時(shí),
* 首先系統(tǒng)會(huì)先調(diào)用writeReplace方法,在這個(gè)階段,
* 可以進(jìn)行自己操作,將需要進(jìn)行序列化的對(duì)象換成我們指定的對(duì)象.
* 一般很少重寫該方法
*/
private Object writeReplace() throws ObjectStreamException {
System.out.println("writeReplace invoked");
return this;
}
/**
*接著系統(tǒng)將調(diào)用writeObject方法,
* 來(lái)將對(duì)象中的屬性一個(gè)個(gè)進(jìn)行序列化,
* 我們可以在這個(gè)方法中控制住哪些屬性需要序列化.
* 這里只序列化name屬性
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("writeObject invoked");
out.writeObject(this.name == null ? "默認(rèn)值" : this.name);
}
/**
* 反序列化時(shí),系統(tǒng)會(huì)調(diào)用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,
* 反序列化回來(lái).然后通過(guò)readResolve方法,我們也可以指定系統(tǒng)返回給我們特定的對(duì)象
* 可以不是writeReplace序列化時(shí)的對(duì)象,可以指定其他對(duì)象.
*/
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);
}
/**
* 通過(guò)readResolve方法,我們也可以指定系統(tǒng)返回給我們特定的對(duì)象
* 可以不是writeReplace序列化時(shí)的對(duì)象,可以指定其他對(duì)象.
* 一般很少重寫該方法
*/
private Object readResolve() throws ObjectStreamException {
System.out.println("readResolve invoked");
return this;
}
}
通過(guò)上面的4個(gè)方法纵竖,我們就可以隨意控制序列化的過(guò)程了漠烧,由于在大部分情況下我們都沒(méi)必要重寫這4個(gè)方法,因此這里我們也不過(guò)介紹了靡砌,只要知道有這么一回事就行已脓。ok~,對(duì)于Serializable的介紹就先到這里通殃。
Parcelable
鑒于Serializable在內(nèi)存序列化上開(kāi)銷比較大度液,而內(nèi)存資源屬于android系統(tǒng)中的稀有資源(android系統(tǒng)分配給每個(gè)應(yīng)用的內(nèi)存開(kāi)銷都是有限的),為此android中提供了Parcelable接口來(lái)實(shí)現(xiàn)序列化操作画舌,Parcelable的性能比Serializable好堕担,在內(nèi)存開(kāi)銷方面較小,所以在內(nèi)存間數(shù)據(jù)傳輸時(shí)推薦使用Parcelable曲聂,如通過(guò)Intent在activity間傳輸數(shù)據(jù)霹购,而Parcelable的缺點(diǎn)就使用起來(lái)比較麻煩,下面給出一個(gè)Parcelable接口的實(shí)現(xiàn)案例句葵,大家感受一下:
public class User implements Parcelable {
public int id;
public String name;
public User friend;
/**
* 當(dāng)前對(duì)象的內(nèi)容描述,一般返回0即可
*/
@Override
public int describeContents() {
return 0;
}
/**
* 將當(dāng)前對(duì)象寫入序列化結(jié)構(gòu)中
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeString(this.name);
dest.writeParcelable(this.friend, 0);
}
public NewClient() {}
/**
* 從序列化后的對(duì)象中創(chuàng)建原始對(duì)象
*/
protected NewClient(Parcel in) {
this.id = in.readInt();
this.name = in.readString();
//friend是另一個(gè)序列化對(duì)象厕鹃,此方法序列需要傳遞當(dāng)前線程的上下文類加載器,否則會(huì)報(bào)無(wú)法找到類的錯(cuò)誤
this.friend=in.readParcelable(Thread.currentThread().getContextClassLoader());
}
/**
* public static final一個(gè)都不能少乍丈,內(nèi)部對(duì)象CREATOR的名稱也不能改變剂碴,必須全部大寫。
* 重寫接口中的兩個(gè)方法:
* createFromParcel(Parcel in) 實(shí)現(xiàn)從Parcel容器中讀取傳遞數(shù)據(jù)值,封裝成Parcelable對(duì)象返回邏輯層轻专,
* newArray(int size) 創(chuàng)建一個(gè)類型為T忆矛,長(zhǎng)度為size的數(shù)組,供外部類反序列化本類數(shù)組使用请垛。
*/
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
/**
* 從序列化后的對(duì)象中創(chuàng)建原始對(duì)象
*/
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}
/**
* 創(chuàng)建指定長(zhǎng)度的原始對(duì)象數(shù)組
*/
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
從代碼可知催训,在序列化的過(guò)程中需要實(shí)現(xiàn)的功能有序列化和反序列以及內(nèi)容描述。其中writeToParcel方法實(shí)現(xiàn)序列化功能宗收,其內(nèi)部是通過(guò)Parcel的一系列write方法來(lái)完成的漫拭,接著通過(guò)CREATOR內(nèi)部對(duì)象來(lái)實(shí)現(xiàn)反序列化,其內(nèi)部通過(guò)createFromParcel方法來(lái)創(chuàng)建序列化對(duì)象并通過(guò)newArray方法創(chuàng)建數(shù)組混稽,最終利用Parcel的一系列read方法完成反序列化采驻,最后由describeContents完成內(nèi)容描述功能审胚,該方法一般返回0,僅當(dāng)對(duì)象中存在文件描述符時(shí)返回1礼旅。同時(shí)由于User是另一個(gè)序列化對(duì)象膳叨,因此在反序列化方法中需要傳遞當(dāng)前線程的上下文類加載器,否則會(huì)報(bào)無(wú)法找到類的錯(cuò)誤痘系。
??
簡(jiǎn)單用一句話概括來(lái)說(shuō)就是通過(guò)writeToParcel將我們的對(duì)象映射成Parcel對(duì)象菲嘴,再通過(guò)createFromParcel將Parcel對(duì)象映射成我們的對(duì)象。也可以將Parcel看成是一個(gè)類似Serliazable的讀寫流汰翠,通過(guò)writeToParcel把對(duì)象寫到流里面龄坪,在通過(guò)createFromParcel從流里讀取對(duì)象,這個(gè)過(guò)程需要我們自己來(lái)實(shí)現(xiàn)并且寫的順序和讀的順序必須一致奴璃。ok~悉默,到此Parcelable接口的序列化實(shí)現(xiàn)基本介紹完。
??
那么在哪里會(huì)使用到Parcelable對(duì)象呢苟穆?其實(shí)通過(guò)Intent傳遞復(fù)雜類型(如自定義引用類型數(shù)據(jù))的數(shù)據(jù)時(shí)就需要使用Parcelable對(duì)象抄课,如下是日常應(yīng)用中Intent關(guān)于Parcelable對(duì)象的一些操作方法,引用類型必須實(shí)現(xiàn)Parcelable接口才能通過(guò)Intent傳遞雳旅,而基本數(shù)據(jù)類型跟磨,String類型則可直接通過(guò)Intent傳遞而且Intent本身也實(shí)現(xiàn)了Parcelable接口,所以可以輕松地在組件間進(jìn)行傳輸攒盈。
方法名稱 | 含義 |
---|---|
putExtra(String name, Parcelable value) | 設(shè)置自定義類型并實(shí)現(xiàn)Parcelable的對(duì)象 |
putExtra(String name, Parcelable[] value) | 設(shè)置自定義類型并實(shí)現(xiàn)Parcelable的對(duì)象數(shù)組 |
putParcelableArrayListExtra(String name, ArrayList value) | 設(shè)置List數(shù)組抵拘,其元素必須是實(shí)現(xiàn)了Parcelable接口的數(shù)據(jù) |
除了以上的Intent外系統(tǒng)還為我們提供了其他實(shí)現(xiàn)Parcelable接口的類,再如Bundle型豁、Bitmap僵蛛,它們都是可以直接序列化的,因此我們可以方便地使用它們?cè)诮M件間進(jìn)行數(shù)據(jù)傳遞迎变,當(dāng)然Bundle本身也是一個(gè)類似鍵值對(duì)的容器充尉,也可存儲(chǔ)Parcelable實(shí)現(xiàn)類,其API方法跟Intent基本相似衣形,由于這些屬于android基礎(chǔ)知識(shí)點(diǎn)驼侠,這里我們就不過(guò)多介紹了。
Parcelable 與 Serializable 區(qū)別
(1)兩者的實(shí)現(xiàn)差異
Serializable的實(shí)現(xiàn)谆吴,只需要實(shí)現(xiàn)Serializable接口即可倒源。這只是給對(duì)象打了一個(gè)標(biāo)記(UID),系統(tǒng)會(huì)自動(dòng)將其序列化句狼。而Parcelabel的實(shí)現(xiàn)笋熬,不僅需要實(shí)現(xiàn)Parcelabel接口,還需要在類中添加一個(gè)靜態(tài)成員變量CREATOR腻菇,這個(gè)變量需要實(shí)現(xiàn) Parcelable.Creator 接口突诬,并實(shí)現(xiàn)讀寫的抽象方法苫拍。
(2)兩者的設(shè)計(jì)初衷
Serializable的設(shè)計(jì)初衷是為了序列化對(duì)象到本地文件、數(shù)據(jù)庫(kù)旺隙、網(wǎng)絡(luò)流、RMI以便數(shù)據(jù)傳輸骏令,當(dāng)然這種傳輸可以是程序內(nèi)的也可以是兩個(gè)程序間的蔬捷。而Android的Parcelable的設(shè)計(jì)初衷是由于Serializable效率過(guò)低,消耗大榔袋,而android中數(shù)據(jù)傳遞主要是在內(nèi)存環(huán)境中(內(nèi)存屬于android中的稀有資源)周拐,因此Parcelable的出現(xiàn)為了滿足數(shù)據(jù)在內(nèi)存中低開(kāi)銷而且高效地傳遞問(wèn)題。
(3)兩者效率選擇
Serializable使用IO讀寫存儲(chǔ)在硬盤上凰兑。序列化過(guò)程使用了反射技術(shù)妥粟,并且期間產(chǎn)生臨時(shí)對(duì)象,優(yōu)點(diǎn)代碼少吏够,在將對(duì)象序列化到存儲(chǔ)設(shè)置中或?qū)?duì)象序列化后通過(guò)網(wǎng)絡(luò)傳輸時(shí)建議選擇Serializable勾给。
Parcelable是直接在內(nèi)存中讀寫,我們知道內(nèi)存的讀寫速度肯定優(yōu)于硬盤讀寫速度锅知,所以Parcelable序列化方式性能上要優(yōu)于Serializable方式很多播急。所以Android應(yīng)用程序在內(nèi)存間數(shù)據(jù)傳輸時(shí)推薦使用Parcelable,如activity間傳輸數(shù)據(jù)和AIDL數(shù)據(jù)傳遞售睹。大多數(shù)情況下使用Serializable也是沒(méi)什么問(wèn)題的桩警,但是針對(duì)Android應(yīng)用程序在內(nèi)存間數(shù)據(jù)傳輸還是建議大家使用Parcelable方式實(shí)現(xiàn)序列化,畢竟性能好很多昌妹,其實(shí)也沒(méi)多麻煩捶枢。
Parcelable也不是不可以在網(wǎng)絡(luò)中傳輸,只不過(guò)實(shí)現(xiàn)和操作過(guò)程過(guò)于麻煩并且為了防止android版本不同而導(dǎo)致Parcelable可能不同的情況飞崖,因此在序列化到存儲(chǔ)設(shè)備或者網(wǎng)絡(luò)傳輸方面還是盡量選擇Serializable接口烂叔。
AndroidStudio中的快捷生成方式
(1)AndroidStudio快捷生成Parcelable代碼
在程序開(kāi)發(fā)過(guò)程中,我們實(shí)現(xiàn)Parcelable接口的代碼都是類似的蚜厉,如果我們每次實(shí)現(xiàn)一個(gè)Parcelable接口類长已,就得去編寫一次重復(fù)的代碼,這顯然是不可取的昼牛,不過(guò)幸運(yùn)的是术瓮,android studio 提供了自動(dòng)實(shí)現(xiàn)Parcelable接口的方法的插件,相當(dāng)實(shí)現(xiàn)贰健,我們只需要打開(kāi)Setting胞四,找到plugin插件,然后搜索Parcelable插件伶椿,最后找到android Parcelable code generator 安裝即可:
重啟android studio后辜伟,我們創(chuàng)建一個(gè)User類氓侧,如下:
public class User {
public int id;
public int age;
public String name;
}
然后使用剛剛安裝的插件協(xié)助我們生成實(shí)現(xiàn)Parcelable接口的代碼,window快捷鍵:Alt+Insert导狡,Mac快捷鍵:cmd+n约巷,如下:
最后結(jié)果如下:
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是默認(rèn)關(guān)閉serialVersionUID生成提示的旱捧,我們需要打開(kāi)setting独郎,找到檢測(cè)(Inspections選項(xiàng)),開(kāi)啟 Serializable class without serialVersionUID 檢測(cè)即可枚赡,如下:
然后新建User類實(shí)現(xiàn)Serializable接口氓癌,右側(cè)會(huì)提示添加serialVersionUID,如下:
鼠標(biāo)放在類名上贫橙,Alt+Enter(Mac:cmd+Enter)贪婉,快捷代碼提示,生成serialVersionUID即可:
最終生成結(jié)果:
public class User implements Serializable {
private static final long serialVersionUID = 6748592377066215128L;
public int id;
public int age;
public String name;
}
總結(jié)
以上就是Android序列化的全部?jī)?nèi)容卢肃,很簡(jiǎn)單疲迂,但是也有細(xì)節(jié)。我有一個(gè)想法践剂,就是后面專門寫一些表面很簡(jiǎn)單但是細(xì)節(jié)可能不清楚的知識(shí)點(diǎn)鬼譬,我們不要始終把目光聚集在大框架上、高端前沿技術(shù)什么的逊脯,偶爾研究研究基礎(chǔ)的東西也不錯(cuò)优质。
我的博客即將搬運(yùn)同步至騰訊云+社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1i1t9pd69ux2t