Android-序列化/反序列化

什么是序列化/反序列化?

  • 簡(jiǎn)單來(lái)說(shuō)就是將對(duì)象轉(zhuǎn)換為可以傳輸?shù)亩M(jìn)制流(二進(jìn)制序列)的過(guò)程,這樣我們就可以通過(guò)序列化,轉(zhuǎn)化為可以在網(wǎng)絡(luò)傳輸或者保存到本地的流(序列),從而進(jìn)行傳輸數(shù)據(jù) 。
  • 那反序列化就是從二進(jìn)制流(序列)轉(zhuǎn)化為對(duì)象的過(guò)程.
  • 由于存在于內(nèi)存中的對(duì)象都是暫時(shí)的,無(wú)法長(zhǎng)期駐存,為了把對(duì)象的狀態(tài)保持下來(lái),這時(shí)需要把對(duì)象寫入到磁盤或者其他介質(zhì)中,這個(gè)過(guò)程就叫做序列化。
  • 反序列化恰恰是序列化的反向操作,也就是說(shuō),把已存在在磁盤或者其他介質(zhì)中的對(duì)象,反序列化(讀?。┑絻?nèi)存中,以便后續(xù)操作,而這個(gè)過(guò)程就叫做反序列化。
  • 反序列化:把字節(jié)序列恢復(fù)為原先的Java對(duì)象。
  • 序列化:把Java對(duì)象轉(zhuǎn)換為字節(jié)序列。

什么時(shí)候使用序列化

  • 數(shù)據(jù)傳輸?shù)臅r(shí)候
  • 數(shù)據(jù)保存的時(shí)候

序列化是干啥用的?

  • 序列化的原本意圖是希望對(duì)一個(gè)Java對(duì)象作一下“變換”,變成字節(jié)序列,這樣一來(lái)方便持久化存儲(chǔ)到磁盤,避免程序運(yùn)行結(jié)束后對(duì)象就從內(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接口非常簡(jiǎn)單,聲明一下就可以了,而實(shí)現(xiàn)Parcelable接口稍微復(fù)雜一些,但效率更高,在一般的時(shí)候推薦用這種方法提高性能。

對(duì)象如何序列化?

  • 舉個(gè)例子,假如我們要對(duì)Student類對(duì)象序列化到一個(gè)名為student.txt的文本文件中,然后再通過(guò)文本文件反序列化成Student類對(duì)象:
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è)空接口,并沒(méi)有包含任何方法!
public interface Serializable {
}
試想,如果上面在定義Student類時(shí)忘了加implements Serializable時(shí)會(huì)發(fā)生什么呢?
  • 實(shí)驗(yàn)結(jié)果是:此時(shí)的程序運(yùn)行會(huì)報(bào)錯(cuò),并拋出NotSerializableException異常:


  • 我們按照錯(cuò)誤提示,由源碼一直跟到ObjectOutputStream的writeObject0()方法底層一看,才恍然大悟:


  • 如果一個(gè)對(duì)象既不是字符串、數(shù)組、枚舉,而且也沒(méi)有實(shí)現(xiàn)Serializable接口的話,在序列化時(shí)就會(huì)拋出NotSerializableException異常!

  • 原來(lái)Serializable接口也僅僅只是做一個(gè)標(biāo)記用?。。?/p>

  • 它告訴代碼只要是實(shí)現(xiàn)了Serializable接口的類都是可以被序列化的!然而真正的序列化動(dòng)作不需要靠它完成。

serialVersionUID號(hào)有何用?
  • 相信你一定經(jīng)常看到有些類中定義了如下代碼行,即定義了一個(gè)名為serialVersionUID的字段:
private static final long serialVersionUID = -4392658638228508589L;
  • 繼續(xù)來(lái)做一個(gè)簡(jiǎn)單實(shí)驗(yàn),還拿上面的Student類為例,我們并沒(méi)有人為在里面顯式地聲明一個(gè)serialVersionUID字段。
  • 我們首先還是調(diào)用上面的serialize()方法,將一個(gè)Student對(duì)象序列化到本地磁盤上的student.txt文件:
  • 接下來(lái)我們?cè)赟tudent類里面動(dòng)點(diǎn)手腳,比如在里面再增加一個(gè)名為studentID的字段,表示學(xué)生學(xué)號(hào):新增一個(gè)字段 private Long studentId; // 表示學(xué)號(hào)
  • 這時(shí)候,我們拿剛才已經(jīng)序列化到本地的student.txt文件,還用如下代碼進(jìn)行反序列化,試圖還原出剛才那個(gè)Student對(duì)象:
  • 運(yùn)行發(fā)現(xiàn)報(bào)錯(cuò)了,并且拋出了InvalidClassException異常:


  • 這地方提示的信息非常明確了:序列化前后的serialVersionUID號(hào)碼不兼容!
