原文 A study of the Parcelize feature from Kotlin Android Extensions
两年前我写了一篇文章介绍如何利用Kotlin来让Parcelable
接口的实现更简洁和可读性更强. 但是这并不意味着我喜欢手写这部分代码. 和大多数开发者一样, 我相信
the best code is the code you don’t have to write.
能使用工具辅助当然很好, 但是我也要求这工具生成的代码能够满足我对代码质量的要求.
因此我对往项目里增加依赖持保守态度, 另外相比第三方的解决方案, 我更喜欢Jetbrains或者Google写的官方库. 不过在我写下之前的文章后, 随着Kotlin 1.3.40的发布, 情况发生了变化, @Parcelize
现在已经是 Kotlin Android Extensions
提供的一个稳定特性了. 而且在1.3.60版本后, Android Studio插件同样认为这个特性是非实验(non-experimental)特性, 因此可以认为该特性可以用在正式项目上了.
对比手写实现, 我之前列举了使用第三方工具库生成Parcelable代码的几点劣势. 针对这些劣势, 以下是我认为@Parcelize
比其他工具优秀的理由:
- 它是由JetBrains和Google合作开发的官方插件提供的功能, 能保证在未来会一直得到支持
-
@Parcelize
生成的代码非常优雅, 这一点稍后在文章会有讨论 - 不再需要声明
CREATOR
属性和那个容易漏掉的@JvmFiel
注解 - 多得
Parceler
接口, 现在写一个简单的实现就可以实现自定义序列化器, 来处理原本不支持的类型, 或者覆盖默认的实现 - 插件不会生成额外的类, 所有生成的代码会包含在被注解的类中, 所以对于应用来说, 这些代码和你手写是完全一样的
- 不需要其他运行时库(runtime library), 所以不会引入任何额外的方法
配置项目
首先, 你要升级Kotlin Gradle plugins和Android Studio plugin到1.3.60以上.
为了启用Parcelable实现生成器(Parcelable implementation generator)的功能, 你需要在项目中应用Kotlin Android Extensions Gradle plugin, 实现这点只需要在模块的build.gradle
中添加以下声明
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
使用最新版本的插件后, 不再需要额外声明启动实验功能来允许使用@Parcelize
特性和让IDE检测到其可用.
不过我并不喜欢这个插件提供的其他特性, 例如View binding and caching. 在默认情况, 这个特性会扫描布局文件, 为每一个有id的View生成一个静态属性, 同时添加一个隐藏的控件缓存到Activity和Fragment中. 我不想这样. 幸运的是, 我们有方法可以只启用Parcelize而禁用其他特性. 只需要在同样的build.gradle
中, 在android
下面添加以下配置:
// 这个配置是为了禁用除了parcelize外的其他功能
androidExtensions {
features = ["parcelize"]
}
现在我们已经配置完成, 可以使用@Parcelize
注解了.
用法
只需要在实现了Parcelable
接口的类中添加@Parcelize
注解就可以自动生成Parcelable实现代码了. 如下
@Parcelize
class Person(val name: String, val age: Int) : Parcelable
这和之前的文章的例子一样, 仅仅是两行代码.
这里类也可以是一个data class
, 不过不是强制的. 但是, 类中所有需要序列化的属性都需要声明在主构造器中(primary constructor), 并且和data class
一样, 主构造器的所有参数都必须是属性. 这些属性可以是val
或者var
, 私有或者公开都可以.
这个特性的主要限制在于, 它不允许注解abstract
和sealed
类. 想让它们实现Parcelable
接口, 你需要注解它们的子类. 接下来, 我们通过具体的例子来探索更多细节.
分析生成的代码
目前为止看起来还不错, 但是生成的代码质量如何? 生成的代码实现是不是足够优雅?
接下来我们使用Kotlin Bytecode inspector, 将被注解的类反编译回Java, 来看看底层的代码.
基本类型(Basic types)
@Parcelize
class Basic(val aByte: Byte, val aShort: Short,
val anInt: Int, val aLong: Long,
val aFloat: Float, val aDouble: Double,
val aBoolean: Boolean, val aString: String) : Parcelable
会生成以下代码
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeByte(this.aByte);
parcel.writeInt(this.aShort);
parcel.writeInt(this.anInt);
parcel.writeLong(this.aLong);
parcel.writeFloat(this.aFloat);
parcel.writeDouble(this.aDouble);
parcel.writeInt(this.aBoolean);
parcel.writeString(this.aString);
}
对于原始类型和String
这些基本类型, 我们可以看到和预期一样, 调用了Parcel
类的对应方法. 注意, Boolean
使用了writeInt
. 这是因为在Java的字节层面, 布尔值是使用整形(取值为0或1)表示的. 编程语言隐藏了这一细节. 同样地, Short
类型也当作Int
类型, 这是因为Parcel
类没有提供序列化Short
的方法, 所以使用Int
类型代替.
反序列化的代码如下:
public static class Creator implements android.os.Parcelable.Creator {
@NotNull
public final Object[] newArray(int size) {
return new Basic[size];
}
@NotNull
public final Object createFromParcel(@NotNull Parcel in) {
return new Basic(in.readByte(), (short)in.readInt(), in.readInt(), in.readLong(), in.readFloat(), in.readDouble(), in.readInt() != 0, in.readString());
}
}
除了以上代码, 不会有其他额外的构造器或者方法添加到类中, 同时所有反序列化的代码都放到了Creator
类的createFromParcel()
方法中. 这确保最终放进APK文件中方法尽可能少, 这点很重要.
可空类型(Nullable types)
接下来看另一个带可空类型的Kotlin例子
@Parcelize
class NullableFields(
val aNullableInt: Int?,
val aNullableFloat: Float?,
val aNullableBoolean: Boolean?,
val aNullableString: String?
) : Parcelable
生成的序列化代码如下:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
Integer var10001 = this.aNullableInt;
if (var10001 != null) {
parcel.writeInt(1);
parcel.writeInt(var10001);
} else {
parcel.writeInt(0);
}
Float var3 = this.aNullableFloat;
if (var3 != null) {
parcel.writeInt(1);
parcel.writeFloat(var3);
} else {
parcel.writeInt(0);
}
Boolean var4 = this.aNullableBoolean;
if (var4 != null) {
parcel.writeInt(1);
parcel.writeInt(var4);
} else {
parcel.writeInt(0);
}
parcel.writeString(this.aNullableString);
}
对于可空的基本类型, 插件生成的代码会在具体的值前增加一个额外的整形来标记这个值是否为null
, 这个方法和手写实现一样高效优雅. 对于String?
类型, Parcel.writeString()
已经能够正确处理空的情况, 所以这里不需要增加整形标记.
任意类型的可空属性都能够正确高效地转换.
嵌套Parcelable(Nested Parcelables)
让我们来声明一个包含了Parcelable
属性的Parcelable
类
@Parcelize
class Book(val title: String, val author: Person) : Parcelable
Person
是我们之前定义的类, 它也继承Parcelable
结构, 序列表部分的代码如下:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
this.author.writeToParcel(parcel, 0);
}
有趣的是, 生成的代码直接嵌套调用了writeToParcel()
方法. 这等同于调用Parcel.writeTypedObject()
. 这非常高效, 因为对比Parcel.writeParcelable()
, 它不会序列化嵌套类的类名. 这个类名正常来说是在反序列化时, 通过反射查找这个类名对应类下的CREATOR
属性, 以此来创建正确类型.
但在这个例子中, CREATOR
属性是已知且唯一的, 因为Person
类是final
的(注: 在Kotlin中, 类默认是final
的). 所以反序列化的代码如下:
@NotNull
public final Object createFromParcel(@NotNull Parcel in) {
return new Book(in.readString(), (Person)Person.CREATOR.createFromParcel(in));
}
对于不是final
的类和接口, 插件生成的代码会使用Parcel.writeParcelable()
方法.
在Kotlin中, 类默认是
final
的, 这在大多数情况下可以优化嵌套序列化的实现. 如果你声明的Kotlin类的属性中使用了Java的Parcelable
类, 应该尽量把这个Java类声明为final
, 来触发上述优化.
枚举类
插件通过特殊的方式系列化enum
类.
enum class State {
ON, OFF
}
@Parcelize
class PowerSwitch(var state: State) : Parcelable
序列化代码如下:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.state.name());
}
枚举值的名称当作String
序列化, 在反序列时使用Enum.valueOf(Class enumType, String name)
把名称转换回枚举值. 这绝对比writeSerializable()
更高效优雅, 在Android下, 处于性能考虑, 我们应该尽量避免使用Serializable
. 在底层, Enum
对名称做了缓存, 这样可以快速通过名称来获取对应的枚举值.
另一个高效序列化枚举值的方式是保存它的
ordinal()
值, 不过缺点是每次反序列化时都会调用EnumType.values()
产生一个克隆数组. 总的来说, 插件生成的代码在性能的角度看表现不错.
Bonus feature:让枚举类实现Parcelable
实际上, 你可以直接使用@Parcelize
注解实现了Parcelable
接口的枚举类.
@Parcelize
enum class State : Parcelable {
ON, OFF
}
和普通类序列化属性不同, 枚举类会序列化它的枚举值名称, 在反序列化时通过名称在内存中获取相应的值(这是因为枚举值都是单例).
当你想把枚举值放进Bundle
中(例如用在Intent
或者Fragment
的参数中)时非常方便, 而不需要使用效率更低的Bundle.putSerializable()
方法, 也不需要自己额外编写序列化代码. 现在你可以仅仅这些写:
val args = Bundle(1).apply {
putParcelable("state", state)
}
你甚至可以使用core-ktx
提供的bundleOf()
方法进一步简化, 因为这个方法会自动把参数当作Parcelable
, 在这个方法内部, Parcelable
比Serializable
优先级更高.
val args = bundleOf("state" to state)
集合
@Parcelize
默认支持很多集合:
- 所有数组类型(除了
ShortArray
) -
List
,Set
,Map
接口(各自对应ArrayList
,LinkedHashSet
,LinkedHashMap
) -
ArrayList
,LinkedList
,SortedSet
,NavigableSet
,HashSet
,LinkedHashSet
,TreeSet
,SortedMap
,NavigableMap
,HashMap
,LinkedHashMap
,TreeMap
,ConcurrentHashMap
- Android特有的集合:
SparseArray
,SparseIntArray
,SparseLongArray
,SparseBooleanArray
.
对于类型: ByteArray
, IntArray
, CharArray
, LongArray
, FloatArray
, DoubleArray
, BooleanArray
, Array<String>
, List<String>
, Array<Binder>
, 生成的代码会简单地调用对应的Parcel
方法. 对于其他数组和集合类型, 则有所不同.
以下是包含了集合的实现了Parcelable
接口的类:
@Parcelize
class Library(val books: List<Book>) : Parcelable
生成的代码没有使用Parcel.writeTypedList()
方法, 而是直接在writeToParcel()
中内置处理列表的逻辑:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
List var10002 = this.books;
parcel.writeInt(var10002.size());
Iterator var10000 = var10002.iterator();
while(var10000.hasNext()) {
((Book)var10000.next()).writeToParcel(parcel, 0);
}
}
在反序列化代码中也类似. 这种方式有两个好处:
- 相比
Parcel
提供的集合相关接口, 这种方式让Parcelize
注解不需要增加额外的方法就可以支持更多的集合类型. - 相比
Parcel
提供的集合相关接口, 插件在生成序列化代码的时候, 会根据不同的类型选择最佳的序列化方式, 以此提高效率. 例如, 使用Parcel.writeSparseArray()
序列化SparseArray<Book>
(此处Book
是final
类), 内部会使用Parcel.writeValue()
序列化Book
类型. 这个方法相对低效, 因为它会在每个值前增加一个整形来描述值的类型(这是多余的, 因为在这个例子中, 所有值的类型都是一样的). 如果使用Parcel.writeParcelable()
, 就如上文提到的, 它同样比嵌套调用Book.writeToParcel()
低效. 作为对比, 插件会使用Book.writeToParcel()
序列化集合中的每个值.
注意, Android框架中的ArrayMap
, ArraySet
, LongSparseArray
, Jetpack库中的ArrayMap
, ArraySet
, LongSparseArray
, SimpleArrayMap
, SparseArrayCompat
默认情况下都不支持. 考虑使用SparseArray
而不是SparseArrayCompat
, 或者在必要时, 自己写序列化方法来序列化这些集合.
最后, ArrayDeque
, EnumMap
和EnumSet
能够使用, 但是是因为它们实现了Serializable
接口, 所以生成的代码会使用低效的Parcel.writeSerializable()
方法序列化它们. 所以强烈建议使用通用的Set<enum>
或者自己编写序列化器, 这个下一节我们会讲到.
当你在
Parcelable
对象中使用集合的时候, 务必确保它们可以被插件支持, 或者提供一个自定义序列化器, 尤其是当所使用的集合实现了Serializable
时
自定义序列化器
Parcelize插件同样支持Parcel
提供API的剩余类型: CharSequence
, Exception
(which only supports a few types of Exceptions), Size
, SizeF
, Bundle
, IBinder
, IInterface
and FileDescriptor
.
对于其他的所有类型, 如果该类型实现了Serializable
接口, 那么插件默认会使用Parcel.writeSerializable()
方法(再次提醒, 这个方法性能很差, 同时会产生一大堆数据, 必须避免使用). 如果该类型没有实现Serializable
接口, 会有Warning提示, 同时会使用Parcel.writeValue()
方法, 这个方法在运行时会失败.
为了避免上述情况, 你应该使用一个继承Parceler
接口的object
来提供指定类型的自定义序列化器实现. 下面是一个针对Date
类型的例子:
object DateParceler : Parceler<Date> {
override fun create(parcel: Parcel) = Date(parcel.readLong())
override fun Date.write(parcel: Parcel, flags: Int)
= parcel.writeLong(time)
}
然后, 为了让插件使用这个Parceler
实现去序列化和反序列化Date
类型, 你需要在你的Parcelable
类中增加额外的注解. 你可以使用@WriteWith<ParcelerType>
注解某个属性, 或者使用@TypeParceler<PropertyType, ParcelerType>
注解整个类.
- 注解整个类可以把所有注解放在一起, 同时避免重复, 尤其是当你有多个相同类型的属性想要使用自定义序列化器的时候.
@Parcelize
@TypeParceler<Date, DateParceler>
class Session(val title: String,
val startTime: Date,
val endTime: Date): Parcelable
- 注解具体的属性可以避免歧义. 如果注解和类型不匹配, IDE会提示Warning(但仍可编译)
@Parcelize
class Session(
val title: String,
val startTime: @WriteWith<DateParceler> Date,
val endTime: @WriteWith<DateParceler> Date
) : Parcelable
我们来看看生成的代码, 看看自定义的Parceler
是如何替换Date
类型默认使用的writeSerializable()
的:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
DateParceler.INSTANCE.write(this.startTime, parcel, flags);
DateParceler.INSTANCE.write(this.endTime, parcel, flags);
}
处理可空的自定义类型
尽管Parcelize支持序列化内建类型的可空形式, 但是它并不会自动支持自定义类型的可空形式, 即使我们提供了这种类型的非空形式的Parceler
实现. 这意味着, 如果我们为Date
提供了Parceler
实现, 它并不会自动支持Date?
类型, 除非你为Date?
提供另一个实现.
我们可以通过把上述例子中的endTime
声明为Date?
来验证这点...
@Parcelize
@TypeParceler<Date, DateParceler>
class Session(val title: String,
val startTime: Date,
val endTime: Date?): Parcelable
...然后再看看生成的代码:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
DateParceler.INSTANCE.write(this.startTime, parcel, flags);
parcel.writeSerializable(this.endTime);
}
确实和推测一样, DateParceler
并不会应用在endTime
属性上. 为了修复这点, 我们需要添加另外一个针对Date?
类型的@TypeParceler
(或者@WriteWith
)注解.
幸运的是, 我们不需要为了同一类型的可空和非空形式提供两个独立的Parceler
实现: 我们可以使用可空形式的实现来处理非空的形式.
下面是Parceler
实现的例子, 可以用来处理可空和非空形式的Date
, BigInteger
和BigDecimal
类型.
package be.digitalia.sample
import android.os.Parcel
import kotlinx.android.parcel.Parceler
import java.math.BigDecimal
import java.math.BigInteger
import java.util.*
inline fun <T> Parcel.readNullable(reader: () -> T) =
if (readInt() != 0) reader() else null
inline fun <T> Parcel.writeNullable(value: T?, writer: T.() -> Unit) {
if (value != null) {
writeInt(1)
value.writer()
} else {
writeInt(0)
}
}
object DateParceler : Parceler<Date?> {
override fun create(parcel: Parcel) = parcel.readNullable { Date(parcel.readLong()) }
override fun Date?.write(parcel: Parcel, flags: Int) = parcel.writeNullable(this) { parcel.writeLong(time) }
}
object BigIntegerParceler : Parceler<BigInteger?> {
override fun create(parcel: Parcel) = parcel.readNullable { BigInteger(parcel.createByteArray()) }
override fun BigInteger?.write(parcel: Parcel, flags: Int) = parcel.writeNullable(this) {
parcel.writeByteArray(toByteArray())
}
}
object BigDecimalParceler : Parceler<BigDecimal?> {
override fun create(parcel: Parcel) =
parcel.readNullable { BigDecimal(BigInteger(parcel.createByteArray()), parcel.readInt()) }
override fun BigDecimal?.write(parcel: Parcel, flags: Int) = parcel.writeNullable(this) {
parcel.writeByteArray(unscaledValue().toByteArray())
parcel.writeInt(scale())
}
}
当创建自定义
Parceler
实现时, 优先声明可空类型, 以便可以使用它同时处理类型的可空形式和非空形式.
回到我们的例子, 现在我们可以使用修改后的DateParceler
来处理可空和非空的Date
类型:
@Parcelize
@TypeParceler<Date, DateParceler>
@TypeParceler<Date?, DateParceler>
class Session(val title: String,
val startTime: Date,
val endTime: Date?): Parcelable
我们可以查看生成的代码来确认一下:
public void writeToParcel(@NotNull Parcel parcel, int flags) {
parcel.writeString(this.title);
DateParceler.INSTANCE.write(this.startTime, parcel, flags);
DateParceler.INSTANCE.write(this.endTime, parcel, flags);
}
为所有属性添加针对正确类型形式(可空或非空)的自定义序列化器, 要求在
Parcelable
类的主构造器中增加注解. 针对非空类型的注解会忽略可空类型, 反之亦然.
处理泛型
自定义Parceler
的其中一个限制在于它需要是object
单例. 这意味着它们不能通过构造器接收额外参数: 所以每个不同的序列化算法都需要独立声明一个object
.
但是你创建可复用的工具方法或者父类来让这些object
实例使用, 以此来避免重复代码.
接下来我们用Parceler
处理EnumSet<E>
的一个例子来解释下怎么做. 在反序列化时, 我们需要知道明确的Enum
类才能实例化EnumSet
和把具体的值放进集合中. 一般我们会想到在反序列化时把这个类当作参数传进去, 但是我们不能直接这样做. 不过, 我们可以把这个类作为构造器的一个参数传递到一个可复用的Parceler
实现中, 如下:
open class EnumSetParceler<E : Enum<E>>(private val elementType: Class<E>) : Parceler<EnumSet<E>> {
private val values = elementType.enumConstants
override fun create(parcel: Parcel): EnumSet<E> {
val set = EnumSet.noneOf(elementType)
for (i in 0 until parcel.readInt()) {
set.add(values[parcel.readInt()])
}
return set
}
override fun EnumSet<E>.write(parcel: Parcel, flags: Int) {
parcel.writeInt(size)
for (value in this) {
parcel.writeInt(value.ordinal)
}
}
}
这个open class
可以用作多个object
实例的父类, 此时, 需要使用Parcelize序列化的具体的EnumSet<E>
可以这样写:
object StateEnumSetParceler
: EnumSetParceler<State>(State::class.java)
object CategoryEnumSetParceler
: EnumSetParceler<Category>(Category::class.java)
这样, 我们就可以通过object
来系列化相同类型的不同变种, 而不需要写重复的代码.
从Kotlin代码中访问生成的CREATOR
变量
当你编写自定义Parcel
反序列代码时, 例如实现一个Parceler
实例, 有时你会需要用到Parcelable
的CREATOR
静态变量. 当你想不依赖反射来创建常见类型的实例时, 它非常有用. 同时Parcel.creteTypedArrayList()
和Parcel.readTypedObject()
中也用到它.
不过遗憾的是, 你可以注意到, 由Parcelize生成的CREATOR
静态变量不能被同模块的Kotlin代码访问. 这是因为这些变量是在较后面的Kotlin编译阶段才被添加到被@Parcelize
注释的类中的, 这意味着它们在刚开始编译阶段并不存在, 所以编译器不能够解析到它们的引用. 这个问题在2017年已知了, 但目前看起来并没有修复计划, 这对一个花了数年时间才被认为"production-ready"的特性来说有点出乎意料. 不管怎样, 还有几个办法可以解决这个问题:
- 自己手写你需要使用到
CREATOR
变量的Parcelable
实现, 而不是使用自动生成. - 对于自定义集合类型的情况, 使用额外受支持的标准集合类型.
- 使用稍微低效的, 利用反射访问
CREATOR
的Parcel
方法:Parcel.readArrayList()
代替Parcel.createTypedArrayList()
,Parcel.readParcelable()
代替Parcel.readTypedObject()
. 由于在序列化时, 每个实例前都会添加类名称, 所以这些方法序列化后的数据格式相对会复杂点. 用到的CREATOR
字段在第一次反射后会被Parcel
实例缓存起来, 所以这对性能的影响较小. - 利用Java交叉编译(Java cross-compilation): 生成的
CREATOR
字段实际上可以被Java类访问, 反过来可以再暴露回Kotlin代码. 相比单纯的Kotlin模块, 在一个模块中混合Kotlin和Java会令到编译变慢, 另外创建一个中间Java类绝对是一个丑陋的方法. 但是这方法让你可以仍利用生成的代码来写出最高效的自定义序列化器.
处理类继承
你可以使用@Parcelize
注释一个open
类. 但是除非你重复序列化父类的所有属性, 否则你不能够通过简单注释子类来序列化子类. 让我们来看下下面的例子来更好地理解这个问题:
@Parcelize
open class Book(val title: String, val author: Person) : Parcelable
@Parcelize
class ElectronicBook(private val _title: String,
private val _author: Person,
val downloadSize: Long) : Book(_title, _author)
Parcelize要求所有属性都在主构造器中声明成val
或者var
, 为了遵循这个限制, 我们为父类已经有的title
和author
重复声明了两个不必要的属性. 这是一种解决办法, 但是远称不上优雅, 更不要提重复字段浪费的内存了.
如果你需要创建一个子类继承一个
open
的Parcelable
类, 你要么手写这个子类的Parcelable实现, 要么重构这个父类, 把它变成abstract
或则sealed
.
在上面的例子中, Book
类可以转换成abstract
或者sealed
类(或者接口, 再创建一个新的子类
PaperBook`.
Parcelable带有普通字段的abstract
和sealed
类
@Parcelize
并不能用来注释abstract
或者sealed
类: 相反, 所有继承它们的子类都要被注释.
有一些需要注意地方:
- 我们不可能从子类中复用父类的序列化代码, 这意味着生成的代码在每一个子类中都会重复序列化父类声明的字段.
- 所有
abstract
和sealed
类需要被序列化的属性都必须被声明为abstract
, 以此避免在子类中重复定义它们.
接下来看下关于Parcelable
一个abstract
类的例子.
abstract class Vehicle(val wheels: Int) : Parcelable {
abstract val model: String
abstract var speed: Float
}
@Parcelize
class Car(override val model: String, override var speed: Float)
: Vehicle(4)
@Parcelize
class Bicycle(override val model: String, override var speed: Float)
: Vehicle(2)
-
wheels
属性不需要被序列化, 它的值会在子类的构造器中直接赋值 -
model
和speed
属性在每一个实例中都需要被序列化, 所以它们在Vehicle
类中声明为abstract
. 所以, 它们会在子类的构造器中被覆盖, 同时被Parcelize处理. - 额外的需要被序列化的属性同样可以添加到子类的主构造器中.
继承父类的object
另一个Parcelize还没被提到的有趣特性中, 它支持注解object
. 咋一看好像支持序列化一个单例没什么用, 但实际上, 当object
有父类, 同时它表示了父类的其中一个可能值时, 这个特性会很有用.
尤其当实现一个Parcelable的sealed
类, 它的值包含了一些object
时. 一个典型的例子如下:
sealed class Status : Parcelable
@Parcelize
object Success : Status()
@Parcelize
class Error(val message: String) : Status()
我们可以看下 bytecode inspector来确认下针对object
生成的反序列化代码, 确实是返回了已存在的单例, 而不是新的实例:
@NotNull
public final Object createFromParcel(@NotNull Parcel in) {
return in.readInt() != 0 ? Success.INSTANCE : null;
}
忽略属性
序列化时需要忽略一些用来储存临时状态的属性. 在经典Java中, 封装类会实现Serializable
接口, 同时需要被忽略的字段会使用@Transient
注释.
而在Android中, 我们使用Parcelable
接口, 但是Parcelize并不支持@Transient
注解. 相对地, 把属性放在类主体(body)中定义而不是主构造器就可以不序列化这些属性:
@Parcelize
class Book(val title: String, val totalPages: Int) : Parcelable {
@IgnoredOnParcel
var readPages: Int = 0
}
在这个例子中, readPages
会被跳过. 除非你使用@IgnoredOnParcel
明确表示忽略这个字段, 否则IDE默认会高亮警告提示.
那么我们是否还可以像其他属性一样在构造器中传递这个被忽略的属性? 答案是可以的: 通过使用一个调用主构造器的次构造器(secondary constructor).
@Parcelize
class Book private constructor(val title: String,
val totalPages: Int) : Parcelable {
@IgnoredOnParcel
var readPages: Int = 0
constructor(title: String, totalPages: Int, readPages: Int)
: this(title, totalPages) {
this.readPages = readPages
}
}
在上面的例子中, 主构造器被声明为private
, 因此它只能被次构造器和生成ParcelableCREATOR
调用. 而外部类则必须使用有3个参数的次构造器来创建实例.
类似地, 同样可以使用类似的方式来实现相反的目的: 通过公有的次构造器来避免传入需要序列化的私有属性.
@Parcelize
class ClickCounter private constructor(private var count: Int)
: Parcelable {
constructor() : this(0)
fun click() {
count++
}
val currentValue
get() = count
}
在这个例子中, count
是一个记录内部状态的变量, 当公有的构造器被调用时会被初始化为0, 在反序列化时则会恢复成当前值.
通过
public
的次构造器配合private
的主构造器, 可以自由控制哪些属性会在公有构造器中被初始化, 和哪些属性会被序列化.
说完收工. 这个功能基本覆盖了所有常见的情况, 除了非常罕见的情况, Android开发者再也不需要手写Parcelable
序列化代码了. 如果你有其他关于Parcelize
的提示和技巧, 请在评论区尽情评论说明.
感谢你阅读这篇特别长的文章~~~
(翻译了两个周末才弄完....水平有限, 欢迎指正, 原文作者对细节的考究值得学习;D)