序列化 & 数据通讯

序列化应用场景:网络传输、文件存储、进程间通讯

Difference between Serialization & Parcelable & Json

  • Serializable
    • 只需要继承 Serializable 接口,实现简单
    • 序列化逻辑通过反射实现,且会产生大量临时对象,性能差
    • 只能用在 Java 上,无法跨语言
    • 向前兼容性不好,实践无法落地(serializationID)
    • 序列化的对象数据会包含类信息,序列化体积不佳
    • 可以自定义序列化和反序列化流程(writeObject、readObject)
  • Parcelable
    • 需要实现序列化和反序列化方法
    • 效率高
    • 主要应用于进程间通讯,不适用于文件序列化和网络传输
  • Json
    • 只包含有单纯的数据信息
    • 文本格式,可读性好,可以跨平台
    • 性能好

Bundle:A mapping from String keys to various Parcelable values

Bundle 具体支持的类型:

  • String
  • primitives
  • Arraylist<String / primitives>
  • Serializable
  • Parcelable

Gson 用法

Gson 提供了 fromJson 和 toJson 两个直接用于解析和生成 Json 字符串的方法,前者实现反序列化,后者实现了序列化

//POJO 类的生成与解析
public class User {
    public String name;
    public int age;
    public String emailAddress;
}

Gson gson = new Gson();
User user = new User("怪盗kidou",24);
String jsonObject = gson.toJson(user); // {"name":"怪盗kidou","age":24}

Gson gson = new Gson();
String jsonString = "{\"name\":\"怪盗kidou\",\"age\":24}";
User user = gson.fromJson(jsonString, User.class);

@SerializedName 注解

@SerializedName(value = "emailAddress", alternate = {"email", "email_address"})
public String emailAddress;

手动序列化和反序列化,对应 JsonReader、JsonWriter

String json = "{\"name\":\"怪盗kidou\",\"age\":\"24\"}";
User user = new User();
JsonReader reader = new JsonReader(new StringReader(json));
reader.beginObject(); // throws IOException
while (reader.hasNext()) {
    String s = reader.nextName();
    switch (s) {
        case "name":
            user.name = reader.nextString();
            break;
        case "age":
            user.age = reader.nextInt(); //自动转换
            break;
        case "email":
            user.email = reader.nextString();
            break;
    }
}
reader.endObject(); // throws IOException

JsonWriter writer = new JsonWriter(new OutputStreamWriter(System.out));
writer.beginObject() // throws IOException
        .name("name").value("怪盗kidou")
        .name("age").value(24)
        .name("email").nullValue() //演示null
        .endObject(); // throws IOException
writer.flush(); // throws IOException

//除了beginObject、endObject还有beginArray和endArray,两者可以相互嵌套,注意配对即可
//beginArray后不可以调用name方法,同样beginObject后在调用value之前必须要调用name方法

使用 GsonBuilder 导出 null 值

Gson在默认情况下是不动导出值null的键的,如:

public class User {
    public String name;
    public int age;
    public String email;
}

Gson gson = new Gson();
User user = new User("怪盗kidou",24);
System.out.println(gson.toJson(user)); //{"name":"怪盗kidou","age":24}

可以看出,email 字段是没有在 json 中出现的,当我们在调试是、需要导出完整的 json 串时或 API 接口中要求没有值必须用Null时,就会比较有用

Gson gson = new GsonBuilder()
        .serializeNulls()
        .create();
User user = new User("怪盗kidou", 24);
System.out.println(gson.toJson(user)); //{"name":"怪盗kidou","age":24,"email":null}

Serializable 字段过滤

  • transient 关键字
  • 自定义 readObject、writeObject

Gson 字段过滤

基于 Expose 注解

@Expose
@Expose(deserialize = true,serialize = true)  //序列化和反序列化都都生效,等价于上一条
@Expose(deserialize = true,serialize = false) //反序列化时生效
@Expose(deserialize = false,serialize = true) //序列化时生效
@Expose(deserialize = false,serialize = false)// 和不写注解一样

Gson gson = new GsonBuilder()
        .excludeFieldsWithoutExposeAnnotation()
        .create();

基于版本

Gson 在对基于版本的字段导出提供了两个注解 @Since 和 @Until,和GsonBuilder.setVersion(Double) 配合使用。@Since 和 @Until都接收一个 Double 值

class SinceUntilSample {
    @Since(4)
    public String since = "since";
    @Until(5)
    public String until = "until";
}

public void sineUtilTest(double version){
    Gson gson = new GsonBuilder().setVersion(version).create();
    System.out.println(gson.toJson(sinceUntilSample));
}

基于访问修饰符

Gson gson = new GsonBuilder()
        .excludeFieldsWithModifiers(Modifier.FINAL, Modifier.STATIC, Modifier.PRIVATE)
        .create();

基于自定义策略

