公園裏,一位仙風鶴骨的老者在打太極,一招一式都仙氣十足,一個年輕人走過去:「大爺,太極這玩意兒花拳繡腿,你練它幹啥?」老者淡淡一笑:「年輕人,你尚未領悟到太極的真諦,這樣,你用最大力氣打我試試。」因而年輕人用力打了老頭一拳,被訛了八萬六。java
從段子就能看出來,今天這篇博客又是一碗炒冷飯。序列化使用很簡單,可是其中的一些細節並非全部人都清楚。在平常的應用開發中,咱們可能須要讓某些對象離開內存空間,存儲到物理磁盤,以便長期保存,同時也能減小對內存的壓力,而在須要時再將其從磁盤讀取到內存,好比將某個特定的對象保存到文件中,隔一段時間後再把它讀取到內存中使用,那麼該對象就須要實現序列化操做,在java中可使用Serializable接口實現對象的序列化,而在android中既可使用Serializable接口實現對象序列化也可使用Parcelable接口實現對象序列化,可是在內存操做時更傾向於實現Parcelable接口,這樣會使用傳輸效率更高效。接下來咱們將分別詳細地介紹這樣兩種序列化操做。android
首先來了解一下序列化與反序列化。數據庫
因爲存在於內存中的對象都是暫時的,沒法長期駐存,爲了把對象的狀態保持下來,這時須要把對象寫入到磁盤或者其餘介質中,這個過程就叫作序列化。數組
反序列化偏偏是序列化的反向操做,也就是說,把已存在在磁盤或者其餘介質中的對象,反序列化(讀取)到內存中,以便後續操做,而這個過程就叫作反序列化。bash
歸納性來講序列化是指將對象實例的狀態存儲到存儲媒體(磁盤或者其餘介質)的過程。在此過程當中,先將對象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉換爲字節流,而後再把字節流寫入數據流。在隨後對對象進行反序列化時,將建立出與原對象徹底相同的副本。網絡
一個對象要實現序列化操做,該類就必須實現了Serializable接口或者Parcelable接口,其中Serializable接口是在java中的序列化抽象類,而Parcelable接口則是android中特有的序列化接口,在某些狀況下,Parcelable接口實現的序列化更爲高效,關於它們的實現案例咱們後續會分析,這裏只要清楚知道實現序列化操做時必須實現Serializable接口或者Parcelable接口之一便可。架構
主要有如下狀況(但不限於如下狀況) 1)內存中的對象寫入到硬盤; 2)用套接字在網絡上傳送對象;框架
Serializable是java提供的一個序列化接口,它是一個空接口,專門爲對象提供標準的序列化和反序列化操做,使用Serializable實現類的序列化比較簡單,只要在類聲明中實現Serializable接口便可,同時強烈建議聲明序列化標識。以下:編輯器
public class User 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;
}
}
複製代碼
如上述代碼所示,User類實現的Serializable接口並聲明瞭序列化標識serialVersionUID,該ID由編輯器生成,固然也能夠自定義,如1L,5L,不過仍是建議使用編輯器生成惟一標識符。那麼serialVersionUID有什麼做用呢?實際上咱們不聲明serialVersionUID也是能夠的,由於在序列化過程當中會自動生成一個serialVersionUID來標識序列化對象。既然如此,那咱們還需不須要要指定呢?緣由是serialVersionUID是用來輔助序列化和反序列化過程的,原則上序列化後的對象中serialVersionUID只有和當前類的serialVersionUID相同纔可以正常被反序列化,也就是說序列化與反序列化的serialVersionUID必須相同纔可以使序列化操做成功。具體過程是這樣的:序列化操做的時候系統會把當前類的serialVersionUID寫入到序列化文件中,當反序列化時系統會去檢測文件中的serialVersionUID,判斷它是否與當前類的serialVersionUID一致,若是一致就說明序列化類的版本與當前類版本是同樣的,能夠反序列化成功,不然失敗。報出以下UID錯誤:ide
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~,瞭解這麼多,下面來看一個如何進行對象序列化和反序列化的列子:
public class Demo {
public static void main(String[] args) throws Exception {
// 構造對象
User user = new User();
user.setId(1000);
user.setName("韓梅梅");
// 把對象序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/serializable/user.txt"));
oos.writeObject(user);
oos.close();
// 反序列化到內存
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/serializable/user.txt"));
User userBack = (User) ois.readObject();
System.out.println("read serializable user:id=" + userBack.getId() + ", name=" + userBack.getName());
ois.close();
}
}
複製代碼
輸出結果:
read serializable user:id=1000, name=韓梅梅
複製代碼
從代碼能夠看出只須要ObjectOutputStream和ObjectInputStream就能夠實現對象的序列化和反序列化操做,經過流對象把user對象寫到文件中,並在須要時恢復userBack對象,可是二者並非同一個對象了,反序列化後的對象是新建立的。這裏有兩點特別注意的是若是反序列類的成員變量的類型或者類名,發生了變化,那麼即便serialVersionUID相同也沒法正常反序列化成功。其次是靜態成員變量屬於類不屬於對象,不會參與序列化過程,使用transient關鍵字標記的成員變量也不參與序列化過程。
另外,系統的默認序列化過程是能夠改變的,經過實現以下4個方法,便可以控制系統的默認序列化和反序列過程:
public class User 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方法,在這個階段,
* 能夠進行本身操做,將須要進行序列化的對象換成咱們指定的對象.
* 通常不多重寫該方法
*/
private Object writeReplace() throws ObjectStreamException {
System.out.println("writeReplace invoked");
return this;
}
/**
*接着系統將調用writeObject方法,
* 來將對象中的屬性一個個進行序列化,
* 咱們能夠在這個方法中控制住哪些屬性須要序列化.
* 這裏只序列化name屬性
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
System.out.println("writeObject invoked");
out.writeObject(this.name == null ? "默認值" : this.name);
}
/**
* 反序列化時,系統會調用readObject方法,將咱們剛剛在writeObject方法序列化好的屬性,
* 反序列化回來.而後經過readResolve方法,咱們也能夠指定系統返回給咱們特定的對象
* 能夠不是writeReplace序列化時的對象,能夠指定其餘對象.
*/
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序列化時的對象,能夠指定其餘對象.
* 通常不多重寫該方法
*/
private Object readResolve() throws ObjectStreamException {
System.out.println("readResolve invoked");
return this;
}
}
複製代碼
經過上面的4個方法,咱們就能夠隨意控制序列化的過程了,因爲在大部分狀況下咱們都不必重寫這4個方法,所以這裏咱們也不過介紹了,只要知道有這麼一回事就行。ok~,對於Serializable的介紹就先到這裏。
鑑於Serializable在內存序列化上開銷比較大,而內存資源屬於android系統中的稀有資源(android系統分配給每一個應用的內存開銷都是有限的),爲此android中提供了Parcelable接口來實現序列化操做,Parcelable的性能比Serializable好,在內存開銷方面較小,因此在內存間數據傳輸時推薦使用Parcelable,如經過Intent在activity間傳輸數據,而Parcelable的缺點就使用起來比較麻煩,下面給出一個Parcelable接口的實現案例,你們感覺一下:
public class User implements Parcelable {
public int id;
public String name;
public User friend;
/**
* 當前對象的內容描述,通常返回0便可
*/
@Override
public int describeContents() {
return 0;
}
/**
* 將當前對象寫入序列化結構中
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeString(this.name);
dest.writeParcelable(this.friend, 0);
}
public NewClient() {}
/**
* 從序列化後的對象中建立原始對象
*/
protected NewClient(Parcel in) {
this.id = in.readInt();
this.name = in.readString();
//friend是另外一個序列化對象,此方法序列須要傳遞當前線程的上下文類加載器,不然會報沒法找到類的錯誤
this.friend=in.readParcelable(Thread.currentThread().getContextClassLoader());
}
/**
* public static final一個都不能少,內部對象CREATOR的名稱也不能改變,必須所有大寫。
* 重寫接口中的兩個方法:
* createFromParcel(Parcel in) 實現從Parcel容器中讀取傳遞數據值,封裝成Parcelable對象返回邏輯層,
* newArray(int size) 建立一個類型爲T,長度爲size的數組,供外部類反序列化本類數組使用。
*/
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
/**
* 從序列化後的對象中建立原始對象
*/
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}
/**
* 建立指定長度的原始對象數組
*/
@Override
public User[] newArray(int size) {
return new User[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的對象數組 |
putParcelableArrayListExtra(String name, ArrayList value) | 設置List數組,其元素必須是實現了Parcelable接口的數據 |
除了以上的Intent外系統還爲咱們提供了其餘實現Parcelable接口的類,再如Bundle、Bitmap,它們都是能夠直接序列化的,所以咱們能夠方便地使用它們在組件間進行數據傳遞,固然Bundle自己也是一個相似鍵值對的容器,也可存儲Parcelable實現類,其API方法跟Intent基本類似,因爲這些屬於android基礎知識點,這裏咱們就不過多介紹了。
Serializable的實現,只須要實現Serializable接口便可。這只是給對象打了一個標記(UID),系統會自動將其序列化。而Parcelabel的實現,不只須要實現Parcelabel接口,還須要在類中添加一個靜態成員變量CREATOR,這個變量須要實現 Parcelable.Creator 接口,並實現讀寫的抽象方法。
Serializable的設計初衷是爲了序列化對象到本地文件、數據庫、網絡流、RMI以便數據傳輸,固然這種傳輸能夠是程序內的也能夠是兩個程序間的。而Android的Parcelable的設計初衷是因爲Serializable效率太低,消耗大,而android中數據傳遞主要是在內存環境中(內存屬於android中的稀有資源),所以Parcelable的出現爲了知足數據在內存中低開銷並且高效地傳遞問題。
Serializable使用IO讀寫存儲在硬盤上。序列化過程使用了反射技術,而且期間產生臨時對象,優勢代碼少,在將對象序列化到存儲設置中或將對象序列化後經過網絡傳輸時建議選擇Serializable。 Parcelable是直接在內存中讀寫,咱們知道內存的讀寫速度確定優於硬盤讀寫速度,因此Parcelable序列化方式性能上要優於Serializable方式不少。因此Android應用程序在內存間數據傳輸時推薦使用Parcelable,如activity間傳輸數據和AIDL數據傳遞。大多數狀況下使用Serializable也是沒什麼問題的,可是針對Android應用程序在內存間數據傳輸仍是建議你們使用Parcelable方式實現序列化,畢竟性能好不少,其實也沒多麻煩。 Parcelable也不是不能夠在網絡中傳輸,只不過實現和操做過程過於麻煩而且爲了防止android版本不一樣而致使Parcelable可能不一樣的狀況,所以在序列化到存儲設備或者網絡傳輸方面仍是儘可能選擇Serializable接口。
在程序開發過程當中,咱們實現Parcelable接口的代碼都是相似的,若是咱們每次實現一個Parcelable接口類,就得去編寫一次重複的代碼,這顯然是不可取的,不過幸運的是,android studio 提供了自動實現Parcelable接口的方法的插件,至關實現,咱們只須要打開Setting,找到plugin插件,而後搜索Parcelable插件,最後找到android Parcelable code generator 安裝便可:
重啓android studio後,咱們建立一個User類,以下:
public class User {
public int id;
public int age;
public String name;
}
複製代碼
而後使用剛剛安裝的插件協助咱們生成實現Parcelable接口的代碼,window快捷鍵:Alt+Insert,Mac快捷鍵:cmd+n,以下:
最後結果以下:
public class User implements Parcelable {
public int id;
public int age;
public String name;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeInt(this.age);
dest.writeString(this.name);
}
public User() {
}
protected User(Parcel in) {
this.id = in.readInt();
this.age = in.readInt();
this.name = in.readString();
}
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
複製代碼
在正常狀況下,AS是默認關閉serialVersionUID生成提示的,咱們須要打開setting,找到檢測(Inspections選項),開啓 Serializable class without serialVersionUID 檢測便可,以下:
而後新建User類實現Serializable接口,右側會提示添加serialVersionUID,以下:
鼠標放在類名上,Alt+Enter(Mac:cmd+Enter),快捷代碼提示,生成serialVersionUID便可:
最終生成結果:
public class User implements Serializable {
private static final long serialVersionUID = 6748592377066215128L;
public int id;
public int age;
public String name;
}
複製代碼
以上就是Android序列化的所有內容,很簡單,可是也有細節。我有一個想法,就是後面專門寫一些表面很簡單可是細節可能不清楚的知識點,咱們不要始終把目光彙集在大框架上、高端前沿技術什麼的,偶爾研究研究基礎的東西也不錯。
若是你看到了這裏,以爲文章寫得不錯就給個贊唄?若是你以爲那裏值得改進的,請給我留言。必定會認真查詢,修正不足。謝謝。
爲何某些人會一直比你優秀,是由於他自己就很優秀還一直在持續努力變得更優秀,而你是否是還在知足於現狀心裏在竊喜!但願讀到這的您能點個小贊和關注下我,之後還會更新技術乾貨,謝謝您的支持!
轉發分享+點贊,關注我獲取更多知識點
Android架構師之路很漫長,一塊兒共勉吧!