從這地方最起碼可以得出兩個(gè)重要信息:
  • 1、serialVersionUID是序列化前后的唯一標(biāo)識(shí)符
  • 2、默認(rèn)如果沒(méi)有人為顯式定義過(guò)serialVersionUID,那編譯器會(huì)為它自動(dòng)聲明一個(gè)!
  • serialVersionUID序列化ID,可以看成是序列化和反序列化過(guò)程中的“暗號(hào)”,在反序列化時(shí),JVM會(huì)把字節(jié)流中的序列號(hào)ID和被序列化類中的序列號(hào)ID做比對(duì),只有兩者一致,才能重新反序列化,否則就會(huì)報(bào)異常來(lái)終止反序列化的過(guò)程。
  • 如果在定義一個(gè)可序列化的類時(shí),沒(méi)有人為顯式地給它定義一個(gè)serialVersionUID的話,則Java運(yùn)行時(shí)環(huán)境會(huì)根據(jù)該類的各方面信息自動(dòng)地為它生成一個(gè)默認(rèn)的serialVersionUID,一旦像上面一樣更改了類的結(jié)構(gòu)或者信息,則類的serialVersionUID也會(huì)跟著變化!
  • 所以,為了serialVersionUID的確定性,寫代碼時(shí)還是建議,凡是implements Serializable的類,都最好人為顯式地為它聲明一個(gè)serialVersionUID明確值!
  • Android Studio 自動(dòng)添加serialVersionUID http://www.itdecent.cn/p/9252e1e4e82e

Parcelable

AndroidStudio中的快捷生成方式

  • 插件 android Parcelable code generator

Parcel的簡(jiǎn)介 [?pɑ?rsl]

  • 在介紹之前我們需要先了解Parcel是什么?Parcel翻譯過(guò)來(lái)是打包的意思,其實(shí)就是包裝了我們需要傳輸?shù)臄?shù)據(jù),然后在Binder中傳輸,也就是用于跨進(jìn)程傳輸數(shù)據(jù)。

Parcel模型

  • 簡(jiǎn)單來(lái)說(shuō),Parcel提供了一套機(jī)制,可以將序列化之后的數(shù)據(jù)寫入到一個(gè)共享內(nèi)存中,其他進(jìn)程通過(guò)Parcel可以從這塊共享內(nèi)存中讀出字節(jié)流,并反序列化成對(duì)象,下圖是這個(gè)過(guò)程的模型。
  • Parcel可以包含原始數(shù)據(jù)類型(用各種對(duì)應(yīng)的方法寫入,比如writeInt(),writeFloat()等),可以包含Parcelable對(duì)象,它還包含了一個(gè)活動(dòng)的IBinder對(duì)象的引用,這個(gè)引用導(dǎo)致另一端接收到一個(gè)指向這個(gè)IBinder的代理IBinder。


Parcelable中的三大過(guò)程介紹(序列化,反序列化,描述)

