Android - 從淺到懂理解 Serializeable 和 Parcelable 實現的序列化和反序列化

背景

在開發插件化App時用到了AIDL實現進程間通訊。而AIDL要想傳遞對象類型的數據就須要將對象序列化。html

Android 開發中,咱們常常須要對對象進行序列化與反序列化操做。
最多見的就是經過 Intent 傳輸數據時,Intent 只能傳輸基本數據類型、String 類型和可序列化與反序列化的對象類型,
要想經過 Intent 傳遞對象類型,咱們須要讓該對象類型支持序列化和反序列化。java

咱們知道,Android 給咱們提供了兩種方式來完成序列化與反序列化過程:android

  • 一種是 Serializable 方式
  • 另外一種是 Parcelable 方式;

本篇文章將盡量詳細講述兩種方式實現序列化。git

咱們首先來了解序列化和反序列化。github

序列化和反序列化是什麼?

除了基本數據類型外的其它類型,如對象、文件、圖片都有本身的數據格式,很難統一傳輸和保存。
基本數據類型提供了轉爲byte[]的方法,因此數據能夠統一成字節流web

爲了實現對象、文件、圖片數據也能在計算機中傳輸和保存,所以能夠將這些數據格式也轉爲字節流編程

在特定語言語言範疇內將一個實例對象編碼成字節流,稱爲序列化;將一個字節流中讀出一個對象實例,稱爲反序列化數組

PS:基本數據類型網絡

  1. 四種整數類型byte、short、int、long
  2. 兩種浮點數類型float、double
  3. 一種字符類型char
  4. 一種布爾類型boolean

上面提到的字節和字符,我簡要描述一下。編程語言

字節流 和 字符流

字節流是由字節組成的, 與之相關的還有字符流,它是由字符組成的

這裏的能夠看做是水流水就是數據,而是指流入流出

那麼字節和字符是什麼呢?

字節 和 字符

  • 字節(byte):計算機的計量單位,表示數據量的多少。一般狀況下 1byte = 8bit
  • 字符(Character):計算機中用於表示字母、數字、字和符號

通常(ASCII編碼)在英文狀態下1個字母或字符佔用1個字節,1個漢字用2個字節表示。

之因此有編碼表是由於計算機在全球各個國家和地區使用,爲了支持不一樣國家的文字在計算機上能統一使用,
所以提供編碼表的方式,統一將文字轉爲字節。

常見的編碼表中字符和字節的對應關係以下:

  • ASCII 碼中,1個英文字母(不分大小寫)爲1個字節,1箇中文漢字爲2個字節。
  • GBK 碼中,1個英文字母(不分大小寫)爲1個字節,1箇中文漢字爲2個字節。
  • UTF-8 編碼中,1個英文字爲1個字節,1箇中文爲3個字節。
  • Unicode 編碼中,1個英文爲1個字節,1箇中文爲2個字節。
  • 符號:英文標點爲1個字節,中文標點爲2個字節。例如:英文句號.1個字節的大小,中文句號2個字節的大小。

那麼咱們能夠明確序列化的目的就算爲了在計算機中傳遞統一格式的數據
而計算機傳輸本質又都是字節,因此本質上就是將數據轉爲byte[]數據格式,而後在計算機中傳遞。

Android 中常見的序列化場景

  • 文件的讀寫是經過字節流的方式,而序列化對象後就能夠將對象保存文件中。
  • 網絡數據傳輸也是經過字節流的方式,因此可將對象序列化後用於在網絡間傳遞。
  • 跨進程通訊,如使用 AIDL 傳遞對象時須要進行序列化。
  • Intent之間只能傳遞基本的數據類型, 如須要傳遞複雜對象,就須要用到序列化。

Android 實現序列化的兩種方式

  • SerializeableJava提供的序列化方式
  • ParcelableAndroid提供的序列化方式

Serializable

SerializableJava 提供的序列化接口。
要將對象序列化,只需讓對象實現java.io.Serializable接口便可。
實現Serializable類對象的全部屬性必須是可序列化的。若是有一個屬性不須要可序列化的,則使用transient 關鍵字修飾。

例如:將對象保存到文本文件並讀取出來。