Gson gson = new GsonBuilder()
        .addSerializationExclusionStrategy(new ExclusionStrategy() {
            @Override
            public boolean shouldSkipField(FieldAttributes f) {
                // 这里作判断,决定要不要排除该字段,return true为排除
                if ("finalField".equals(f.getName())) return true; //按字段名排除
                Expose expose = f.getAnnotation(Expose.class); 
                if (expose != null && expose.deserialize() == false) return true; //按注解排除
                return false;
            }
            @Override
            public boolean shouldSkipClass(Class<?> clazz) {
                // 直接排除某个类 ,return true为排除
                return (clazz == int.class || clazz == Integer.class);
            }
        })
        .create();

POJO 与 JSON 的字段映射规则

GsonBuilder.setFieldNamingStrategy 方法用于实现将 POJO 的字段与 JSON 的字段相对应。@SerializedName 注解拥有最高优先级,在加有 @SerializedName 注解的字段上 FieldNamingStrategy 不生效

Gson gson = new GsonBuilder()
        .setFieldNamingStrategy(new FieldNamingStrategy() {
            @Override
            public String translateName(Field f) {
                //实现自己的规则
                return null;
            }
        })
        .create();

TypeToken

解决 Gson 解析时遇到的泛型擦除问题

class Pojo2<T>(val s1: String, val s2: String, val t: T)

class PojoInner(val i: String, val j: String)

class JsonTest {
    private val gson = GsonBuilder().create()
    fun test() {
        val type = object : TypeToken<Pojo2<PojoInner>>() {}.type
        val pojo2 = gson.fromJson<Pojo2<PojoInner>>(
            "{\"s1\":\"1\",\"s2\":\"2\",\"t\":{\"i\":\"3\",\"j\":\"4\"}}",
            type
        )
    }
}

为什么要用 TypeToken 来定义反序列化的类型?

正如上面说的,如果直接把 Pojo2<PojoInner> 的类型传过去,但是因为运行时泛型被擦除了,所以得到的其实是 Pojo2<Object>,那么后面的 Gson 就不知道要转成 Pojo2<PojoInner> 类型了,这时 Gson 会默认转成 LinkedTreeMap 类型

为什么带有大括号?

这个大括号就是精髓所在。大家都知道,在 Java 语法中,在这个语境,{ } 是用来定义匿名类,这个匿名类是继承了 TypeToken 类,它是 TypeToken 的子类

为什么要通过子类来获取泛型的类型?

这是 TypeToken 能够获取到泛型类型的关键,这是一个巧妙的方法。 这个想法是这样子的,既然像 List<String> 这样中的泛型会被擦除掉,那么我用一个子类 SubList extends List<String> 这样的话,在 JVM 内部中会不会把父类泛型的类型给保存下来呢?我这个子类需要继承的父类的泛型都是已经确定了的呀,果然,JVM 是有保存这部分信息的,它是保存在子类的 Class 信息中

class Pojo2<T>(val s1: String, val s2: String, val t: T)

//反编译后的 Pojo2
# annotations
.annotation system Ldalvik/annotation/Signature;
    value = {
        "<T:",
        "Ljava/lang/Object;",
        ">",
        "Ljava/lang/Object;"
    }
.end annotation

class PojoInner(val i: String, val j: String)
//PojoInner 不存在 Ldalvik/annotation/Signature信息

class CustomTypeToken : TypeToken<Pojo2<PojoInner>>()

//反编译后的 CustomTypeToken
.annotation system Ldalvik/annotation/Signature;
    value = {
        "Lcom/google/gson/reflect/TypeToken<",
        "Lcom/example/test/serialization/Pojo2<",
        "Lcom/example/test/serialization/PojoInner;",
        ">;>;"
    }
.end annotation

ProtoBuf

  • 优点
    • 性能好 / 效率高
    • 支持“向后兼容”和“向前兼容”
    • 支持多种编程语言
  • 缺点
    • 二进制格式导致可读性差
    • 缺乏自描述

Proto2 注意事项

  • 必须不可以改变已存在标签的数字
  • 必须不可以增加或删除必须(required)字段
  • 可以删除可选(optional)或重复(repeated)字段
  • 可以添加新的可选或重复字段,但是必须使用新的标签数字,必须是之前的字段所没有用过的

proto3 与 proto2 的区别

  • 字段规则移除了 “required”,并把 “optional” 改名为 “singular”
  • 移除了 default 选项
    • 在 proto2 中,可以使用 default 选项为某一字段指定默认值。在 proto3 中,字段的默认值只能根据字段类型由系统决定。也就是说,默认值全部是约定好的,而不再提供指定默认值的语法
    • 在字段被设置为默认值的时候,该字段不会被序列化。这样可以节省空间,提高效率
    • 但这样就无法区分某字段是根本没赋值,还是赋值了默认值。这在 proto3 中问题不大,但在 proto2 中会有问题
    • 比如,在更新协议的时候使用 default 选项为某个字段指定了一个与原来不同的默认值,旧代码获取到的该字段的值会与新代码不一样

参考文档

Android 最全 Intent 传递数据姿势-腾讯云开发者社区-腾讯云

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

little-sparrow

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值