
数据序列化是每一位移动开发者都无法回避的基础课题。无论是网络请求、文件读写,还是组件间的数据传递,都离不开它。在七年的Android开发经历中,我最近才遇到一个颇为有趣的“坑”。本文将围绕两种序列化方式混合使用引发的问题展开,并揭示其背后关于类加载器工作原理的深层原因。理解之后,你便能实现从Serializable向Parcelable的渐进式迁移,而无需一次性重写所有类。
Parcelable本身并非通用序列化方案,它无法用于磁盘存储或直接处理网络请求,因此不能完全替代Serializable。但不可否认,在Android体系中,Parcelable的性能优势巨大。这意味着在未来很长一段时间内,我们的项目中必然存在两种方案并存的情况,因此掌握它们的正确共存之道至关重要。
问题起源
这个问题的发现,源于公司日志系统中一个偶现的崩溃。我们先通过一个简化示例来定位问题所在:
data class ScreenViewModel(
val payload: Any
): Serializable
在这个页面模型中,payload字段可能承载来自深度链接的字符串,也可能是跳转Activity时传递的Intent对象。乍一看似乎没问题?但让我们看看Intent类的声明:
public class Intent implements Parcelable, Cloneable
问题浮现了:与String不同,Intent本身并未实现 Serializable接口!因此,我们需要设计一套兼容逻辑,能够自动识别类型并选择正确的序列化方式,同时保证上层调用代码无需改动。
data class ScreenViewModel(
val payload: Any
): Parcelable {
companion object CREATOR : Parcelable.Creator<ScreenViewModel> {
override fun createFromParcel(parcel: Parcel): ScreenViewModel {
return ScreenViewModel(parcel)
}
override fun newArray(size: Int): Array<ScreenViewModel?> {
return arrayOfNulls(size)
}
}
constructor(
parcel: Parcel
) : this(
payload = parcel.readPayload(),
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writePayload(payload, flags)
}
override fun describeContents(): Int = 0
}
在这段Parcelable样板代码中,核心在于两个扩展方法:readPayload和writePayload。接下来,我们来实现它们。
实现读写逻辑
要实现动态的类型识别,我们只需在写入对象时,附带一个类型标记:
private const val TYPE_NULL = 0
private const val TYPE_PARCELABLE = 1
private const val TYPE_SERIALIZABLE = 2
fun Parcel.writePayload(payload: Any?, flags: Int) {
when (payload) {
is Parcelable -> {
writeInt(TYPE_PARCELABLE)
writeParcelable(payload, flags)
}
is Serializable -> {
writeInt(TYPE_SERIALIZABLE)
writeSerializable(payload)
}
null -> {
writeInt(TYPE_NULL)
}
else -> {
throw IllegalArgumentException("Unsupported type for payload")
}
}
}
相应地,在读取时根据标记使用对应的方法:
fun Parcel.readPayload(): Any {
return requireNotNull(readNullablePayload())
}
fun Parcel.readNullablePayload(): Any? {
return when (readInt()) {
TYPE_PARCELABLE -> readParcelable<Parcelable>()
TYPE_SERIALIZABLE -> readSerializable<Serializable>()
TYPE_NULL -> null
else -> throw IllegalArgumentException("Unknown payload type")
}
}
现在,只剩下readParcelable和readSerializable这两个方法需要补全了。看起来只是简单地调用系统API?但这里隐藏着一个大坑。先看一个最容易想到的实现:
inline fun <reified T> Parcel.readParcelable(): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
readParcelable(T::class.java.classLoader, T::class.java)
} else {
readParcelable(T::class.java.classLoader)
}
}
这段代码看似没有问题,但当我们要读取的对象本身就是Parcelable这个抽象类型时(正如我们例子中的payload: Any,其运行时类型是Parcelable),问题就出现了。当我们获取Parcelable::class.java.classLoader时,得到的是java.lang.BootClassLoader——这是虚拟机的系统类加载器,其中只有系统类,不包含你App中的任何自定义类,因此反序列化必然会失败。
解决方案其实很直接:如果我们当前的泛型类型T就是Parcelable本身,那么就不使用它自身的类加载器,转而使用当前应用上下文的类加载器(通常是dalvik.system.PathClassLoader,它包含了App的所有类):
inline fun <reified T> Parcel.readParcelable(): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (T::class != Parcelable::class) {
readParcelable(T::class.java.classLoader, T::class.java)
} else {
readParcelable(Thread.currentThread().contextClassLoader, T::class.java)
}
} else {
readParcelable(T::class.java.classLoader)
}
}
处理Serializable的逻辑与之完全相同:
inline fun <reified T> Parcel.readSerializable(): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (T::class != Serializable::class) {
readSerializable(T::class.java.classLoader, T::class.java)
} else {
readSerializable(Thread.currentThread().contextClassLoader, T::class.java)
}
} else {
readSerializable() as T
}
}
这套序列化读写工具方法不仅适用于Any类型的payload字段,如果你的某个属性不确定是Parcelable还是Serializable,也可以直接使用这些方法。
反过来可以吗?把 Parcelable 嵌套进 Serializable 里
我们上面讨论的是“在Parcelable里存放任意可能是Serializable或Parcelable的对象”。在我的实际场景中,需要将Intent放入payload,这个方案是目前唯一可行的——毕竟我们无法修改Android源码让Intent实现Serializable。而且,反过来做通常也缺乏实际意义。
但从纯技术角度讲,反向实现是完全可行的:为你的类实现Serializable接口,然后重写序列化与反序列化的逻辑。你甚至可以使用Externalizable接口来实现(虽然Serializable也支持自定义序列化,但Externalizable的写法通常更加清晰和优雅)。
总结
一个有趣的现象是,面试中常被问及的、看似基础的序列化问题,深入探究后竟藏着如此多SDVK未曾明示的细节。解决它甚至需要对系统运行原理有比许多资深开发者更深入的理解。这恰恰说明了移动开发中基础知识的深度与广度。
原文链接: https://proandroiddev.com/how-to-configure-kotlin-any-serialization-with-parcelable-and-serializable-in-android-550bd88a499a