寫入文件咱們會經過ObjectOutputStreamwriteObject(Object obj)方法去實現,讀取文件咱們會經過ObjectInputStreamreadObject()去實現。

  • ObjectOutputStream:是對象輸出流,做用是將對象轉成字節數據輸出到文件中保存.
  • ObjectInputStream:是對象輸入流,做用是從文件中將字節數據轉爲對象

看個使用示例:

import java.io.Serializable;

public class User implements Serializable{ 
 
   
    private static final long serialVersionUID = 1L;
    private String name = "";

    // transient 表示不序列化該屬性
    private transient  int age = 0;

    // Child 對象也必需要實現 Serializable 不然得加 transient 禁止該屬性序列化
    private Child child = null;

    // 省略 get set 等方法
}

import java.io.Serializable;

public class Child implements Serializable { 
 
   

    private static final long serialVersionUID = 12L;
    private int age = 0;

     // 省略 get set 等方法
}



// 寫入文件
private static void writeUser(){ 
 
   
    //序列化到本地
    User user=new User("張三",20, new Child(2));
    try { 
 
   
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
        out.writeObject(user);
        out.close();
    } catch (IOException e) { 
 
   
        e.printStackTrace();
    }

}

// 從文件讀取
private static void readUser(){ 
 
   

    try { 
 
   
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\user.txt"));

        User user=(User)in.readObject();
        System.out.println("user :"+user.toString());

        // 輸出
        // user :User{name='張三', age=0, child=Child{age=2}}

        // 解釋
        // 因爲咱們在 User 對象的 age 屬性設置了 transient 關鍵字,因此 age 不會被序列化。
        // 也就是寫入的 age=20 保存不了,因此讀取出來的數據是默認的數字 0

        in.close();

    } catch (Exception e) { 
 
   
        e.printStackTrace();
    }

}

若是對象沒實現Serializable接口就直接調用ObjectOutputStream、ObjectInputStream會報錯java.io.NotSerializableException
並且User對象中的Child對象若是沒實現Serializable接口也會報該錯,進一步說明了對象中全部要序列化的對象都要實現Serializable接口。
咱們在User對象的age屬性設置了transient,在寫入User數據age=20以後可是讀取數據時age=0也說明了transient能夠忽略屬性的序列化。

關於serialVersionUID最好是用private顯式聲明,並且必須是static final long類型,否則在反序列化時可能會報錯。

這個serialVersionUID是用來輔助序列化反序列化的。
如不顯式聲明,編譯器會自動去計算出一個值並賦予它。

那麼Serialable是如何實現序列化的呢?

Serializable 實現原理

咱們在寫入文件時調用了writeObject()方法,那麼咱們就從該方法入手。

參考小緣大佬的源碼解讀:地址-https://www.wanandroid.com/wenda/show/9002

  1. 藉助ObjectStreamClass記錄目標對象的類型,類名等信息,這個類裏面還有個ObjectStreamField數組,用來記錄目標對象的內部變量;
  2. defaultWriteFields方法中,會先經過ObjectStreamClassgetPrimFieldValues方法,把基本數據類型的值都複製到一個叫primValsbyte數組上;
  3. 接着經過getPrimFieldValues方法來獲取全部成員變量的值,出乎意料的是:這兩個獲取值的方法,裏面都不是咱們常規的反射操做(Field.get),而是經過操做Unsafe類來完成的;
  4. 遍歷剩下不是基本數據類型的成員變量,而後遞歸調用writeObject方法(也就是一層層地剝開目標對象,直到找到基本數據類型爲止)

UnSafe類的相關的內容可自行搜索,這裏就簡短描述了,畢竟沒用過。

Unsafe類是在sun.misc包下,不屬於Java標準。可是不少Java的基礎類庫,使用Unsafe可用來直接訪問系統內存資源並進行自主管理。Unsafe類在提高Java運行效率,加強Java語言底層操做能力方面起了很大的做用。Unsafe可認爲是Java中留下的後門,提供了一些低層次操做,如直接內存訪問、線程調度等。可是官方並不建議使用Unsafe
來源:https://www.jb51.net/article/140726.htm

小結:
Serializable實現原理本質是利用UnSafe類去獲取對象的數據。爲何不是用反射呢?
其實反射獲取數據最後也是經過UnSafe類去獲取。

例如:咱們經過反射獲取上面User中的age屬性,getInt的實現以下:

