Android IPC基礎概念之(序列化)介紹

前言

當我們需要通過 Intent 或 Binder 傳輸數據時需要先把數據或者要傳輸的對象完成序列化操作, 這時就需要使用 Serializable 或者 Parcelable。還有的時候我們需要把對象持久化到存儲設備上或者通過網絡傳輸給其他客戶端,這時候也需要使用 Serializable 來完成對象的持久化。

Serializable 接口

Serializable 是 Java 提供的一個序列化接口,它是一個空接口,爲對象提供標準的序列化和反序列化操作。使用 Serializable 來實現序列化相當簡單,只需要實現 Serializable 接口並聲明一個 serialVersionUID 即可;實際上,這個 serialVersionUID 也不是必需的,我們不聲明這個 serialVersionUID 同樣也可以實現序列化,但是這將會對反序列化過程產生影響。

public class Student implements Serializable {

    private static final long serialVersionUID = 519067123721295773L;

    public int age;
    public String name;

    public Student(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return super.toString() + " age:" + age + " name:" + name;
    }
}

序列化過程的示例:

// 序列化
Student student = new Student(18, "張三");
ObjectOutputStream out = null;
try {
    out = new ObjectOutputStream(new FileOutputStream(Environment.getExternalStorageDirectory().getPath() + "/studentCache"));
    out.writeObject(student);
    Log.i("序列化", "student: " + student);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (out != null) {
            out.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

反序列化過程示例:

// 反序列化
ObjectInputStream in = null;
try {
    in = new ObjectInputStream(new FileInputStream(Environment.getExternalStorageDirectory().getPath() + "/studentCache"));
    Student newStudent = (Student) in.readObject();
    Log.i("反序列化---", "newStudent: " + newStudent);
} catch (Exception e) {
    e.printStackTrace();
} finally {
    if (in != null) {
        try {
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述代碼演示了採用 Serializable 方式序列化對象的典型過程,很簡單,只需要把實現了 Serializable 接口的 Student 對象寫到文件中就可以快速恢復了,恢復後的對象 newStudent 和 student 的內容完全一樣,但是二者並不是同一個對象。通過 log 就可以看出。

 I/序列化: student: com.wiwide.aidldemo.bean.Student@121a3f63 age:18 name:張三
 I/反序列化---: newStudent: com.wiwide.aidldemo.bean.Student@1000bd51 age:18 name:張三

開始提到的 serialVersionUID 是用來輔助序列化和反序列化過程的,原則上序列化後的數據中的 serialVersionUID 只有和當前類的 serialVersionUID 相同才能夠正常地被反序列化。

serialVersionUID 工作機制:

序列化的時候系統會把當前類的 serialVersionUID 寫入到序列化的文件中(也可能是其他的中介),當反序列化的時候系統會去檢測文件中的 serialVersionUID,看它是否和當前類的 serialVersionUID 一致,如果一致就說明序列化類的版本和當前類的版本是相同的,這個時候可以成功得反序列化;否則就說明當前類和序列化的類相比發生了某些變換,比如成員變量的數量、類型可能發生了改變,這個時候是無法正常反序列化的。

分析結論

一般來說,我們應該手動指定 serialVersionUID 的值,比如 1L,也可以讓 IDE 根據當前類的結構去自動生成它的 hash 值,這樣序列化和反序列化時兩者的 serialVersionUID 是相同的,因此可以正常進行反序列化。如果不手動指定 serialVersionUID 的值,反序列化時當前類有所改變,比如增加或者刪除了某些成員變量,那麼系統就會重新計算當前類的 hash 值並把它賦值給 serialVersionUID,這個時候當前類的 serialVersionUID 就和序列化的數據中的 serialVersionUID 不一致,於是反序列化失敗,程序就會出現 crash。

所以,我們可以明顯感覺到 serialVersionUID 的作用,當我們手動指定了它之後,就可以在很大程度上避免反序列化過程的失敗。比如當版本升級後,我們可能刪除了某個成員變量也可能增加了一些新的成員變量,這個時候我們的反序列化過程仍然能夠成功,程序仍然能最大限度地恢復數據,相反,如果不指定 serialVersionUID 的話,程序則會掛掉。

當然,我們還要考慮另外一種情況,如果類的結構發生了非常規性的改變,比如修改了類名,修改了成員變量的類型,這個時候儘管 serialVersionUID 驗證通過了,但是反序列化過程還是會失敗,因爲類結構有了毀滅性的改變,根本無法從老版本的數據中還原出一個新的類結構的對象。

注意

  1. 靜態成員變量屬於類,不屬於對象,所以不會參與序列化過程。
  2. 用 transient 關鍵字標記的成員變量不參與序列化過程。

Parcelable 接口

Parcelable 也是一個接口,只要實現這個接口,一個類的對象就可以實現序列化並可以通過 Intent 和 Binder 傳遞。

在 Android Studio 的 Settings 界面的 Plugins (插件)標籤下安裝 Parcelable 插件,如圖:

這裏寫圖片描述

然後在要序列化的類中,通過 Alt + Insert 打開代碼自動生成窗口,選擇 Parcelable 按照提示即可自動生成完整的代碼。

這裏寫圖片描述

public class Book implements Parcelable{
    public int bookId;
    public String bookName;

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.bookId);
        dest.writeString(this.bookName);
    }

    protected Book(Parcel in) {
        this.bookId = in.readInt();
        this.bookName = in.readString();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel source) {
            return new Book(source);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };
}

這裏先說一下 Parcel,Parcel 內部包裝了可序列化的數據,可以在 Binder 中自由傳輸。從上述代碼中可以看出,在序列化過程中需要實現的功能有序列化、反序列化和內容描述。序列化功能由 writeParcel 方法來完成,最終是通過 Parcel 中的一系列 write 方法來完成的,反序列化功能由 CREATOR 來完成,其內部標明瞭如何創建序列化對象和數組,並通過 Parcel 的一系列 read 方法來完成反序列化過程;內容描述功能由 describeContents 方法來完成,幾乎在所有情況下這個方法都應該返回 0,僅噹噹前對象中存在文件描述符時,此方法返回 1。

系統已經爲我們提供了許多實現了 Parcelable 接口的類,它們都是可以直接序列化的,比如:Intent、Bundle、Bitmap 等,同時 List 和 Map 也可以序列化,前提是它們裏邊的每個元素都是可序列化的。

總結

既然 Serializable 和 Parcelable 都能夠實現序列化並且都可以用於 Intent 間的數據傳遞,那麼二者應該如何選擇呢?

Serializable
Java 中的序列化接口,使用簡單但開銷很大,序列化過程和反序列化過程需要大量的 I/O 操作。
一般需要將對象序列化到存儲設備中或者將對象序列化後通過網絡傳輸時使用 Serializable 。(當然 Parcelable 也可以實現,過程會稍微複雜一些)

Parcelable
Android 中的序列化方式,缺點使用起來稍微麻煩,不過可以藉助插件生成代碼,但效率很高。
主要用在內存序列化上,除了上述兩種情況推薦使用 Serializable,其他情況首選 Parcelable 。

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