Android序列化-Serializable和Parcelable

總要說兩句

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

序列化與反序列

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

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

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

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

  • 序列化的應用情景
    主要有以下情況(但不限於以下情況)
    1)內存中的對象寫入到硬盤;
    2)用套接字在網絡上傳送對象;

Serializable

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

package com.zejian.ipctest;

import java.io.Serializable;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class Client 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;
    }
}

  如上述代碼所示,Client類實現的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~,瞭解這麼多,下面來看一個如何進行對象序列化和反序列化的列子:

package com.zejian.ipctest;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class Stest {

    public static void main(String[] args) throws Exception {

        //把對象序列化到文件
        Client client = new Client();
        client.setId(000001);
        client.setName("client");

        ObjectOutputStream oo = new ObjectOutputStream
                (new FileOutputStream("/Users/zejian/Desktop/cache.txt"));
        oo.writeObject(client);
        oo.close();

        //反序列化到內存
        ObjectInputStream oi = new ObjectInputStream
            (new FileInputStream("/Users/zejian/Desktop/cache.txt"));
        Client c_back = (Client) oi.readObject();
        System.out.println("Hi, My name is " + c_back.getName());
        oi.close();

    }
}

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

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

package com.zejian.ipctest;

import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class Client 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方法,在這個階段,
     * 可以進行自己操作,將需要進行序列化的對象換成我們指定的對象.
     * 一般很少重寫該方法
     * @return
     * @throws ObjectStreamException
     */
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace invoked");
        return this;
    }
    /**
     *接着系統將調用writeObject方法,
     * 來將對象中的屬性一個個進行序列化,
     * 我們可以在這個方法中控制住哪些屬性需要序列化.
     * 這裏只序列化name屬性
     * @param out
     * @throws IOException
     */
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        System.out.println("writeObject invoked");
        out.writeObject(this.name == null ? "zejian" : this.name);
    }

    /**
     * 反序列化時,系統會調用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,
     * 反序列化回來.然後通過readResolve方法,我們也可以指定系統返回給我們特定的對象
     * 可以不是writeReplace序列化時的對象,可以指定其他對象.
     * @param in
     * @throws IOException
     * @throws ClassNotFoundException
     */
    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序列化時的對象,可以指定其他對象.
     * 一般很少重寫該方法
     * @return
     * @throws ObjectStreamException
     */
    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接口的實現案例,大家感受一下:

package com.zejian.ipctest;
import android.os.Parcel;
import android.os.Parcelable;

/**
 * Created by zejian
 * Time 2016/9/25.
 * Description:
 */
public class NewClient implements Parcelable {

    public int id;
    public String name;
    public User user;

    /**
     * 當前對象的內容描述,一般返回0即可
     * @return
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 將當前對象寫入序列化結構中
     * @param dest
     * @param flags
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.id);
        dest.writeString(this.name);
        dest.writeParcelable(this.user,0);
    }

    public NewClient() {
    }

    /**
     * 從序列化後的對象中創建原始對象
     * @param in
     */
    protected NewClient(Parcel in) {
        this.id = in.readInt();
        this.name = in.readString();
       //User是另一個序列化對象,此方法序列需要傳遞當前線程的上下文類加載器,否則會報無法找到類的錯誤
       this.user=in.readParcelable(Thread.currentThread().getContextClassLoader());
    }

    /**
     * public static final一個都不能少,內部對象CREATOR的名稱也不能改變,必須全部大寫。
     * 重寫接口中的兩個方法:
     * createFromParcel(Parcel in) 實現從Parcel容器中讀取傳遞數據值,封裝成Parcelable對象返回邏輯層,
     * newArray(int size) 創建一個類型爲T,長度爲size的數組,供外部類反序列化本類數組使用。
     */
    public static final Parcelable.Creator<NewClient> CREATOR = new Parcelable.Creator<NewClient>() {
        /**
         * 從序列化後的對象中創建原始對象
         */
        @Override
        public NewClient createFromParcel(Parcel source) {
            return new NewClient(source);
        }

        /**
         * 創建指定長度的原始對象數組
         * @param size
         * @return
         */
        @Override
        public NewClient[] newArray(int size) {
            return new NewClient[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的對象數組
public Intent putParcelableArrayListExtra(String name, ArrayList value) 設置List數組,其元素必須是實現了Parcelable接口的數據

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

Parcelable 與 Serializable 區別

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

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

  • 兩者效率選擇
    Parcelable的性能比Serializable好,在內存開銷方面較小,所以Android應用程序在內存間數據傳輸時推薦使用Parcelable,如activity間傳輸數據和AIDL數據傳遞,而Serializable將數據持久化的操作方便,因此在將對象序列化到存儲設置中或將對象序列化後通過網絡傳輸時建議選擇Serializable(Parcelable也是可以,只不過實現和操作過程過於麻煩並且爲了防止android版本不同而導致Parcelable可能不同的情況,因此在序列化到存儲設備或者網絡傳輸方面還是儘量選擇Serializable接口)。

  • 兩者需要注意的共同點
    無論是Parcelable還是Serializable,執行反序列操作後的對象都是新創建的,與原來的對象並不相同,只不過內容一樣罷了。

Android studio 中的快捷生成方式

  • Android studio 快捷生成Parcelable代碼

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

最後結果如下:

這裏寫圖片描述

  • Android studio 快捷生成Serializable的UID

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

最終結果如下:

這裏寫圖片描述

完啦,以上便是Parcelable與Serializable接口的全部內容,如有錯誤還請指出。

本期節目就到這裏,感謝大家收看,我們下期再見~
http://blog.csdn.net/javazejian/article/details/52665164

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章