什么是序列化/反序列化?
- 簡單來說就是將對象轉(zhuǎn)換為可以傳輸?shù)亩M(jìn)制流(二進(jìn)制序列)的過程,這樣我們就可以通過序列化,轉(zhuǎn)化為可以在網(wǎng)絡(luò)傳輸或者保存到本地的流(序列),從而進(jìn)行傳輸數(shù)據(jù) 癣亚。
- 那反序列化就是從二進(jìn)制流(序列)轉(zhuǎn)化為對象的過程.
- 由于存在于內(nèi)存中的對象都是暫時(shí)的撩鹿,無法長期駐存棉钧,為了把對象的狀態(tài)保持下來,這時(shí)需要把對象寫入到磁盤或者其他介質(zhì)中,這個(gè)過程就叫做序列化娩践。
- 反序列化恰恰是序列化的反向操作,也就是說烹骨,把已存在在磁盤或者其他介質(zhì)中的對象翻伺,反序列化(讀取)到內(nèi)存中沮焕,以便后續(xù)操作吨岭,而這個(gè)過程就叫做反序列化。
- 反序列化:把字節(jié)序列恢復(fù)為原先的Java對象峦树。
- 序列化:把Java對象轉(zhuǎn)換為字節(jié)序列辣辫。
什么時(shí)候使用序列化
- 數(shù)據(jù)傳輸?shù)臅r(shí)候
- 數(shù)據(jù)保存的時(shí)候
序列化是干啥用的?
-
序列化的原本意圖是希望對一個(gè)Java對象作一下“變換”魁巩,變成字節(jié)序列急灭,這樣一來方便持久化存儲到磁盤,避免程序運(yùn)行結(jié)束后對象就從內(nèi)存里消失谷遂,另外變換成字節(jié)序列也更便于網(wǎng)絡(luò)運(yùn)輸和傳播葬馋,所以概念上很好理解:
怎么序列化
- Android中實(shí)現(xiàn)序列化有兩個(gè)選擇:一是實(shí)現(xiàn)Serializable接口(是JavaSE本身就支持的),一是實(shí)現(xiàn)Parcelable接口(是Android特有功能肾扰,效率比實(shí)現(xiàn)Serializable接口高效畴嘶,可用于Intent數(shù)據(jù)傳遞,也可以用于進(jìn)程間通信(IPC))集晚。
- 實(shí)現(xiàn)Serializable接口非常簡單窗悯,聲明一下就可以了,而實(shí)現(xiàn)Parcelable接口稍微復(fù)雜一些甩恼,但效率更高蟀瞧,在一般的時(shí)候推薦用這種方法提高性能沉颂。
對象如何序列化?
- 舉個(gè)例子悦污,假如我們要對Student類對象序列化到一個(gè)名為student.txt的文本文件中铸屉,然后再通過文本文件反序列化成Student類對象:
1、Student類定義
public class Student implements Serializable {
private String name;
private Integer age;
private Integer score;
@Override
public String toString() {
return "Student:" + '\n' +
"name = " + this.name + '\n' +
"age = " + this.age + '\n' +
"score = " + this.score + '\n'
;
}
// ... 其他省略 ... 這里省略了set()切端、get()方法
}
2彻坛、序列化
public static void serialize( ) throws IOException {
Student student = new Student();
student.setName("CodeSheep");
student.setAge( 18 );
student.setScore( 1000 );
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File(context.getCacheDir()+"/student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
System.out.println("序列化成功!已經(jīng)生成student.txt文件");
System.out.println("==============================================");
}
3踏枣、反序列化
public static void deserialize( ) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File(context.getCacheDir()+"/student.txt") ) );
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
System.out.println("反序列化結(jié)果為:");
System.out.println( student );
}
4昌屉、運(yùn)行結(jié)果
序列化成功!已經(jīng)生成student.txt文件
==============================================
反序列化結(jié)果為:
Student:
name = CodeSheep
age = 18
score = 1000
Serializable接口
- 上面在定義Student類時(shí)茵瀑,實(shí)現(xiàn)了一個(gè)Serializable接口间驮,然而當(dāng)我們點(diǎn)進(jìn)Serializable接口內(nèi)部查看,發(fā)現(xiàn)它竟然是一個(gè)空接口马昨,并沒有包含任何方法竞帽!
public interface Serializable {
}
試想,如果上面在定義Student類時(shí)忘了加implements Serializable時(shí)會發(fā)生什么呢鸿捧?
-
實(shí)驗(yàn)結(jié)果是:此時(shí)的程序運(yùn)行會報(bào)錯(cuò)屹篓,并拋出NotSerializableException異常:
-
我們按照錯(cuò)誤提示,由源碼一直跟到ObjectOutputStream的writeObject0()方法底層一看匙奴,才恍然大悟:
如果一個(gè)對象既不是字符串堆巧、數(shù)組、枚舉泼菌,而且也沒有實(shí)現(xiàn)Serializable接口的話谍肤,在序列化時(shí)就會拋出NotSerializableException異常!
原來Serializable接口也僅僅只是做一個(gè)標(biāo)記用T詈洹Rシ小刷钢!
它告訴代碼只要是實(shí)現(xiàn)了Serializable接口的類都是可以被序列化的笋颤!然而真正的序列化動作不需要靠它完成。
serialVersionUID號有何用内地?
- 相信你一定經(jīng)嘲槌危看到有些類中定義了如下代碼行,即定義了一個(gè)名為serialVersionUID的字段:
private static final long serialVersionUID = -4392658638228508589L;
- 繼續(xù)來做一個(gè)簡單實(shí)驗(yàn)阱缓,還拿上面的Student類為例非凌,我們并沒有人為在里面顯式地聲明一個(gè)serialVersionUID字段。
- 我們首先還是調(diào)用上面的serialize()方法荆针,將一個(gè)Student對象序列化到本地磁盤上的student.txt文件:
- 接下來我們在Student類里面動點(diǎn)手腳敞嗡,比如在里面再增加一個(gè)名為studentID的字段颁糟,表示學(xué)生學(xué)號:新增一個(gè)字段 private Long studentId; // 表示學(xué)號
- 這時(shí)候,我們拿剛才已經(jīng)序列化到本地的student.txt文件喉悴,還用如下代碼進(jìn)行反序列化棱貌,試圖還原出剛才那個(gè)Student對象:
-
運(yùn)行發(fā)現(xiàn)報(bào)錯(cuò)了,并且拋出了InvalidClassException異常:
- 這地方提示的信息非常明確了:序列化前后的serialVersionUID號碼不兼容箕肃!
從這地方最起碼可以得出兩個(gè)重要信息:
- 1婚脱、serialVersionUID是序列化前后的唯一標(biāo)識符
- 2、默認(rèn)如果沒有人為顯式定義過serialVersionUID勺像,那編譯器會為它自動聲明一個(gè)障贸!
- serialVersionUID序列化ID,可以看成是序列化和反序列化過程中的“暗號”吟宦,在反序列化時(shí)篮洁,JVM會把字節(jié)流中的序列號ID和被序列化類中的序列號ID做比對,只有兩者一致殃姓,才能重新反序列化嘀粱,否則就會報(bào)異常來終止反序列化的過程。
- 如果在定義一個(gè)可序列化的類時(shí)辰狡,沒有人為顯式地給它定義一個(gè)serialVersionUID的話锋叨,則Java運(yùn)行時(shí)環(huán)境會根據(jù)該類的各方面信息自動地為它生成一個(gè)默認(rèn)的serialVersionUID,一旦像上面一樣更改了類的結(jié)構(gòu)或者信息宛篇,則類的serialVersionUID也會跟著變化娃磺!
- 所以,為了serialVersionUID的確定性叫倍,寫代碼時(shí)還是建議偷卧,凡是implements Serializable的類,都最好人為顯式地為它聲明一個(gè)serialVersionUID明確值吆倦!
- Android Studio 自動添加serialVersionUID http://www.reibang.com/p/9252e1e4e82e
Parcelable
AndroidStudio中的快捷生成方式
- 插件 android Parcelable code generator
Parcel的簡介 [?pɑ?rsl]
- 在介紹之前我們需要先了解Parcel是什么?Parcel翻譯過來是打包的意思,其實(shí)就是包裝了我們需要傳輸?shù)臄?shù)據(jù),然后在Binder中傳輸,也就是用于跨進(jìn)程傳輸數(shù)據(jù)听诸。
Parcel模型
- 簡單來說,Parcel提供了一套機(jī)制蚕泽,可以將序列化之后的數(shù)據(jù)寫入到一個(gè)共享內(nèi)存中晌梨,其他進(jìn)程通過Parcel可以從這塊共享內(nèi)存中讀出字節(jié)流,并反序列化成對象,下圖是這個(gè)過程的模型须妻。
-
Parcel可以包含原始數(shù)據(jù)類型(用各種對應(yīng)的方法寫入仔蝌,比如writeInt(),writeFloat()等),可以包含Parcelable對象荒吏,它還包含了一個(gè)活動的IBinder對象的引用敛惊,這個(gè)引用導(dǎo)致另一端接收到一個(gè)指向這個(gè)IBinder的代理IBinder。
Parcelable中的三大過程介紹(序列化,反序列化,描述)
/**
* ================================================
* 作 者:SharkZ
* 郵 箱:229153959@qq.com
* 創(chuàng)建日期:2020/8/26 22:51
* 描 述
* 修訂歷史:
* ================================================
*/
public class MyParcelable implements Parcelable {
private String paramsA;
private int paramsB;
private boolean paramsC;
public MyParcelable() {
}
public MyParcelable(String paramsA, int paramsB, boolean paramsC) {
this.paramsA = paramsA;
this.paramsB = paramsB;
this.paramsC = paramsC;
}
public String getParamsA() {
return paramsA;
}
public void setParamsA(String paramsA) {
this.paramsA = paramsA;
}
public int getParamsB() {
return paramsB;
}
public void setParamsB(int paramsB) {
this.paramsB = paramsB;
}
public boolean isParamsC() {
return paramsC;
}
public void setParamsC(boolean paramsC) {
this.paramsC = paramsC;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.paramsA);
dest.writeInt(this.paramsB);
dest.writeByte(this.paramsC ? (byte) 1 : (byte) 0);
}
protected MyParcelable(Parcel in) {
this.paramsA = in.readString();
this.paramsB = in.readInt();
this.paramsC = in.readByte() != 0;
}
public static final Creator<MyParcelable> CREATOR = new Creator<MyParcelable>() {
@Override
public MyParcelable createFromParcel(Parcel source) {
return new MyParcelable(source);
}
@Override
public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};
}
- command+n 選擇parcelable插件自動生成
- 如果實(shí)現(xiàn)Parcelable接口的對象中包含對象或者集合,那么其中的對象也要實(shí)現(xiàn)Parcelable接口
兩種特殊情況
1绰更、凡是被static修飾的字段是不會被序列化的
對于第一點(diǎn)瞧挤,因?yàn)樾蛄谢4娴氖菍ο蟮臓顟B(tài)而非類的狀態(tài)锡宋,所以會忽略static靜態(tài)域也是理所應(yīng)當(dāng)?shù)摹?/p>
2、凡是被transient修飾符修飾的字段也是不會被序列化的
對于第二點(diǎn)特恬,就需要了解一下transient修飾符的作用了员辩。
如果在序列化某個(gè)類的對象時(shí),就是不希望某個(gè)字段被序列化(比如這個(gè)字段存放的是隱私值鸵鸥,如:密碼等)奠滑,那這時(shí)就可以用transient修飾符來修飾該字段。
-
比如在之前定義的Student類中妒穴,加入一個(gè)密碼字段宋税,但是不希望序列化到txt文本,則可以:
-
這樣在序列化Student類對象時(shí)讼油,password字段會設(shè)置為默認(rèn)值null杰赛,這一點(diǎn)可以從反序列化所得到的結(jié)果來看出:
Parcelable 與 Serializable 區(qū)別
(1)兩者的實(shí)現(xiàn)差異
- Serializable的實(shí)現(xiàn),只需要實(shí)現(xiàn)Serializable接口即可矮台。這只是給對象打了一個(gè)標(biāo)記(UID)乏屯,系統(tǒ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ì)初衷是為了序列化對象到本地文件、數(shù)據(jù)庫校辩、網(wǎng)絡(luò)流窘问、RMI以便數(shù)據(jù)傳輸,當(dāng)然這種傳輸可以是程序內(nèi)的也可以是兩個(gè)程序間的宜咒。而Android的Parcelable的設(shè)計(jì)初衷是由于Serializable效率過低惠赫,消耗大,而android中數(shù)據(jù)傳遞主要是在內(nèi)存環(huán)境中(內(nèi)存屬于android中的稀有資源)故黑,因此Parcelable的出現(xiàn)為了滿足數(shù)據(jù)在內(nèi)存中低開銷而且高效地傳遞問題儿咱。
(3)兩者效率選擇
- Serializable使用IO讀寫存儲在硬盤上。序列化過程使用了反射技術(shù)倍阐,并且期間產(chǎn)生臨時(shí)對象概疆,優(yōu)點(diǎn)代碼少逗威,在將對象序列化到存儲設(shè)置中或?qū)ο笮蛄谢笸ㄟ^網(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也是沒什么問題的鞠柄,但是針對Android應(yīng)用程序在內(nèi)存間數(shù)據(jù)傳輸還是建議大家使用Parcelable方式實(shí)現(xiàn)序列化侦高,畢竟性能好很多,其實(shí)也沒多麻煩厌杜。
- Parcelable也不是不可以在網(wǎng)絡(luò)中傳輸奉呛,只不過實(shí)現(xiàn)和操作過程過于麻煩并且為了防止android版本不同而導(dǎo)致Parcelable可能不同的情況,因此在序列化到存儲設(shè)備或者網(wǎng)絡(luò)傳輸方面還是盡量選擇Serializable接口夯尽。
序列化的受控和加強(qiáng)
約束性加持
- 從上面的過程可以看出,序列化和反序列化的過程其實(shí)是有漏洞的,因?yàn)閺男蛄谢椒葱蛄谢怯兄虚g過程的义矛,如果被別人拿到了中間字節(jié)流派诬,然后加以偽造或者篡改,那反序列化出來的對象就會有一定風(fēng)險(xiǎn)了圈纺。
- 畢竟反序列化也相當(dāng)于一種 “隱式的”對象構(gòu)造 秦忿,因此我們希望在反序列化時(shí),進(jìn)行受控的對象反序列化動作蛾娶。
那怎么個(gè)受控法呢灯谣?
- 答案就是: 自行編寫readObject()函數(shù),用于對象的反序列化構(gòu)造蛔琅,從而提供約束性酬屉。
- 既然自行編寫readObject()函數(shù),那就可以做很多可控的事情:比如各種判斷工作揍愁。
- 還以上面的Student類為例呐萨,一般來說學(xué)生的成績應(yīng)該在0 ~ 100之間,我們?yōu)榱朔乐箤W(xué)生的考試成績在反序列化時(shí)被別人篡改成一個(gè)奇葩值莽囤,我們可以自行編寫readObject()函數(shù)用于反序列化的控制:
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
// 調(diào)用默認(rèn)的反序列化函數(shù)
objectInputStream.defaultReadObject();
// 手工檢查反序列化后學(xué)生成績的有效性谬擦,若發(fā)現(xiàn)有問題,即終止操作朽缎!
if( 0 > score || 100 < score ) {
throw new IllegalArgumentException("學(xué)生分?jǐn)?shù)只能在0到100之間惨远!");
}
}
-
比如我故意將學(xué)生的分?jǐn)?shù)改為101,此時(shí)反序列化立馬終止并且報(bào)錯(cuò):
-
對于上面的代碼话肖,有些小伙伴可能會好奇北秽,為什么自定義的private的readObject()方法可以被自動調(diào)用,這就需要你跟一下底層源碼來一探究竟了最筒,我?guī)湍愀搅薕bjectStreamClass類的最底層贺氓,看到這里我相信你一定恍然大悟:
又是反射機(jī)制在起作用!是的床蜘,在Java里辙培,果然萬物皆可“反射”(滑稽)蔑水,即使是類中定義的private私有方法,也能被摳出來執(zhí)行了扬蕊,簡直引起舒適了搀别。
單例模式增強(qiáng)
- 一個(gè)容易被忽略的問題是:可序列化的單例類有可能并不單例!
- 比如這里我們先用java寫一個(gè)常見的「靜態(tài)內(nèi)部類」方式的單例模式實(shí)現(xiàn):
public class Singleton implements Serializable {
private static final long serialVersionUID = -1576643344804979563L;
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
public static synchronized Singleton getSingleton() {
return SingletonHolder.singleton;
}
}
- 然后寫一個(gè)驗(yàn)證主函數(shù):
public class Test2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(
new FileOutputStream( new File("singleton.txt") )
);
// 將單例對象先序列化到文本文件singleton.txt中
objectOutputStream.writeObject( Singleton.getSingleton() );
objectOutputStream.close();
ObjectInputStream objectInputStream =
new ObjectInputStream(
new FileInputStream( new File("singleton.txt") )
);
// 將文本文件singleton.txt中的對象反序列化為singleton1
Singleton singleton1 = (Singleton) objectInputStream.readObject();
objectInputStream.close();
Singleton singleton2 = Singleton.getSingleton();
// 運(yùn)行結(jié)果竟打印 false 尾抑!
System.out.println( singleton1 == singleton2 );
}
}
- 運(yùn)行后我們發(fā)現(xiàn):反序列化后的單例對象和原單例對象并不相等了歇父,這無疑沒有達(dá)到我們的目標(biāo)。
解決辦法是:在單例類中手寫readResolve()函數(shù)再愈,直接返回單例對象庶骄,來規(guī)避之:
private Object readResolve() {
return SingletonHolder.singleton;
}
- 這樣一來,當(dāng)反序列化從流中讀取對象時(shí)践磅,readResolve()會被調(diào)用单刁,用其中返回的對象替代反序列化新建的對象。
App 性能優(yōu)化
- 界面?zhèn)髦档炔僮魇褂肞racelable來序列化,畢竟谷歌官方推薦的么府适。