/**
 * ================================================
 * 作    者: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插件自動(dòng)生成
  • 如果實(shí)現(xiàn)Parcelable接口的對(duì)象中包含對(duì)象或者集合,那么其中的對(duì)象也要實(shí)現(xiàn)Parcelable接口

兩種特殊情況

1、凡是被static修飾的字段是不會(huì)被序列化的

對(duì)于第一點(diǎn),因?yàn)樾蛄谢4娴氖菍?duì)象的狀態(tài)而非類的狀態(tài),所以會(huì)忽略static靜態(tài)域也是理所應(yīng)當(dāng)?shù)摹?/p>

2、凡是被transient修飾符修飾的字段也是不會(huì)被序列化的
  • 對(duì)于第二點(diǎn),就需要了解一下transient修飾符的作用了。

  • 如果在序列化某個(gè)類的對(duì)象時(shí),就是不希望某個(gè)字段被序列化(比如這個(gè)字段存放的是隱私值,如:密碼等),那這時(shí)就可以用transient修飾符來(lái)修飾該字段。

  • 比如在之前定義的Student類中,加入一個(gè)密碼字段,但是不希望序列化到txt文本,則可以:


  • 這樣在序列化Student類對(duì)象時(shí),password字段會(huì)設(shè)置為默認(rèn)值null,這一點(diǎn)可以從反序列化所得到的結(jié)果來(lái)看出:


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)存中低開銷而且高效地傳遞問(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接口。

序列化的受控和加強(qiáng)

約束性加持

  • 從上面的過(guò)程可以看出,序列化和反序列化的過(guò)程其實(shí)是有漏洞的,因?yàn)閺男蛄谢椒葱蛄谢怯兄虚g過(guò)程的,如果被別人拿到了中間字節(jié)流,然后加以偽造或者篡改,那反序列化出來(lái)的對(duì)象就會(huì)有一定風(fēng)險(xiǎn)了。
  • 畢竟反序列化也相當(dāng)于一種 “隱式的”對(duì)象構(gòu)造 ,因此我們希望在反序列化時(shí),進(jìn)行受控的對(duì)象反序列化動(dòng)作。

那怎么個(gè)受控法呢?

  • 答案就是: 自行編寫readObject()函數(shù),用于對(duì)象的反序列化構(gòu)造,從而提供約束性。
  • 既然自行編寫readObject()函數(shù),那就可以做很多可控的事情:比如各種判斷工作。
  • 還以上面的Student類為例,一般來(lái)說(shuō)學(xué)生的成績(jī)應(yīng)該在0 ~ 100之間,我們?yōu)榱朔乐箤W(xué)生的考試成績(jī)?cè)诜葱蛄谢瘯r(shí)被別人篡改成一個(gè)奇葩值,我們可以自行編寫readObject()函數(shù)用于反序列化的控制:
private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {

    // 調(diào)用默認(rèn)的反序列化函數(shù)
    objectInputStream.defaultReadObject();

    // 手工檢查反序列化后學(xué)生成績(jī)的有效性,若發(fā)現(xiàn)有問(wèn)題,即終止操作!
    if( 0 > score || 100 < score ) {
        throw new IllegalArgumentException("學(xué)生分?jǐn)?shù)只能在0到100之間!");
    }
}
  • 比如我故意將學(xué)生的分?jǐn)?shù)改為101,此時(shí)反序列化立馬終止并且報(bào)錯(cuò):


  • 對(duì)于上面的代碼,有些小伙伴可能會(huì)好奇,為什么自定義的private的readObject()方法可以被自動(dòng)調(diào)用,這就需要你跟一下底層源碼來(lái)一探究竟了,我?guī)湍愀搅薕bjectStreamClass類的最底層,看到這里我相信你一定恍然大悟:


  • 又是反射機(jī)制在起作用!是的,在Java里,果然萬(wàn)物皆可“反射”(滑稽),即使是類中定義的private私有方法,也能被摳出來(lái)執(zhí)行了,簡(jiǎn)直引起舒適了。

單例模式增強(qiáng)

  • 一個(gè)容易被忽略的問(wèn)題是:可序列化的單例類有可能并不單例!
  • 比如這里我們先用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") )
                );
        // 將單例對(duì)象先序列化到文本文件singleton.txt中
        objectOutputStream.writeObject( Singleton.getSingleton() );
        objectOutputStream.close();

        ObjectInputStream objectInputStream =
                new ObjectInputStream(
                    new FileInputStream( new File("singleton.txt") )
                );
        // 將文本文件singleton.txt中的對(duì)象反序列化為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):反序列化后的單例對(duì)象和原單例對(duì)象并不相等了,這無(wú)疑沒(méi)有達(dá)到我們的目標(biāo)。
解決辦法是:在單例類中手寫readResolve()函數(shù),直接返回單例對(duì)象,來(lái)規(guī)避之:
private Object readResolve() {
    return SingletonHolder.singleton;
}
  • 這樣一來(lái),當(dāng)反序列化從流中讀取對(duì)象時(shí),readResolve()會(huì)被調(diào)用,用其中返回的對(duì)象替代反序列化新建的對(duì)象。

App 性能優(yōu)化

  • 界面?zhèn)髦档炔僮魇褂肞racelable來(lái)序列化,畢竟谷歌官方推薦的么。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容