草庐IT

什么?还在傻傻地手写Parcelable实现?

王可大虾 2023-03-28 原文

缘起

序列化已经是Android司空见惯的东西了,场景太多了。就拿Intent来说吧,extra能放的数据,除了基本类型外,就是序列化的数据了,有两种:

  • Serializable:Java世界自带的序列化工具,大道至简,是一个无方法接口
  • Parcelable:Android的官配序列化工具

这二者在性能、用法乃至适用场景上均有不同,网上的讨论已经很多了,这里不再赘述。

下面来看看官配正品怎么用的。

Android的Parcelable

首先看看官方示例:

public class MyParcelable implements Parcelable {
    private int mData;

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(mData);
    }

    public static final Parcelable.Creator<MyParcelable> CREATOR
            = new Parcelable.Creator<MyParcelable>() {
        public MyParcelable createFromParcel(Parcel in) {
            return new MyParcelable(in);
        }

        public MyParcelable[] newArray(int size) {
            return new MyParcelable[size];
        }
    };

    private MyParcelable(Parcel in) {
        mData = in.readInt();
    }
}

可以总结,实现Parcelable的数据类,有两个要点:

  1. 必须有一个 非空的、静态的且名为"CREATOR" 的对象,该对象实现 Parcelable.Creator 接口
  2. 实现方法 describeContents ,描述内容;
    实现方法 writeToParcel ,将类数据打入parcel内

示例中,实际的数据只有一个简单的整型。

实验:Intent中的Parcelable传递

这里通过一个案例来说明一下Parcelable的使用。

首先,定义一个数据类User,它包含一个String和一个Int:

class User() : Parcelable {

    var name: String? = ""
    var updatedTime: Long = 0L

    constructor(parcel: Parcel) : this() {
        name = parcel.readString()
        updatedTime = parcel.readLong()
    }

    constructor(name: String?, time: Long) : this() {
        this.name = name
        updatedTime = time
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        Log.d("p-test", "write to")
        parcel.writeString(name)
        parcel.writeLong(updatedTime)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<User> {
        override fun createFromParcel(parcel: Parcel): User {
            Log.d("p-test", "createFromParcel")
            return User(parcel)
        }

        override fun newArray(size: Int): Array<User?> {
            return arrayOfNulls(size)
        }
    }

    override fun toString(): String = "$name - [${
        DateFormat.getInstance().format(Date(updatedTime))
    }]"

}

启动方带上User数据:

Log.d("p-test", "navigate to receiver")
context.startActivity(Intent(context, ReceiverActivity::class.java).apply {
    putExtra("user", User("Dale", System.currentTimeMillis())) // 调用Intent.putExtra(String name, @Nullable Parcelable value)
})

接收方读取并显示User数据:

Log.d("p-test", "onCreate")
val desc: User? = intent?.getParcelableExtra("user")
// 省略展示:desc?.toString()

来看看日志:

2022-05-18 11:45:28.280 26148-26148 p-test  com.jacee.example.parcelabletest     D  navigate to receiver
2022-05-18 11:45:28.282 26148-26148 p-test  com.jacee.example.parcelabletest     D  write to
2022-05-18 11:45:28.342 26148-26148 p-test  com.jacee.example.parcelabletest     D  onCreate
2022-05-18 11:45:28.343 26148-26148 p-test  com.jacee.example.parcelabletest     D  createFromParcel

其过程为:

  1. 启动
  2. User类调用writeToParcel,将数据写入Parcel
  3. 接收
  4. CREATOR调用createFromParcel,从Parcel中读取数据,并构造相应的User数据类对象

界面上,User正确展示:

image.png

由此,Parcelable的数据类算是正确实现了。

看起来,虽然没有很难,但是,是真心有点儿烦啊,尤其是相较于Java的Serializable来说。有没有简化之法呢?当然有啊,要知道,现在可是Kotlin时代了!

kotlin-parcelize插件

隆重介绍kotlin-parcelize插件:它提供了一个 Parcelable 的实现生成器。有了此生成器,就不必再写如前的复杂代码了。

怎么使用呢?

首先,需要在gradle里面添加此插件:

plugins {
    id 'kotlin-parcelize'
}

然后,在需要 Parcelable 的数据类上添加 @kotlinx.parcelize.Parcelize 注解就行了。

来吧,改造前面的例子:

import kotlinx.parcelize.Parcelize

@Parcelize
data class User(
    val name: String?,
    val updatedTime: Long
): Parcelable {
    override fun toString(): String = "new: $name - [${
        DateFormat.getInstance().format(Date(updatedTime))
    }]"
}

哇,简化如斯,真能实现?还是来看看上述代码对应的字节码吧:

