背景
在開發插件化App
時用到了AIDL
實現進程間通訊。而AIDL
要想傳遞對象類型
的數據就須要將對象序列化。html
在 Android
開發中,咱們常常須要對對象
進行序列化與反序列化操做。
最多見的就是經過 Intent
傳輸數據時,Intent
只能傳輸基本數據類型、String
類型和可序列化與反序列化的對象類型,
要想經過 Intent
傳遞對象類型,咱們須要讓該對象類型支持序列化和反序列化。java
咱們知道,Android
給咱們提供了兩種方式來完成序列化與反序列化過程:android
- 一種是
Serializable
方式 - 另外一種是
Parcelable
方式;
本篇文章將盡量詳細講述兩種方式實現序列化。git
咱們首先來了解序列化和反序列化。github
序列化和反序列化是什麼?
除了基本數據類型外的其它類型,如對象、文件、圖片
都有本身的數據格式,很難統一傳輸和保存。
基本數據類型提供了轉爲byte[]
的方法,因此數據能夠統一成字節流
。web
爲了實現對象、文件、圖片
數據也能在計算機中傳輸和保存,所以能夠將這些數據格式也轉爲字節流
。編程
在特定語言
語言範疇
內將一個實例對象編碼成字節流,稱爲序列化;將一個字節流中讀出一個對象實例,稱爲反序列化。數組
PS:基本數據類型網絡
- 四種整數類型
byte、short、int、long
- 兩種浮點數類型
float、double
- 一種字符類型
char
- 一種布爾類型
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 實現序列化的兩種方式
Serializeable
:Java
提供的序列化方式Parcelable
:Android
提供的序列化方式
Serializable
Serializable
是 Java
提供的序列化接口。
要將對象序列化,只需讓對象
實現java.io.Serializable
接口便可。
實現Serializable
的類對象
的全部屬性必須是可序列化的。若是有一個屬性不須要可序列化的,則使用transient
關鍵字修飾。
例如:將對象保存到文本文件並讀取出來。
寫入文件咱們會經過
ObjectOutputStream
的writeObject(Object obj)
方法去實現,讀取文件咱們會經過ObjectInputStream
的readObject()
去實現。
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
- 藉助
ObjectStreamClass
記錄目標對象的類型,類名等信息,這個類裏面還有個ObjectStreamField
數組,用來記錄目標對象的內部變量;- 在
defaultWriteFields
方法中,會先經過ObjectStreamClass
的getPrimFieldValues
方法,把基本數據類型的值都複製到一個叫primVals
的byte
數組上;- 接着經過
getPrimFieldValues
方法來獲取全部成員變量的值,出乎意料的是:這兩個獲取值的方法,裏面都不是咱們常規的反射操做(Field.get)
,而是經過操做Unsafe類
來完成的;- 遍歷剩下不是基本數據類型的成員變量,而後遞歸調用
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
Parcelable
是 Android
提供的序列化接口。
這裏舉個例子:兩個頁面之間傳遞對象
- 先定義對象並實現
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.cpp
的write
方法,它是經過memcpy函數
直接複製內存地址由一個叫mData
的uint8_t
來保存。
read
方法同理,它也是經過memcpy函數
來把mData
上的某部分數據複製出來。
兩者的區別
區別 | Serializable | Parcelable |
---|---|---|
所屬API | JAVA API | Android SDK API |
原理 | 序列化和反序列化過程須要在磁盤上進行大量的I/O操做,且經過反射實現有性能問題 | 序列化和反序列化過程直接在native端操做內存 |
開銷 | 開銷大 | 開銷小 |
效率 | 低 | 很高 |
使用場景 | 序列化到本地或者經過網絡傳輸 | 本地內存序列化 |
總結
-
序列化的做用
統一數據格式便於數據傳輸和保存。 -
因爲計算機中數據是以
字節流
傳遞,所以大部分的編程語言實現序列化的作法都是轉爲byte[]
而全球衆多文字轉爲字節
是經過特定的編碼方式實現,例如GBK
編碼中,一個英文字符表示一個字節,一箇中文字符表示兩個字節。 -
Android
中有兩種方式實現序列化,一個是實現Serializable
,另外一個是實現Parcelable
。
Serializable
是Java
提供的序列化方案,
Parcelable
是Android
爲了解決Serializable
序列化致使的性能問題而提供的方案。 -
Serializable
實現簡單,可是會有性能問題,緣由是它數據的讀寫是經過I/O
在磁盤上操做,並且獲取數據使用的是和反射
底層用的是一個類:UnSafe
類,該類能夠直接操做內存,官方建議不熟悉該類最好別使用。
Parcelable
實現略微複雜,經過writeXXX,readXXX
方法實現數據的讀寫,最終經過native
的方法去內存中讀取數據。 -
Serializable
序列化後的數據能夠進行網絡傳輸本地存儲
Parcelable
是基於Android
的,只能在內存間傳輸。
參考
- Java:字節流和字符流(輸入流和輸出流)
- Android中兩種序列化方式的比較Serializable和Parcelable
- 一直在用,但Serializable究竟是怎麼一回事?
- 每日一問 Parcelable 爲何效率高於 Serializable ?
- Java中Unsafe類詳解
本文同步分享在 博客「_龍衣」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。