android string数组转json_高效生成JSON串——jsongen

概述

游戏服务端的很多操作(包括玩家的和非玩家的)需要传给公司中台收集汇总,根据运营的需求分析数据。中台那边要求传过去的数据为 JSON 格式。一开始我们使用 golang 标准库中的encoding/json,发现性能不够理想(因为序列化使用了反射,涉及多次内存分配)。由于数据原始格式都是map[string]interface{},且需要自己一个字段一个字段构造,于是我想可以在构造过程中就计算出最终 JSON 串的长度,那么就只需要一次内存分配了。

使用

下载:

$ go get github.com/darjun/json-gen

导入:

import (  jsongen "github.com/darjun/json-gen")

使用起来还是比较方便的:

m := jsongen.NewMap()m.PutUint("key1", 123)m.PutInt("key2", -456)m.PutUintArray("key3", []uint64{78, 90})data := m.Serialize(nil)

data即为最终序列化完成的 JSON 串。当然,类型可以任意嵌套。代码参见github。

github上有 Benchmark,是标准 JSON 库的性能的 10 倍!

LibraryTime/op(ns)B/opallocs/op
encoding/json222096673127
darjun/json-gen330011521

实现

首先定义一个接口Value,所有可以序列化为 JSON 的值都实现这个接口:

type Value interface {  Serialize(buf []byte) []byte  Size() int}
  • Serialize可以传入一个分配好的内存,该方法会将值序列化后的 JSON 串追加到buf后面。

  • Size返回该值最终在 JSON 串中占用的字节数。

分类

我将可序列化为 JSON 串的值分为了 4 类:

  • QuotedValue:在最终的串中需要用"包裹起来的值,例如 golang 中的字符串。

  • UnquotedValue:在最终的串中不需要用"包裹起来的值,例如uint/int/bool/float32等。

  • Array:对应 JSON 中的数组。

  • Map:对应 JSON 中的映射。

目前这 4 种类型已经可以满足我的需求了,后续扩展也很方便,只需要实现Value接口即可。下面根据Value的两个接口讨论这 4 种类型的实现。

QuotedValue

底层基于string类型定义QuotedValue

type QuotedValue string

由于QuotedValue最终在 JSON 串中会有 2 个",故其大小为:长度 + 2。我们来看SerializeSize方法的实现:

func (q QuotedValue) Serialize(buf []byte) []byte {  buf = append(buf, '"')  buf = append(buf, []byte(q)...)  return append(buf, '"')}func (q QuotedValue) Size() int {  return len(q) + 2}

UnquotedValue

同样基于string类型定义UnquotedValue

type UnquotedValue string

QuotedValue不同的是,UnquotedValue不需要"包裹,SerializeSize方法的实现可以参见上面,比较简单!

Array

Array表示一个 JSON 的数组。因为 JSON 数组可以包含任意类型的数据,我们可以基于[]Value为底层类型定义Array

type Array []Value

这样Array在最终 JSON 串中占用的字节包括所有元素大小、元素之间的,和数组前后的[]Size方法实现如下:

func (a Array) Size() int {  size := 0  for _, e := range a {    // 递归求元素的大小    size += e.Size()  }    // for []  size += 2  if len(a) > 1 {    // for ,    size += len(a) - 1  }    return size}

Serialize方法递归调用元素的Serialize方法,在元素之间添加,,整个数组用[]包裹:

func (a Array) Serialize(buf []byte) []byte {  // 如果未传入分配好的空间,根据 Size 分配空间  if len(buf) == 0 {    buf = make([]byte, 0, a.Size())  }  buf = append(buf, '[')  count := len(a)  for i, e := range a {    buf = e.Serialize(buf)        if i != count-1 {            // 除了最后一个元素,每个元素后添加,      buf = append(buf, ',')    }  }     return append(buf, ']')}

为了方便操作数组,我给数组添加很多方法,常用的基本类型和Array/Map都有对应的操作方法。操作方法命名为AppendTypeAppendTypeArray(其中Typeuint/int/bool/float/Array/Map等类型名)。

除了string/Array/Map,其它的基本类型都使用strconv转为字符串,且强制转换为UnquotedValue,因为它不需要"包裹:

func (a *Array) AppendUint(u uint64) {  value := strconv.FormatUint(u, 10)  *a = append(*a, UnquotedValue(value))}func (a *Array) AppendString(value string) {  *a = append(*a, QuotedValue(escapeString(value)))}func (a *Array) AppendUintArray(u []uint64) {  value := make([]Value, 0, len(u))   for _, v := range u {    value = append(value, UnquotedValue(strconv.FormatUint(v, 10)))  }  *a = append(*a, Array(value))}func (a *Array) AppendStringArray(s []string) {  value := make([]Value, 0, len(s))   for _, v := range s {    value = append(value, QuotedValue(escapeString(v)))  }  *a = append(*a, Array(value))}

这里有点需要注意,由于Append*方法会修改Array(即切片),所以接收者需要使用指针!

Map

实现Map时,有两种选择。第一种定义为map[string]Value,这样结构简单,但是由于map遍历的随机性会导致同一个Map生成的 JSON 串不一样。最终我选择了第二种方案,即键和值分开存放,这样可以保证在最终的 JSON 串中,键的顺序与插入的顺序相同:

type Map struct {  keys []string  values []Value}

Map的大小包含多个部分:

  • 键和值的大小。

  • 前后需要{}包裹。

  • 每个键需要用"包裹。

  • 键和值之间需要有一个:

  • 每个键值对之间需要用,分隔。

搞清楚了这些组成部分,Size方法的实现就简单了:

func (m Map) Size() int {  size := 0  for i, key := range m.keys {    // +2 for ", +1 for :    size += len(key) + 2 + 1    size += m.values[i].Size()  }    // +2 for {}  size += 2  if len(m.keys) > 1 {    // for ,    size += len(m.keys) - 1  }    return size}

Serialize将多个键值对组装:

func (m Map) Serialize(buf []byte) []byte {  if len(buf) == 0 {    buf = make([]byte, 0, m.Size())  }  buf = append(buf, '{')  count := len(m.keys)  for i, key := range m.keys {    buf = append(buf, '"')    buf = append(buf, []byte(key)...)    buf = append(buf, '"')    buf = append(buf, ':')    buf = m.values[i].Serialize(buf)        if i != count-1 {      buf = append(buf, ',')    }  }    return append(buf, '}')}

Array类似,为了方便操作Map,我给Map添加了很多方法,常见的基本数据类型和Array/Map都有对应的操作方法。操作方法命名为PutTypePutTypeArray(其中Typeuint/int/bool/float/Array/Map等)。具体代码如下:

func (m *Map) put(key string, value Value) {  m.keys = append(m.keys, key)  m.values = append(m.values, value)}func (m *Map) PutUint(key string, u uint64) {  value := strconv.FormatUint(u, 10)  m.put(key, UnquotedValue(value))}func (m *Map) PutUintArray(key string, u []uint64) {  value := make([]Value, 0, len(u))  for _, v := range u {    value = append(value, UnquotedValue(strconv.FormatUint(v, 10)))  }  m.put(key, Array(value))}

结语

我根据自身需求实现了一个生成 JSON 串的库,性能大为提升,尽管还不完善,但是后续扩展也非常简单。希望能给有相同需求的朋友带来启发。

推荐阅读

  • 如何控制Go编码JSON数据时的行为


喜欢本文的朋友,欢迎关注“Go语言中文网”:

8e9098fe35d7d0dce3643bab51982488.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值