翻译: Kotlin Android Extensions中的Parcelize详解

原文 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, 私有或者公开都可以.

这个特性的主要限制在于, 它不允许注解abstractsealed类. 想让它们实现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, 在这个方法内部, ParcelableSerializable优先级更高.

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>(此处Bookfinal类), 内部会使用Parcel.writeValue()序列化Book类型. 这个方法相对低效, 因为它会在每个值前增加一个整形来描述值的类型(这是多余的, 因为在这个例子中, 所有值的类型都是一样的). 如果使用Parcel.writeParcelable(), 就如上文提到的, 它同样比嵌套调用Book.writeToParcel()低效. 作为对比, 插件会使用Book.writeToParcel()序列化集合中的每个值.

注意, Android框架中的ArrayMap, ArraySet, LongSparseArray, Jetpack库中的ArrayMap, ArraySet, LongSparseArray, SimpleArrayMap, SparseArrayCompat默认情况下都不支持. 考虑使用SparseArray而不是SparseArrayCompat, 或者在必要时, 自己写序列化方法来序列化这些集合.

最后, ArrayDeque, EnumMapEnumSet能够使用, 但是是因为它们实现了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, BigIntegerBigDecimal类型.

Parcelers.kt

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实例, 有时你会需要用到ParcelableCREATOR静态变量. 当你想不依赖反射来创建常见类型的实例时, 它非常有用. 同时Parcel.creteTypedArrayList()Parcel.readTypedObject()中也用到它.

不过遗憾的是, 你可以注意到, 由Parcelize生成的CREATOR静态变量不能被同模块的Kotlin代码访问. 这是因为这些变量是在较后面的Kotlin编译阶段才被添加到被@Parcelize注释的类中的, 这意味着它们在刚开始编译阶段并不存在, 所以编译器不能够解析到它们的引用. 这个问题在2017年已知了, 但目前看起来并没有修复计划, 这对一个花了数年时间才被认为"production-ready"的特性来说有点出乎意料. 不管怎样, 还有几个办法可以解决这个问题:

  • 自己手写你需要使用到CREATOR变量的Parcelable实现, 而不是使用自动生成.
  • 对于自定义集合类型的情况, 使用额外受支持的标准集合类型.
  • 使用稍微低效的, 利用反射访问CREATORParcel方法: 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, 为了遵循这个限制, 我们为父类已经有的titleauthor重复声明了两个不必要的属性. 这是一种解决办法, 但是远称不上优雅, 更不要提重复字段浪费的内存了.

如果你需要创建一个子类继承一个openParcelable类, 你要么手写这个子类的Parcelable实现, 要么重构这个父类, 把它变成abstract或则sealed.

在上面的例子中, Book类可以转换成abstract或者sealed类(或者接口, 再创建一个新的子类PaperBook`.

Parcelable带有普通字段的abstractsealed

@Parcelize并不能用来注释abstract或者sealed类: 相反, 所有继承它们的子类都要被注释.

有一些需要注意地方:

  • 我们不可能从子类中复用父类的序列化代码, 这意味着生成的代码在每一个子类中都会重复序列化父类声明的字段.
  • 所有abstractsealed类需要被序列化的属性都必须被声明为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属性不需要被序列化, 它的值会在子类的构造器中直接赋值
  • modelspeed属性在每一个实例中都需要被序列化, 所以它们在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)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容