Android 序列化框架 Gson 原理分析,可以優化嗎?
theme: jzman
本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 提問。
前言
大家好,我是小彭。
Gson 是 Google 推出的 Java Json 解析庫,具有接入成本低、使用便捷、功能擴充套件性良好等優點,想必大家都很熟悉了。在這篇文章裡,我們將討論 Gson 的基本用法和以及主要流程的原始碼分析。
學習路線圖:
1. Gson 的基本使用
Gradle 依賴
groovy
dependencies {
implementation 'com.google.code.gson:gson:2.10'
}
1.1 GsonBuilder 配置項
Gson 類是整個庫的核心 API,在進行任何序列化或反序列化之前,我們都需要獲得一個 Gson 物件。可以直接 new 建立預設配置的 Gson 物件,也可以使用 GsonBuilder 構造者配置 Gson 物件。
事實上,一個 Gson 物件代表一個 Gson 工作環境,不同 Gson 物件之間的配置和快取都不會複用。 因此,在專案中有必要在 common 層提供一個全域性的 Gson 物件,既有利於統一序列化配置,也是 Gson 效能優化的基本保障。
GsonBuilder 使用示例
groovy
Gson gson = new GsonBuilder()
// 設定自定義解析(不支援協變)
.registerTypeAdapter(Id.class, new IdTypeAdapter())
// 設定自定義解析(支援協變)
registerTypeHierarchyAdapter(List.class, new MyListTypeAdapter())
// 設定自定義解析(以工廠方式)
.registerTypeAdapterFactory(new IdTypeAdapterFactory())
// 設定日期格式
.setDateFormat("yyyy-MM-dd HH:mm:ss:SSS")
// 設定自動切換命名風格規則(預設不切換命名風格)
.setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
// 設定過濾指定欄位識別符號(預設只過濾 transient 和 static 欄位)
.excludeFieldsWithModifiers(Modifier.TRANSIENT | Modifier.STATIC)
// 設定類或欄位過濾規則
.setExclusionStrategies(new MyExclusionStrategy1())
// 設定過濾規則(只適用於序列化)
.addSerializationExclusionStrategy(new MyExclusionStrategy2())
// 設定過濾規則(只適用於反序列化)
.addDeserializationExclusionStrategy(new MyExclusionStrategy3())
// 設定序列化版本號
.setVersion(1.0)
// 啟用非基礎型別 Map Key
.enableComplexMapKeySerialization()
// 啟用不過濾空值(預設會過濾空值)
.serializeNulls()
// 啟用 Json 格式化
.setPrettyPrinting()
.create();
1.2 註解配置
Gson 沒有編譯時處理,所以註解均是執行時註解。
- @SerializedName 欄位別名: 支援設定多個別名,
value
變數在序列化和反序列化時都有效,而alternate
變數只是在反序列化時做相容而已; - @Expose 欄位暴露:
- 預設情況下,一個類中所有欄位都會暴露,但使用 @Expose 註解後,只有宣告註解的欄位才會暴露;
- 註解的
serialize
變數或deserialize
變數可以宣告欄位只參與序列化或反序列化,預設都參與。
- @JsonAdapter 註解: 宣告在具體類或欄位上,用於更細粒度地設定 TypeAdapter,優先順序比 registerTypeAdapter 高;
- @Since 註解: 宣告在具體類或欄位上,宣告欄位的起始序列化版本;
- @Until 註解: 宣告在具體類或欄位上,宣告欄位的終止序列化版本。當欄位的序列化版本滿足 since ≥ GsonBuilder#setVersion 且 GsonBuilder#setVersion ≤ until 時,才會參與序列化;
1.3 *JsonSerializer 和 JsonDeserializer* 自定義解析
JsonSerializer
和 JsonDeserializer
是 Gson 1.x 版本提供的自定義解析 API,是基於樹型結構的解析 API。在解析資料時,它們會將 Json 資料一次性解析為 JsonElement
樹型結構。 JsonElement 代表 Json 樹上的一個節點,有 4 種具體型別:
| JsonElement | 描述 | | --- | --- | | JsonObject | {} 物件 | | JsonArray | [] 陣列 | | JsonPrimitive | 基本型別 | | JsonNull | null 值 |
1.4 TypeAdapter 自定義解析
TypeAdapter
是 Gson 2.0 新增的自定義解析 API,是基於流式結構的 API。事實上, JsonSerializer 和 JsonDeserializer 最終也會被構造為 TreeTypeAdapter
;
相較之下,JsonSerializer & JsonDeserializer 相對方便,但更費記憶體。而 TypeAdapter 更節省記憶體,但不方便。不過,如果需要用到完整資料結構(例如根據 type 欄位按照不同型別解析 data),也可以手動解析為樹型結構。因此 TypeAdapter 這個 API 的優先順序更高。
| | TypeAdapter | JsonSerializer、JsonDeserializer | | --- | --- | --- | | 引入版本 | 2.0 | 1.x | | Stream API | 支援 | 不支援 | | Tree API | 支援,可以手動轉換 | 支援 | | 記憶體佔用 | 小 | 比 TypeAdapter 大 | | 效率 | 高 | 比 TypeAdapter 低 | | 作用範圍 | 序列化 + 反序列化 | 序列化 / 反序列化 |
1.5 registerTypeAdapter 和 registerTypeHierarchyAdapter 的區別
- registerTypeAdapter 是不變型的: 只會對註冊的型別生效。例如註冊
<List.class,TypeAdapter>
,則只會影響List
型別的欄位,但不會影響 ArrayList 型別的欄位; - registerTypeHierarchyAdapter 是協變型的: 會對註冊的型別及其子類生效。例如註冊
<List.class,TypeAdapter>
,則只會影響List
、ArrayList
型別的欄位;
| | registerTypeAdapter | registerTypeHierarchyAdapter | | --- | --- | --- | | 支援泛型 | 是 | 否 | | 支援繼承 | 否 | 是 |
2. Gson 原始碼分析
這一節,我們來分析 Gson 核心流程的工作原理和原始碼。
2.1 說一下 Gson 解析的工作過程
“TypeAdapter” 是 Gson 解析的重要角色,Gson 每次解析一種物件型別,首先需要建立一個 TypeAdapter 物件,之後所有的解析工作都會交給其中的 TypeAdapter#write 和 TypeAdapter#read 方法;
Java Bean 型別的 TypeAdapter 物件是交給 “ReflectiveTypeAdapterFactory” 建立的。每建立一種型別的 TypeAdapter,都需要遞迴地使用 “反射” 遍歷所有欄位,並解析欄位上的註解,生成一個 <serializeName - BoundFiled>
的對映表。
- 在序列化時,首先使用反射獲取欄位值,再使用欄位的
BoundFiled
序列化; - 在反序列化時,首先建立物件例項(下文會討論如何建立),再使用依次使用欄位的
BoundField
反序列為欄位型別的值,再通過反射為欄位賦值。
由於欄位值的寫入和讀取是通過 Field
元資料反射操作的,所以 private 欄位也可以操作。
在構造 Gson 物件時,已經初始化了一系列 TypeAdapter 建立工廠,開發者可以註冊自定義的 TypeAdapter:
Gson.java
```java
Gson(final Excluder excluder, ...) {
List
// built-in type adapters that cannot be overridden
factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
factories.add(ObjectTypeAdapter.FACTORY);
// 過濾規則
factories.add(excluder);
// 自定義 TypeAdapter
factories.addAll(factoriesToBeAdded);
// 1. 基礎型別
factories.add(TypeAdapters.STRING_FACTORY);
factories.add(TypeAdapters.INTEGER_FACTORY);
...
// 2. 複合型別
// 2.1 列表型別
factories.add(new CollectionTypeAdapterFactory(constructorConstructor));
// 2.2 集合型別
factories.add(new MapTypeAdapterFactory(constructorConstructor, complexMapKeySerialization));
this.jsonAdapterFactory = new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor);
factories.add(jsonAdapterFactory);
// 2.3 列舉型別
factories.add(TypeAdapters.ENUM_FACTORY);
// 2.4 Java Bean 型別
factories.add(new ReflectiveTypeAdapterFactory(constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory));
} ```
通過 Gson#getAdapter 查詢匹配 TypeAdapter 的方法:
Gson.java
```java
// TypeAdapter 快取對映表
public
使用 ReflectiveTypeAdapterFactory 工廠為每種型別建立 TypeAdapter 物件:
ReflectiveTypeAdapterFactory.java
```java
// 1. 建立 TypeAdapter 物件
@Override
public
public static abstract class Adapter
// 2. 反序列化過程
@Override
public T read(JsonReader in) {
// 2.1 建立物件
T instance = constructor.construct();
// 2.2 消費 {
in.beginObject();
// 2.3 遞迴反序列化每個欄位
while (in.hasNext()) {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
// 讀取流並設定到 instance 物件中
readIntoField(in, instance);
}
}
// 2.4 消費 }
in.endObject();
return instance;
}
// 3. 序列化過程
@Override
public void write(JsonWriter out, T value) {
// 3.1 寫入 {
out.beginObject();
// 3.2 遞迴序列化每個欄位
for (BoundField boundField : boundFields.values()) {
// 將物件的每個欄位寫入流中
boundField.write(out, value);
}
// 3.3 寫入 }
out.endObject();
}
}
// -> 1.2 getBoundFields:解析每個欄位的介面卡
private Map
// -> 1.2.2.3 為所有欄位別名建立 BoundField 物件 private ReflectiveTypeAdapterFactory.BoundField createBoundField( final Gson context, final Field field, final String name, final TypeToken<?> fieldType, boolean serialize, boolean deserialize) { // 基本型別 final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); // @JsonAdapter 註解 JsonAdapter annotation = field.getAnnotation(JsonAdapter.class); TypeAdapter<?> mapped = null; if (annotation != null) { mapped = jsonAdapterFactory.getTypeAdapter(constructorConstructor, context, fieldType, annotation); } final boolean jsonAdapterPresent = mapped != null; if (mapped == null) mapped = context.getAdapter(fieldType); final TypeAdapter<?> typeAdapter = mapped; return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
@Override
void write(JsonWriter writer, Object value) {
if (!serialized) return;
// 通過反射讀取欄位值
Object fieldValue = field.get(value);
TypeAdapter t = jsonAdapterPresent ? typeAdapter : new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType());
// 寫出到流
t.write(writer, fieldValue);
}
@Override
void readIntoField(JsonReader reader, Object target) {
// 從流讀取
Object fieldValue = typeAdapter.read(reader);
// 通過反射寫入欄位值
field.set(target, fieldValue);
}
};
} ```
2.2 List & Set & Map 等容器型別是如何解析的?
- 1、在預置的容器 TypAdapter 中,會先通過容器型別的
RawType
獲取容器構造器,再根據泛型實參elementType
獲取元素型別的 TypeAdapter; - 2、在序列化時,先寫入 [ 左中括號,再用元素型別的 TypeAdapter 依次序列化元素物件,再寫入 ] 右中括號;
- 3、在反序列化時,先建立集合物件,再用元素型別的 TypeAdapter 依次反序列化元素物件;
- 4、Map 型別需要維護 Key 和 Value 兩個 TypeAdapter。
CollectionTypeAdapterFactory.java
```java
// 1. 建立 TypeAdapter
public
private static final class Adapter
// 3. 序列化過程
@Override
public void write(JsonWriter out, Collection<E> collection) {
// 3.1 寫入 [
out.beginArray();
// 3.2 使用 1.2 步驟的 TypeAdapter 序列化每個元素
for (E element : collection) {
elementTypeAdapter.write(out, element);
}
// 3.3 寫入 ]
out.endArray();
}
} ```
2.3 列舉型別是如何解析的?
- 1、在預置的 EnumTypeAdapter 介面卡中,會先獲取列舉型別的整個列舉列表,並生成 2 個對映表。
=""> 對映表 - <列舉 - name> 對映表
- 2、在序列化時,會寫入列舉的 name。在反序列化時,會根據 name 查詢列舉物件。
TypeAdapters.java
```java
private static final class EnumTypeAdapter
public EnumTypeAdapter(Class<T> classOfT) {
for (T constant : classOfT.getEnumConstants()) {
String name = constant.name()
nameToConstant.put(name, constant);
constantToName.put(constant, name);
}
}
@Override
public T read(JsonReader in) {
return nameToConstant.get(in.nextString());
}
@Override
public void write(JsonWriter out, T value) {
out.value(constantToName.get(value));
}
} ```
2.4 同類型巢狀會不會無限遞迴?
ReflectiveTypeAdapterFactory
在建立物件的 TypeAdapter 介面卡時,需要遞迴的建立每個欄位的 TypeAdapter。如果欄位的型別正好與類的型別相同,那麼又會觸發建立一個相同的 TypeAdapter,造成無限遞迴。例如:
無限遞迴的例子
java
public class Article {
public Article linkedArticle;
}
分析發現,無限遞迴只發生在第一次建立某個 Java Bean 型別的 TypeAdapter 時,而下一次會從快取獲取,不會發生無限遞迴。因此,Gson 的做法是:
- 1、在每次新建立 TypeAdapter 前,先在臨時對映表中建立一個
FutureTypeAdapter
代理物件。在建立真實的 TypeAdapter 後,將其注入到代理物件中。這樣在遞迴獲取欄位的 TypeAdapter 時,就會拿到代理物件,而不是重新建立 TypeAdapter,因此解決遞迴問題; - 2、另外,考慮到多執行緒環境下,臨時對映表的新增和移除會有併發問題,因此 Gson 的策略是使用
ThreadLocal
隔離各個執行緒的臨時對映表。
Gson.java
```java // 執行緒隔離的對映表 private final ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>> calls = new ThreadLocal<Map<TypeToken<?>, FutureTypeAdapter<?>>>(); // 執行緒共享的對映表(基於 ConrurrentHashMap) private final Map<TypeToken<?>, TypeAdapter<?>> typeTokenCache = new ConcurrentHashMap<TypeToken<?>, TypeAdapter<?>>();
public
// 2. 初始化當前執行緒的臨時對映表
Map<TypeToken<?>, FutureTypeAdapter<?>> threadCalls = calls.get();
boolean requiresThreadLocalCleanup = false;
if (threadCalls == null) {
threadCalls = new HashMap<TypeToken<?>, FutureTypeAdapter<?>>();
calls.set(threadCalls);
requiresThreadLocalCleanup = true;
}
// 3. 嘗試從臨時對映表獲取(遞迴呼叫時,會從這裡獲取到代理 TypeAdapter,而不會走到下面的 factory.create
FutureTypeAdapter<T> ongoingCall = (FutureTypeAdapter<T>) threadCalls.get(type);
if (ongoingCall != null) {
return ongoingCall;
}
try {
// 4.1 建立代理 TypeAdapter
FutureTypeAdapter<T> call = new FutureTypeAdapter<T>();
threadCalls.put(type, call);
for (TypeAdapterFactory factory : factories) {
// 4.2 建立 TypeAdapter
TypeAdapter<T> candidate = factory.create(this, type);
// 4.3 將真實的 TypeAdapter 注入到代理 TypeAdapter 中
if (candidate != null) {
call.setDelegate(candidate);
// 4.4 將 TypeAdapter 寫入快取
typeTokenCache.put(type, candidate);
return candidate;
}
}
} finally {
// 5. 清除臨時對映表
threadCalls.remove(type);
if (requiresThreadLocalCleanup) {
calls.remove();
}
}
} ```
2.5 Gson 是如何建立物件的?
- 1、基礎型別:Integer、Calendar 等基礎型別由固定的 TypeAdapter,會通過 new 關鍵字建立物件;
- 2、列舉:列舉的序列化和反序列化只是在列舉名 name 和列舉物件之間切換,不會建立新的列舉物件;
- 3、List & Set & Map:容器型別會通過預置的物件建立工廠,呼叫 new 關鍵字建立物件;
- 4、Java Bean:Java Bean 的建立分為多種可能:
- 情況 1:自定義了物件建立工廠
InstanceCreator
,則優先通過自定義工廠建立; - 情況 2:存在預設的無參建構函式,則通過反射建構函式建立;
- 情況 3:使用 Unsafe API 兜底建立物件。
- 情況 1:自定義了物件建立工廠
ConstructorConstructor.java
```java
public
// InstanceCreator API
final InstanceCreator<T> typeCreator = (InstanceCreator<T>) instanceCreators.get(type);
if (typeCreator != null) {
return new ObjectConstructor<T>() {
@Override
public T construct() {
return typeCreator.createInstance(type);
}
};
}
final InstanceCreator<T> rawTypeCreator = (InstanceCreator<T>) instanceCreators.get(rawType);
if (rawTypeCreator != null) {
return new ObjectConstructor<T>() {
@Override
public T construct() {
return rawTypeCreator.createInstance(type);
}
};
}
// 無參建構函式
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
return defaultConstructor;
}
// 容器型別
ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}
// Unsafe API
return newUnsafeAllocator(type, rawType);
} ```
2.6 Gson 隱藏的坑
當 Class 未提供預設的無參建構函式時,Gson 會使用 Unsafe API 兜底來建立物件。Unsafe API 主要提供一些用於執行低級別、不安全操作的方法,也提供了一個非常規例項化物件的 allocateInstance
方法。
這個 API 不會呼叫建構函式
- 1、建構函式引數的預設值丟失;
- 2、欄位的預設值丟失;
- 3、Kotlin 非空型別失效;
- 4、初始化塊未執行;
- 5、by 屬性代理(沒有建立代理物件)
3. Gson 如何解析泛型型別?
由於 Java 有泛型擦除,無法直接在 .class
語法上宣告泛型資訊,Gson 的方法是要求程式設計師建立匿名內部類,由 Gson 在執行時通過反射獲取類宣告上的泛型資訊。
示例程式碼
java
// 非法:
Response<User> obj = Gson().fromJson<Response<User>>(jsonStr, Response<User>.class)
// 合法;
TypeToken token = object : TypeToken<Response<User>>() {}
Response<User> obj = Gson().fromJson<Response<User>>(jsonStr, token.type)
為什麼反序列化泛型類要使用匿名內部類呢?
原理是 Class 檔案中的 Signature
屬性會保持類簽名信息,而 TypeToken
只是一個工具類,內部通過反射獲取類簽名中泛型資訊並返回 Type
型別。
TypeToken.java
```java protected TypeToken() { this.type = getSuperclassTypeParameter(getClass()); this.rawType = (Class<? super T>) $Gson$Types.getRawType(type); this.hashCode = type.hashCode(); }
// 返回 Response
public final Type getType() { return type; } ```
既然 TypeToken 只是一個獲取 Type 型別的工具類,我們也可以跳過它直接提供 Type,方法是定義 ParameterizedType
引數化型別的子類:
ParameterizedTypeAdapter.java
```java private static class ParameterizedTypeAdapter implements ParameterizedType {
private final Class<?> rawType;
private final Type[] types;
private ParameterizedTypeAdapter(Class<?> rawType, Type... types) {
this.rawType = rawType;
this.types = types;
}
@Override
public Type[] getActualTypeArguments() {
return types;
}
@Override
public Type getRawType() {
return rawType;
}
@Override
public Type getOwnerType() {
return null;
}
} ```
示例程式碼
java
Response<User> obj = new Gson().fromJson<Response<User>>(jsonStr, ParameterizedTypeAdapter(Response.class, User.class))
在 Kotlin 中,還可以使用 reified 實化型別引數簡化:
Utils.kt
```kotlin
inline fun
inline fun
示例程式碼
kotlin
List<User> obj = toList<User>(jsonStr)
4. 總結
今天,我們討論了 Gson 的基本用法和以及主要流程的原始碼分析。
在 Gson 的反序列化中,首次反序列化一個型別的物件時,Gson 需要使用大量反射呼叫解析一個 TypeAdapter 介面卡物件。隨著 Model 的複雜程度增加,首次解析的耗時會不斷膨脹。
這個問題在抖音的技術部落格中提到一個解決方案,這個問題我們在下篇文章討論,請關注。
版權宣告
本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!
參考資料
- Java Google Json (Gson) Introduction —— Mithil Shah 著
- Gson — Getting Started with Java-JSON —— Norman Peitek 著
- Javadoc · Gson —— Gson 官方文件
- Gson 原始碼解析和它的設計模式 —— 拉丁吳 著
- 還在被資料類的序列化折磨?是時候丟棄 Gson 了—— bennyhuo 著
- 抖音 Android 效能優化系列:啟動優化實踐(反序列化優化) —— 位元組跳動技術團隊 著
- JSON —— Wikipedia
- LeetCode 周賽 336,多少人直接 CV?
- LeetCode 周賽 335,純純手速場!
- LeetCode 雙週賽 98,腦筋急轉彎轉不過來!
- Android IO 框架 Okio 的實現原理,到底哪裡 OK?
- 12 張圖看懂 CPU 快取一致性與 MESI 協議,真的一致嗎?
- Android 序列化框架 Gson 原理分析,可以優化嗎?
- 為什麼計算機中的負數要用補碼錶示?
- 什麼是二叉樹?
- 我把 CPU 三級快取的祕密,藏在這 8 張圖裡
- 全網最全的 ThreadLocal 原理詳細解析 —— 原理篇
- 程式設計師學習 CPU 有什麼用?
- WeakHashMap 和 HashMap 的區別是什麼,何時使用?
- 萬字 HashMap 詳解,基礎(優雅)永不過時 —— 原理篇
- Java 面試題:說一下 ArrayDeque 和 LinkedList 的區別?
- Java 面試題:說一下 ArrayList 和 LinkedList 的區別?
- Java 面試題:ArrayList 可以完全替代陣列嗎?
- 已經有 MESI 協議,為什麼還需要 volatile 關鍵字?
- JVM 系列(6)吊打面試官:為什麼 finalize() 方法只會執行一次?
- 使用字首和陣列解決"區間和查詢"問題
- NDK 系列(5):JNI 從入門到實踐,萬字爆肝詳解!