Android兩種序列化方式詳解(一):Serializable
前言
在 Android 開發中,我們經常需要對物件進行序列化與反序列化操作,最常見的就是通過 Intent 傳輸資料時,Intent 只能傳輸基本資料型別、String 型別和可序列化與反序列化的物件型別,要想通過 Intent 傳遞物件型別,我們需要讓該物件型別支援序列化和反序列化。
我們知道,Android 給我們提供了兩種方式來完成序列化與反序列化過程:一種是 Serializable 方式,另一種是 Parcelable 方式;本篇文章將詳細講述使用 Serializable 方式實現序列化你所需要知道的一切。
你可能會疑問,使用 Serializable 實現序列化,不是隻要讓類實現 Serializable 介面就可以了嗎,有什麼好講的?那你就 too naive 了少年!除了基礎的直接實現 Serializable 介面之外,我們使用 Serializable 方式實現序列化的過程還有很多需要注意的細節,例如 serialVersionUID 是幹什麼的呢?如果我們想自定義實現序列化與反序列化過程該怎麼辦呢?本文將會詳細介紹這些知識。
目錄
本文講述的知識點如下:
- 怎樣序列化與反序列化一個物件
- serialVersionUID 的作用
- 如何自定義序列化和反序列化過程
- 總結
一、怎樣序列化和反序列化一個物件
想要序列化和反序列化一個物件,首先要讓物件支援序列化與反序列化,使用 Serializable 方式實現序列化相當簡單,只需要讓類實現 Serializable 介面就可以:
public class UserSerial implements Serializable {
現在 UserSerial 類就支援序列化和反序列化了,那麼我們應該怎麼將 UserSerial 類的某個物件序列化到檔案,然後再將其讀取出來呢?當然是使用 ObjectOutputStream
和 ObjectInputStream
啦~
完整的序列化流程如下:
public void serial(UserSerial user) { ObjectOutputStream out = null; try { out = new ObjectOutputStream(new FileOutputStream("temp.txt")); out.writeObject(user); } catch (IOException e) { e.printStackTrace(); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { e.printStackTrace(); } } }
完整的反序列化流程如下,這裡為了防止反序列化異常返回 null,預設我們返回了一個新構造的空 UserSerial 物件:
public UserSerial deserial() { ObjectInputStream in = null; try { in = new ObjectInputStream(new FileInputStream("temp.txt")); return(UserSerial) in.readObject(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { try { if (in != null) { in.close(); } } catch (IOException e) { e.printStackTrace(); } } return new UserSerial(); }
可能有人要吐槽我上面說的都是廢話了,上面這些東西,稍微瞭解點 Java 的都知道啊~沒事,且往下看,我們來說一些可能大家使用過程中沒注意到的細節。
二、serialVersionUID 的作用
我們在使用 Serializable 方式實現序列化時,除了實現 Serializable 介面之外,一般還需要宣告一個 serialVersionUID 靜態欄位,當然我們也可以選擇不宣告這個欄位,那麼我們在使用過程中,要不要指定這個欄位呢?如果指定了,這個欄位的值又是幹什麼用的呢?
其實 serialVersionUID 這個欄位,是序列化和反序列化過程中,用來校驗類是否發生了變動的依據,序列化的時候系統會把當前類的 serialVersionUID 欄位寫入序列化的檔案中,當反序列化的時候,系統會去檢測檔案中的 serialVersionUID,看它是否和當前類的 serialVersionUID 一致,如果一致就說明序列化時類的版本和當前類的版本是相同的,這個時候可以成功反序列化,否則就說明當前類和序列化時的類相比發生了某些變換,比如增刪了某些成員變數等,這個時候是無法正常的反序列化的,並且會報 InvalidClassException
。
一般來講,我們都應該手動指定 serialVersionUID 的值,可以隨意指定一個數字,或者根據編輯器提示自動根據當前類的結構生成 hash 值,這樣如果我們不手動修改,序列化和反序列化過程中 serialVersionUID 欄位的值一直都會是一致的,可以最大限度的保證反序列化過程的成功,就算類結構發生了變動,我們也可以保證那些沒有發生變動的成員變數被成功的反序列化。如果我們不手動指定 serialVersionUID 欄位的值,那麼如果反序列化時相比序列化時的類結構發生了變動,比如增刪了成員變數等,那麼系統就會重新計算當前類的 hash 值,並將其賦值給 serialVersionUID,導致序列化時的類和當前類的 serialVersionUID 值不一致,導致反序列化失敗,從而 crash。
最後要說明的是,如果類結構發生了毀滅性的變化,如類名發生了改變,這個時候就算 serialVersionUID 值一致,也是不能被正常反序列化的,這一點後面還會提到。
三、如何自定義序列化和反序列化過程
現在我們知道如果一個類,實現了 Serializable 介面,那麼在需要的時候,自動完成該類的序列化,與反序列化過程;那麼如果我們想自定義序列化過程與反序列化過程該怎麼辦呢?例如我在版本 1 的時候,將 User 物件的 name 序列化了,但是反序列化的時候,我想把這個這個欄位的值賦值給 nameNew,該怎麼做到呢?
第一部分我們我們提到,使用 Serializable 方式實現序列化是,物件的序列化和反序列化過程是通過 ObjectOutputStream.writeObject
和 ObjectInputStream.readObject
實現的,這裡以 ObjectOutputStream.writeObject
為例分析,追蹤 writeObject
方法程式碼,發現其內部呼叫了 writeObject0 方法:
public final void writeObject(Object obj) throws IOException { // ... writeObject0(obj, false); //... }
進入 writeObject0
方法:
private void writeObject0(Object obj, boolean unshared) throws IOException { // ... if (obj instanceof Class) { writeClass((Class) obj, unshared); } else if (obj instanceof ObjectStreamClass) { writeClassDesc((ObjectStreamClass) obj, unshared); // END Android-changed } else if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } // ... }
因為我們的物件是 Serializable,所以最終會走到 writeOrdinaryObject
方法:
private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared) throws IOException { // ... if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } // ... }
我們看到 writeOrdinaryObject
方法內部做了判斷,看當前類是實現的 Externalizable
介面還是 Serializable
介面,因為我們是實現的 Serializable
介面,所以最終走到了 writeSerialData
方法:
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException { ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { // ... try { curContext = new SerialCallbackContext(obj, slotDesc); bout.setBlockDataMode(true); slotDesc.invokeWriteObject(obj, this); bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); } finally { // ... } curPut = oldPut; } else { defaultWriteFields(obj, slotDesc); } } }
走到這裡就很明確了,我們發現,在 ObjectOutputStream.writeObject
過程中,最終會判斷,當前類本身是否 hasWriteObjectMethod()
。
如果 hasWriteObjectMethod()
為 true,就通過反射,呼叫類自帶的方法 slotDesc.invokeWriteObject(obj, this);
,點進 invokeWriteObject
方法,我們發現內部是通過反射呼叫的 writeObjectMethod
,這是一個 Method
型別的欄位,而給該欄位初始化的程式碼如下:
writeObjectMethod = getPrivateMethod(cl, "writeObject", new Class<?>[]{ ObjectOutputStream.class }, Void.TYPE);
最後我們發現,是通過反射呼叫的實現 Serializable 介面的類的 writeObject
方法,而引數型別是 ObjectOutputStream
型別。
而如果 hasWriteObjectMethod()
為 false,就使用 defaultWriteFields
完成序列化,也就是系統預設的序列化方法,想了解系統預設序列化方法的可以點進原始碼自己檢視。
通過同樣的步驟,我們可以發現 ObjectInputStream.readObject
方法內部,也判斷了類是否有 readObject
方法,有就使用類自己的,沒有就使用預設的。
所以如果我們在使用 Serializable 實現序列化與反序列化是,想實現自定義的序列化和反序列化過程,只需要給當前類新增 writeObject
和 readObject
方法即可,參考程式碼如下:
/** * 自定義序列化過程 */ private void writeObject(ObjectOutputStream out) { ObjectOutputStream.PutField putFields = null; try { putFields = out.putFields(); putFields.put("name", name); // ... } catch (IOException e) { e.printStackTrace(); } } /** * 自定義反序列化過程 */ private void readObject(ObjectInputStream in) { try { ObjectInputStream.GetField readFields = in.readFields(); name = (String) readFields.get("name", ""); // ... } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
認真的同學可能還會有一個疑問,那就是為啥你這自己寫的 readObject
方法沒有返回值呢?這個方法不應該返回一個當前類物件嗎,像 ObjectInputStream.readObject
方法一樣?這個問題的答案也可以通過檢視原始碼得到解答,其實反序列化過程可以分為兩步,一步是通過反射建立類物件的過程,一個是給建立的物件內的變數賦值的過程,而我們重寫的方法只是完成了給當前建立的物件複製的過程,是賦給自身變數的,所以沒有返回值,物件建立的過程在 ObjectInputStream.readObject
方法內,可通過原始碼驗證,這也印證了我們上面所說,如果類名發生了變化,那麼反序列化是不可能成功的,因為找不到類了。
四、總結
經過以上分析,我們可以得出一下結論:
- 我們總應該給使用 Serializable 方式實現序列化與反序列化的類指定 serialVersionUID
- 我們可以通過給類新增
writeObject(ObjectOutputStream out)
和readObject(ObjectInputStream in)
方法的方式自定義序列化和反序列化過程 - 靜態變數和 transient 關鍵字標註的欄位是不參與序列化與反序列化過程的,但是我們仍然可以通過自定義序列化和反序列化的過程打破這個限制,當然也可以通過 Java 提供的另一個序列化介面
Externalizable
來打破這個限制,內部原理差不多
歡迎關注我的公眾號,在這裡你可以看到Android&Java技術思考、GitHub經典庫推薦、犯懶休閒福利、讀書感悟、日常吐槽…技術宅的日常都在這裡

qrcode_for_gh_6d8f72c3b2a2_344.jpg