@Metadata(
   mv = {1, 6, 0},
   k = 1,
   d1 = {"\u0000:\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\t\n\u0002\b\t\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u0000\n\u0002\b\u0003\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\b\u0087\b\u0018\u00002\u00020\u0001B\u0017\u0012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u0012\u0006\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006J\u000b\u0010\u000b\u001a\u0004\u0018\u00010\u0003HÆ\u0003J\t\u0010\f\u001a\u00020\u0005HÆ\u0003J\u001f\u0010\r\u001a\u00020\u00002\n\b\u0002\u0010\u0002\u001a\u0004\u0018\u00010\u00032\b\b\u0002\u0010\u0004\u001a\u00020\u0005HÆ\u0001J\t\u0010\u000e\u001a\u00020\u000fHÖ\u0001J\u0013\u0010\u0010\u001a\u00020\u00112\b\u0010\u0012\u001a\u0004\u0018\u00010\u0013HÖ\u0003J\t\u0010\u0014\u001a\u00020\u000fHÖ\u0001J\b\u0010\u0015\u001a\u00020\u0003H\u0016J\u0019\u0010\u0016\u001a\u00020\u00172\u0006\u0010\u0018\u001a\u00020\u00192\u0006\u0010\u001a\u001a\u00020\u000fHÖ\u0001R\u0013\u0010\u0002\u001a\u0004\u0018\u00010\u0003¢\u0006\b\n\u0000\u001a\u0004\b\u0007\u0010\bR\u0011\u0010\u0004\u001a\u00020\u0005¢\u0006\b\n\u0000\u001a\u0004\b\t\u0010\n¨\u0006\u001b"},
   d2 = {"Lcom/jacee/example/parcelabletest/data/User;", "Landroid/os/Parcelable;", "name", "", "updatedTime", "", "(Ljava/lang/String;J)V", "getName", "()Ljava/lang/String;", "getUpdatedTime", "()J", "component1", "component2", "copy", "describeContents", "", "equals", "", "other", "", "hashCode", "toString", "writeToParcel", "", "parcel", "Landroid/os/Parcel;", "flags", "parcelable-test_debug"}
)
@Parcelize
public final class User implements Parcelable {
   @Nullable
   private final String name;
   private final long updatedTime;
   public static final android.os.Parcelable.Creator CREATOR = new User.Creator();

   @NotNull
   public String toString() {
      return "new: " + this.name + " - [" + DateFormat.getInstance().format(new Date(this.updatedTime)) + ']';
   }

   @Nullable
   public final String getName() {
      return this.name;
   }

   public final long getUpdatedTime() {
      return this.updatedTime;
   }

   public User(@Nullable String name, long updatedTime) {
      this.name = name;
      this.updatedTime = updatedTime;
   }

   @Nullable
   public final String component1() {
      return this.name;
   }

   public final long component2() {
      return this.updatedTime;
   }

   @NotNull
   public final User copy(@Nullable String name, long updatedTime) {
      return new User(name, updatedTime);
   }

   // $FF: synthetic method
   public static User copy$default(User var0, String var1, long var2, int var4, Object var5) {
      if ((var4 & 1) != 0) {
         var1 = var0.name;
      }

      if ((var4 & 2) != 0) {
         var2 = var0.updatedTime;
      }

      return var0.copy(var1, var2);
   }

   public int hashCode() {
      String var10000 = this.name;
      return (var10000 != null ? var10000.hashCode() : 0) * 31 + Long.hashCode(this.updatedTime);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof User) {
            User var2 = (User)var1;
            if (Intrinsics.areEqual(this.name, var2.name) && this.updatedTime == var2.updatedTime) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }

   public int describeContents() {
      return 0;
   }

   public void writeToParcel(@NotNull Parcel parcel, int flags) {
      Intrinsics.checkNotNullParameter(parcel, "parcel");
      parcel.writeString(this.name);
      parcel.writeLong(this.updatedTime);
   }

   @Metadata(
      mv = {1, 6, 0},
      k = 3
   )
   public static class Creator implements android.os.Parcelable.Creator {
      @NotNull
      public final User[] newArray(int size) {
         return new User[size];
      }

      // $FF: synthetic method
      // $FF: bridge method
      public Object[] newArray(int var1) {
         return this.newArray(var1);
      }

      @NotNull
      public final User createFromParcel(@NotNull Parcel in) {
         Intrinsics.checkNotNullParameter(in, "in");
         return new User(in.readString(), in.readLong());
      }

      // $FF: synthetic method
      // $FF: bridge method
      public Object createFromParcel(Parcel var1) {
         return this.createFromParcel(var1);
      }
   }
}

嗯,十分眼熟 —— 这不就是 完美且完整地实现了Parcelable 吗?当然是能正确工作的!