public void getUserAgeByReflection() throws Exception { 
 
   
    //獲取student類的字節碼對象
    Class clazz = Class.forName("model.User");
    //用反射建立一個對象
    Object user = clazz.newInstance();
    //獲取字段
    Field ageField = clazz.getDeclaredField("age");

    ageField.getInt(user);

}

ageField.getInt(user); 的實如今 UnsafeCharacterFieldAccessorImpl 類,咱們能夠看出反射底層仍是調用的 UnSafe

public int getInt(Object var1) throws IllegalArgumentException { 
 
   
    this.ensureObj(var1);
    return unsafe.getInt(var1, this.fieldOffset);
}

具體信息可查看源碼。

因爲Serializable是經過I/O讀寫存儲在磁盤上的數據, 而且使用了UnSafe類去獲取數據。
咱們知道反射會有性能問題,而反射底層實現就是UnSafe類,因此使用Serializable序列化會有性能問題。
Android的卡頓問題絕對是用戶最頭疼的問題,所以在Android上經過Parcelable來優化序列化致使的性能問題。

Parcelable

ParcelableAndroid 提供的序列化接口。

這裏舉個例子:兩個頁面之間傳遞對象

  • 先定義對象並實現 Parcelable
class UserParcelable implements Parcelable { 
 
   
    private String name = "";
    private int age = 0;

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

    protected UserParcelable(Parcel in) { 
 
   
        name = in.readString();
        age = in.readInt();
    }

    /** * 反序列化 */
    public static final Creator<UserParcelable> CREATOR = new Creator<UserParcelable>() { 
 
   
        @Override
        public UserParcelable createFromParcel(Parcel in) { 
 
   
            return new UserParcelable(in);
        }

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

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

    /** * 序列化 * @param dest * @param flags */
    @Override
    public void writeToParcel(Parcel dest, int flags) { 
 
   
        // 寫入數據
        dest.writeString(name);
        dest.writeInt(age);
    }

    @Override
    public String toString() { 
 
   
        return "UserParcelable{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

頁面FirstAc經過Intent傳遞給SecondAc

// 發送數據
Intent intent = new Intent(FirstAc.this, SecondAc.class);
intent.putExtra("UserParcelable", new UserParcelable("李四", 18));
startActivity(intent);

// 接收數據
Intent intent = getIntent();
UserParcelable userParcelable = intent.getParcelableExtra("UserParcelable");
Log.d("SecondAc", userParcelable.toString());

Parcelable 實現原理

仍是參考小緣大佬的源碼解讀:地址-https://www.wanandroid.com/wenda/show/9002

它的各類writeXXX方法,在native層都是會調用Parcel.cppwrite方法,它是經過memcpy函數直接複製內存地址由一個叫mDatauint8_t來保存。

read方法同理,它也是經過 memcpy函數來把mData上的某部分數據複製出來。

兩者的區別

區別 Serializable Parcelable
所屬API JAVA API Android SDK API
原理 序列化和反序列化過程須要在磁盤上進行大量的I/O操做,且經過反射實現有性能問題 序列化和反序列化過程直接在native端操做內存
開銷 開銷大 開銷小
效率 很高
使用場景 序列化到本地或者經過網絡傳輸 本地內存序列化

總結

  • 序列化的做用統一數據格式便於數據傳輸和保存。

  • 因爲計算機中數據是以字節流傳遞,所以大部分的編程語言實現序列化的作法都是轉爲byte[]
    而全球衆多文字轉爲字節是經過特定的編碼方式實現,例如GBK編碼中,一個英文字符表示一個字節,一箇中文字符表示兩個字節。

  • Android中有兩種方式實現序列化,一個是實現Serializable,另外一個是實現Parcelable
    SerializableJava提供的序列化方案,
    ParcelableAndroid爲了解決Serializable序列化致使的性能問題而提供的方案。

  • Serializable實現簡單,可是會有性能問題,緣由是它數據的讀寫是經過I/O在磁盤上操做,並且獲取數據使用的是和反射底層用的是一個類:UnSafe類,該類能夠直接操做內存,官方建議不熟悉該類最好別使用。
    Parcelable實現略微複雜,經過writeXXX,readXXX方法實現數據的讀寫,最終經過native的方法去內存中讀取數據。

  • Serializable序列化後的數據能夠進行網絡傳輸本地存儲
    Parcelable是基於Android的,只能在內存間傳輸。

參考

本文同步分享在 博客「_龍衣」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索