2022-05-18 13:13:30.197 27258-27258 p-test   com.jacee.example.parcelabletest     D  navigate to receiver
2022-05-18 13:13:30.237 27258-27258 p-test   com.jacee.example.parcelabletest     D  onCreate
image.png

复杂的序列化逻辑

如果需要添加更复杂的序列化逻辑,就需要额外通过伴随对象实现,该对象需要实现接口 Parceler

interface Parceler<T> {
    /**
     * Writes the [T] instance state to the [parcel].
     */
    fun T.write(parcel: Parcel, flags: Int)

    /**
     * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it.
     */
    fun create(parcel: Parcel): T

    /**
     * Returns a new [Array]<T> with the given array [size].
     */
    fun newArray(size: Int): Array<T> {
        throw NotImplementedError("Generated by Android Extensions automatically")
    }
}

看样子,Parceler 和原生 Parcelable.Creator 十分像啊,不过多了一个 write 函数 —— 其实就是对应了Parcelable.writeToParcel方法。

简单打印点日志模拟所谓的“复杂的序列化逻辑”:

@Parcelize
data class User(
    val name: String?,
    val updatedTime: Long
): Parcelable {
    override fun toString(): String = "new: $name - [${
        DateFormat.getInstance().format(Date(updatedTime))
    }]"

    private companion object : Parceler<User> {
        override fun create(parcel: Parcel): User {
            Log.d("p-test", "new: create")
            return User(parcel.readString(), parcel.readLong())
        }

        override fun User.write(parcel: Parcel, flags: Int) {
            Log.d("p-test", "new: write to")
            parcel.writeString("【${name}】")
            parcel.writeLong(updatedTime)
        }

    }
}

来看看:

2022-05-18 13:24:49.365 29603-29603 p-test  com.jacee.example.parcelabletest     D  navigate to receiver
2022-05-18 13:24:49.366 29603-29603 p-test  com.jacee.example.parcelabletest     D  new: write to
2022-05-18 13:24:49.450 29603-29603 p-test  com.jacee.example.parcelabletest     D  onCreate
2022-05-18 13:24:49.450 29603-29603 p-test  com.jacee.example.parcelabletest     D  new: create

果然调用了,其中,接收方拿到的name,确实就是write函数改造过的(加了“【】”):

image.png

映射序列化

假如数据类不能直接支持序列化,那就可以通过自定义一个Parceler实现映射序列化

怎么理解呢?假如有一个数据类A,是一个普通实现,不支持序列化(或者有其他原因,总之是不支持),但是呢,我们又有需求是将它序列化后使用,这时候就可以实现 Parceler<A> 类,然后用包裹A的类B来实现序列化 —— 即,通过Parceler,将普通的A包裹成了序列化的B

// 目标数据类A
data class User(
    val name: String?,
    val updatedTime: Long
) {
    override fun toString(): String = "new: $name - [${
        DateFormat.getInstance().format(Date(updatedTime))
    }]"
}

// 实现的Parceler<A>
object UserParceler: Parceler<User> {
    override fun create(parcel: Parcel): User {
        Log.d("djx_test", "1 new: create")
        return User(parcel.readString(), parcel.readLong())
    }

    override fun User.write(parcel: Parcel, flags: Int) {
        Log.d("djx_test", "1 new: write to")
        parcel.writeString("【${name}】")
        parcel.writeLong(updatedTime)
    }
}

// 映射类B
@Parcelize
@TypeParceler<User, UserParceler>
class Target(val value: User): Parcelable // 这个类来实现Parcelable

如上就是 A -> B 的序列化映射,同样没问题:

2022-05-18 14:08:26.091 30639-30639 p-test   com.jacee.example.parcelabletest     D  navigate to receiver
2022-05-18 14:08:26.094 30639-30639 p-test   com.jacee.example.parcelabletest     D  1 new: write to
2022-05-18 14:08:26.148 30639-30639 p-test   com.jacee.example.parcelabletest     D  onCreate
2022-05-18 14:08:26.148 30639-30639 p-test   com.jacee.example.parcelabletest     D  1 new: create
image.png

上面的映射类B,还可以这么写:

@Parcelize
class Target(@TypeParceler<User, UserParceler> val value: User): Parcelable

// 或

@Parcelize
class Target(val value: @WriteWith<UserParceler> User): Parcelable

总结

说了这么多,其实总结一下就是:

插件kotlin-parcelize接管了套路化、模版化的工作,帮我们自动生成了序列化的实现,它并没有改变 Parcelable 的实现方式

用它就对了!

有关什么?还在傻傻地手写Parcelable实现?的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  4. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  5. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

    为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

  6. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

    它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

  7. ruby - Infinity 和 NaN 的类型是什么? - 2

    我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

  8. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  9. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  10. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

随机推荐