美文网首页
从使用到源码—Gson(下)

从使用到源码—Gson(下)

作者: horseLai | 来源:发表于2018-09-06 00:57 被阅读0次

    引言

    • 在上一篇文章中,我们主要从Gson#fromGson#toJson两个方法着手分析了Gson在解析过程中进行了那些处理,以及走了什么流程,具体欢迎查看从使用到源码—Gson(上).
    • 目标:那么通过之前的分析,我们已经知道Gsonjson的解析最终都会通过TypeAdapter<T>来进行readwrite,但是至此我们对Gson的处理并不了解,而只是了解了它经历怎样的过程,因此在本篇文章中,我们希望能够尽可能的分析、了解到Gsonjson解析的核心,也就是JsonReaderJsonWriter
    • 好了,废话就不多说了,开工开工。

    JsonReader

    一、先用起来再谈原理

    • JsonReader的功能:简单的说就是将Json数据流按字段及其对应的值分解出来,并做好标记,这样调用者就能够轻易地得到Json的字段和值了。我们不妨先通过一个实例来佐证一下,比方说我们通过下面这个ADAPTER来将Json数据转换成Property类,并记录下每个字段的对应的Java数据类型,那么我们可以这样实现一个TypeAdapter
      • 注意两个点JsonReader#peek()case BEGIN_OBJECT:
        private final TypeAdapter<Property> ADAPTER = new TypeAdapter<Property>() {
            @Override
            public void write(JsonWriter out, Property value) throws IOException {
            }
            @Override
            public Property read(JsonReader in) throws IOException {
                in.setLenient(true);
                switch (in.peek()) {  // 注意点一
                    case STRING:
                        return new Property<String>().setType(String.class).setValue(in.nextString());
                    case NUMBER:
                        return new Property<String>().setType(Number.class).setValue(in.nextString());
                    case BOOLEAN:
                        return new Property<Boolean>().setType(Boolean.class).setValue(in.nextBoolean());
                    case NULL:
                        Property result = new Property<Object>().setType(Object.class).setValue(null);
                        in.nextNull();
                        return result;
                    case BEGIN_ARRAY:
                        List<Property> array = new ArrayList<>();
                        in.beginArray();
                        while (in.hasNext()) {
                            array.add(read(in));
                        }
                        in.endArray();
                        return new Property< List<Property>>().setType(List.class).setValue(array);
                    case BEGIN_OBJECT:  // 注意点二
                        ArrayList<Property> obj = new ArrayList<>();
                        in.beginObject();
                        while (in.hasNext()) {    // 是否到了对象末尾
                            String s = in.nextName();   // 读取字段名
                            Property read = read(in);   // 读取字段值
                            obj.add(read.setName(s)) ;
                        }
                        in.endObject();
                        return new Property<ArrayList<Property>>().setType(Object.class).setValue(obj);
                    case END_DOCUMENT:
                    case NAME:
                    case END_OBJECT:
                    case END_ARRAY:
                    default:
                        throw new IllegalArgumentException();
                }
            }
        }; 
        static class Property<T> {
            String name;
            T value;
            Class<?> type;
       }
    
    • 为啥注意这两个点呢?在这段代码中,我们是JsonReader的调用者,站在调用者的角度,这两个点是理解这个功能实现的核心点,并且从中我们可以了解、推测到它的使用内部功能实现,在这个思考过程中,再次留下疑问(强忍不点开源码)。
      • 注意点一in.peek()用于从JsonReader中获取当前位置对应的字段类型,叫做JsonToken,它是个枚举类。
      • 注意点二BEGIN_OBJECT标识一个Json对象的起点,这点显而易见,但是需要理解的是,它在上述解析过程中起到了根基的作用,因为除了类似于['a','b','c'...]这种只有值的数组外,都会有Json对象的存在,因此对它的处理在整个程序中至关重要。比方说下面这串Json数据(来自豆瓣),Json对象可以看做是一个数据块,每个数据块中包含若干个以key<->value形式存在的字段及值,而Json数组就是若干个数据块的集合,显然是绕不开Json对象。
              [ {
                        "alt": "https://movie.douban.com/celebrity/1002667/",
                        "avatars": {
                            "small": "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1501385708.56.jpg",
                            "large": "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1501385708.56.jpg",
                            "medium": "https://img3.doubanio.com/view/celebrity/s_ratio_celebrity/public/p1501385708.56.jpg"
                        },
                        "name": "保罗·路德",
                        "id": "1002667"
                    }, { . . . }
                ] 
      
    • 那么我们再回头理一理这段逻辑,看似很简单的逻辑,却隐含着坑,因为从JsonReader获取数据,必须遵循先读字段名称,后读字段数据的顺序,多读少读、调换顺序都会出错,这点跟它的内部设计有关,具体原因我们稍后分析,这里先打个断点,留下疑问。
    switch (in.peek()) {    
        case STRING:
            return new Property<String>().setType(String.class).setValue(in.nextString());
    // . . . 
      case BEGIN_OBJECT:   
          ArrayList<Property> obj = new ArrayList<>();
          in.beginObject();
          while (in.hasNext()) {    // 是否到了对象末尾
             String s = in.nextName();   // 读取字段名
             Property read = read(in);   // 读取字段值
            obj.add(read.setName(s)) ;
          }
         in.endObject();
         return new Property<ArrayList<Property>>().setType(Object.class).setValue(obj);
    // . . . 
    }
    
    • 如果不遵守原则,大概你会收到以下异常礼包:
    Exception in thread "main" java.lang.IllegalArgumentException
    

    二、该聊聊原理了

    • 在上面使用分析中,我们大概都了解了作为调用者如何使用JsonReader了,并且知道了需要遵守它的什么规则,但是我们依然不知道这家伙到底怎么实现的,用了什么魔法,因此要扒它的衣服,看看这家伙到底是不是男孩。。在此之前不妨带着以下疑问进行:

      • 它是以什么方式分解Json数据的?跟自己当初思考的解决方案有何不同?
      • 为什么必须严格遵守它的规则,不然就坑我?
      • in.peek()?难道用的栈?用栈怎么存储的?为啥不是Map
      • . . . .
    • 在正式开始之前,我们可以留意一下这些标识,这样有助于我们理解

    final class JsonScope { 
        static final int EMPTY_ARRAY = 1;  // 空数组,如:[]
        static final int NONEMPTY_ARRAY = 2;  // 非空数组,如:[a,b]
        static final int EMPTY_OBJECT = 3;     // 如:{}
        static final int DANGLING_NAME = 4;   // 如: 'a':'b' 
        static final int NONEMPTY_OBJECT = 5;   // 如: {'a':'b'}
        static final int EMPTY_DOCUMENT = 6;    
        static final int NONEMPTY_DOCUMENT = 7; 
        static final int CLOSED = 8;  // 遇到了 "];"、"};"、"]};"等
    }
    
    • 纵观全局,可以看到以下几个数组,stack之前看到了,用来存储是比如数组、对象、空对象等标识,而其他几个我们根据变量名可以大概推测到:
      • buffer极有可能用来缓冲Json数据;
      • pathNamespathIndices两者肯定是下标索引相互对应的,并且很有可能pathIndices记录的是pathNames的长度
    public class JsonReader implements Closeable {
      private final char[] buffer = new char[1024];
      private int[] stack = new int[32];
      private int stackSize = 0;
      {
        stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
      }
      private String[] pathNames = new String[32];
      private int[] pathIndices = new int[32];
    // . . . 
    }
    
    • OK, 那咱就正式开始吧。我们先从最显眼的in.peek()着手,扒开外衣发现这家伙确实是个栈,毕竟stack都这么明显了,而从栈中取出的是类似于JsonScope.EMPTY_ARRAY的这种东东,用于标识 Json数据当前读取到的位置上属于什么类型。
      • 对于doPeek中的逻辑,为什么如果是EMPTY_ARRAY又赋值为 stack[stackSize - 1] =NONEMPTY_ARRAY呢?
        • 逻辑准备:++默认peeked=PEEKED_NONE,并且每次在结束一个操作,如endArrayendObject后都会将其初始化为PEEKED_NONE,因此每次进行取值、标识操作时,比如begainArray(),那么执行时必定会执行一次doPeek进行出栈当前位置的数据类型++。
        • 有了上面逻辑准备,我们可以假设一个实际场景,对于数据[{'a':'aa'}, {'b':'bb'}],如果我们要从JsonReader中解读它,该做什么?首先需要手动执行begainArray()告诉JsonReader这是个数组,而在begainArray()会先假设这是个空数组EMPTY_ARRAY,那么在下次取key:value时必定也会doPeek,于是发现标识为空数组,而为了能够读取到数组中的数据,就得接着假设它是个非空数组NONEMPTY_ARRAY,于是接着doPeek,读取数组中的数据,读取完成,即遇到]后就结束数组读取了,并标记peeked=PEEKED_END_ARRAY,此时我们需要手动执行endArray告诉JsonReader我们结束了数组读取,于是又会设置peeked=PEEKED_NONE
        • 综上:你会发现,除了需要我们手动标记的数组、对象入口和出口,中间过程就是个来回假设、假设、验证假设的循环过程,这也就是为什么如果是EMPTY_ARRAY又赋值为 stack[stackSize - 1] =NONEMPTY_ARRAY的原因了,这两者都是假设,而验证假设是从if (peekStack == JsonScope.NONEMPTY_ARRAY)开始的。对于解析Json对象类型的话,也是这个逻辑,因此就不再单独拿出来分析了。
    public JsonToken peek() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
          p = doPeek();
        }
        // . . . 
    } 
    
    int doPeek() throws IOException {
        int peekStack = stack[stackSize - 1];
        if (peekStack == JsonScope.EMPTY_ARRAY) { // 空数组?
          stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;  // 假设它是非空数组,尝试读取看看有没有数据
        } else if (peekStack == JsonScope.NONEMPTY_ARRAY) { 
          int c = nextNonWhitespace(true);  // Look for a comma before the next element.
          switch (c) {
          case ']':
            return peeked = PEEKED_END_ARRAY; 
            // . . . 
          }
        }else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { // 对象?
          stack[stackSize - 1] = JsonScope.DANGLING_NAME;  // 假设是个类似于'a':'b'的数据
          // Look for a comma before the next element.
          if (peekStack == JsonScope.NONEMPTY_OBJECT) {
            int c = nextNonWhitespace(true);
            switch (c) {
            case '}':
              return peeked = PEEKED_END_OBJECT;
              // . . . 
            }
          }
        } else if (peekStack == JsonScope.DANGLING_NAME) { // 是个类似于'a':'b'的数据?
          stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;  // 假设是个非空对象
          // ...
        } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {   // 是个空文档?
          stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; // 假设是个非空文档
          // ...
        }
         // ...
           
        int c = nextNonWhitespace(true);
        switch (c) { 
        case '[':  // 数组起始位置
          return peeked = PEEKED_BEGIN_ARRAY; 
            
            // . . . 
        } 
    }
    
    // 假设遇到的是数组,需要执行这个beginArray()标记这是数组的初始位置
    public void beginArray() throws IOException {
        int p = peeked;
        if (p == PEEKED_NONE) {
          p = doPeek();  // 实际每次都会执行
        }
        if (p == PEEKED_BEGIN_ARRAY) {
          push(JsonScope.EMPTY_ARRAY);  // 假设是空数组
          pathIndices[stackSize - 1] = 0; // 初始化计数器
          peeked = PEEKED_NONE; // 初始化出栈类型,保证下一次doPeek()出栈
        } else {
          throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + locationString());
        }
      }
    
    // 压栈时stackSize才会增加,意味着
    private void push(int newTop) {
        if (stackSize == stack.length) { // 数组扩容. . .  }
        stack[stackSize++] = newTop;
      }
    
    • 结合上面这段源码以及我们的实际场景,实际上我们已经理清了整个解析过程的逻辑,但是感觉还是缺些什么。那么结合上面的beginArray以及下面的几个方法,我们可以观察到stackSize会在push时自增,而在对象和数组结束时自减,stack、pathIndices、pathNames的关系如下图所示。

      stack、pathIndices、pathNames关系图.png
    • 总的来说就是

      • pathIndices记录的是字段值的数量,也可以看作是元素读取到的位置,需要注意的是:
        • 对于数组,数组中的对象本身不会计入数量,只有当读取到key:valuevalue后才会计数;
        • 对于对象,如果类似于key:{}这种有key对应的对象,{}对象也会算作一个value进行计数,而其内部字段元素则依然按照key:value进行计数。
      • pathNames数组中记录的是每个字段的名称,并且在读取到对象的末尾位置时,就会 将这个元素清空掉
    • 综上: 实际上stack一直在复用stack[0]这个元素,它并不会跟我们当初预想的那样保存每一个元素的数据类型,而只记录当前位置的类型,相应的pathIndicespathNames也是如此。至此,就可以解释为什么我们一定要遵循JsonReader的读取顺序了,因为这家伙只保存了当前位置上的数据类型、字段名称等信息,不按顺序读取铁定出错的,并且调换顺序也是不存在的。

    public void endArray() throws IOException {
        // . . .
        if (p == PEEKED_END_ARRAY) {
          stackSize--; // 实际上与push中的stackSize++相对应 
          pathIndices[stackSize - 1]++;  
          peeked = PEEKED_NONE;
        } // . . .
    }
     
    // 读取读取字段名称
    public String nextName() throws IOException {
        // . . . 
        if (p == PEEKED_UNQUOTED_NAME) {
          result = nextUnquotedValue();
        }  // . . .  
        peeked = PEEKED_NONE;
        pathNames[stackSize - 1] = result;  //将字段名称保存到对应于stack栈顶的pathNames中
        return result;
      }
      
    // 读取字段值 
    public String nextString() throws IOException {
         // . . . 
        if (p == PEEKED_UNQUOTED) {
          result = nextUnquotedValue();
        }  else if (p == PEEKED_NUMBER) {
          result = new String(buffer, pos, peekedNumberLength);
          pos += peekedNumberLength;
        }else if // . . .
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++; // 每个字段值读取完成之后都会自增对应于stack栈顶位置的pathIndices值,表示读取完一对key:value键值对
        return result;
      }
    
    public void beginObject() throws IOException {
        // . . .
        if (p == PEEKED_BEGIN_OBJECT) { // 没有使用到pathIndices和pathNames
          push(JsonScope.EMPTY_OBJECT);
          peeked = PEEKED_NONE;
        }// . . .
      } 
      
    public void endObject() throws IOException {
       // . . .
        if (p == PEEKED_END_OBJECT) {
          stackSize--;   //        
          pathNames[stackSize] = null;  // 回收stack[stackSize]对应位置上的空间
          pathIndices[stackSize - 1]++;  
          peeked = PEEKED_NONE;
        } // . . .
      }
      
     public void skipValue() throws IOException { 
        do {     // ... 
          if (p == PEEKED_BEGIN_ARRAY) {
            push(JsonScope.EMPTY_ARRAY); 
          } else if (p == PEEKED_BEGIN_OBJECT) {
            push(JsonScope.EMPTY_OBJECT); 
          } else if (p == PEEKED_END_ARRAY) {
            stackSize--; 
          } else if (p == PEEKED_END_OBJECT) {
            stackSize--; 
          } // ...
          peeked = PEEKED_NONE;
        } while (count != 0); 
        pathIndices[stackSize - 1]++;
        pathNames[stackSize - 1] = "null";
      }
    
    • 至于buffer,作为直接数据缓冲,它的目的是为了方便缓冲、读取即将到来的字符数据,减少使用StringBuilder作为中间对象而直接创建String对象,它的长度需要大于或等于每个token(比如:JsonToken.STRING)对应数据的最大长度。
    • 个人觉得最能体现buffer的上述目的的是fillBuffer(int minimum)方法,粗略的看可知它用于从in数据流往buffer数据中读取数据,但细看的话,这个方法还是比较难理解的(我比较菜~),根据官方注释可以得知,当limit - pos >= minimum时返回true,而当buffer耗尽时返回false
     private boolean fillBuffer(int minimum) throws IOException {
        char[] buffer = this.buffer;
        lineStart -= pos;
        if (limit != pos) {   
          limit -= pos;    
          System.arraycopy(buffer, pos, buffer, 0, limit);   // 这个为什么这样拷贝,没看明白...
        } else {
          limit = 0;
        } 
        pos = 0;
        int total;
        while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {   // 可看作双指针往buffer的 [limit,buffer.length - limit]区域中写数据,注意到它是从数组两边往中间写的
          limit += total;   // 可写区域[limit,buffer.length - limit]在往中间靠拢
          // if this is the first read, consume an optional byte order mark (BOM) if it exists
          if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
            pos++;
            lineStart++;
            minimum++;
          } 
          if (limit >= minimum) {  // 一旦读取到了数据,并且字节数>= minimum 立马返回
            return true;
          }
        }
        return false; 
      } 
    
    • 小结JsonReader作为Gson的核心处理类,设计的很精妙,很灵活,不过也是基于基本的处理思想的优化,通过对它的分析,可以得出以下结论:
      • JsonReader内部是个基于数组(stack)的栈实现,而stack(缓存每个数组、对象(token)的类型)与pathNames(缓存字段名称)和 pathIndices(缓存每个数组、对象中key:value对的数量),三者形成基于数组索引的映射关系,而实际上它们只会缓存一个token的状态,并且重复使用这个位置上的空间,因此当读取到下一个token时,就会覆盖掉上一个token的状态,这也就导致我们使用JsonReader中读取数据时必须遵循先读字段名,后读字段数据的顺序进行,否则就会出错。
      • JsonReader对数据的解析过程实际上是个假设、假设、验证假设的过程,而验证过程会根据Json数据的标记符,也就是诸如{[]}:"等符号进行判断token的起始和终止点,并记录对应的状态,这是一个精致的循环过程,直到所有假设都不成立或者遇到不合法数据时才会终止循环。

    JsonWriter

    • 如果老哥你都研究到这里了,那么JsonWriter就比较好理解了。JsonWriter的作用是将Java实体类的数据转换成Json数据,它和JsonReader的内部数据结构基本一致,只是操作逻辑正好可看作是JsonReader的逆向过程,因此这里将不再贴源码分析(感兴趣的老哥可以自行查看)。
    • 在使用JsonReader时,我们必须遵循先读字段名,后读字段数据的顺序进行,而使用JsonWriter同样需要注意这类问题,只是变成了写操作。 例如,这里我们摘一小段"从使用到源码—Gson(上)"中定制TypeAdapter部分的代码, 可以看出,跟JsonReader一样,我们在写操作时,同样需要通过诸如beginObjectendObject这类方法去标记我们当前写入的是Json对象还是其他什么类型,此时JsonWriter内部会将这个标识压栈或者出栈, 原理跟JsonReader是一致的,只是换了个方向而已。
    static class MyTypeAdapter extends TypeAdapter<Data> {
     @Override
       public void write(JsonWriter out, Data value) throws IOException
       {
           if (value == null) {
               out.nullValue();
               return;
           } 
           out.setLenient(true);
               out.beginObject();  
               out.name("data");  // 必须先写字段名
               out.value(value.data);  // 再写字段值,否则不能正常写入
               out.name("result");
    
               out.beginObject();
               out.name("name");
               out.value(value.getResult().name);
               out.name("age");
               out.value(value.getResult().age);
               out.endObject();
    
               out.endObject();
       } 
     // ...
    }
    
    static class Data {
           private String data;
           private User result;
           // 省略set/get/toString
       } 
       static class User {
           private String name;
           private int age; 
           // 省略set/get/toString
      }
    

    总结

    • 在本篇文章中,我们依然是从一个实际使用着手,比较详细地分析了JsonReader内部原理,从中我们可以了解到Gson如何分解Json数据、用什么方式解析、以及作为调用者应当遵循什么样的操作规则等。当然还有些细致的问题由于自身菜没有分析透彻的,比如buffer的操作过程等,不过这并不影响我们理解JsonReader的解析原理。
    • 而对于JsonWriter,可以看作是JsonReader的逆向操作过程,两者内部结构几乎一致,调用者都需要遵顼一定的原则才能正常解析,不同的是,JsonWriter作为写操作,并不需要像 JsonReader那样去记录字段名称和key:value数,因此显得更为简洁易懂。
    • OK, 这篇文章可以说是拖了非常久才完结出稿,懒啊,得改得改!!

    相关文章

      网友评论

          本文标题:从使用到源码—Gson(下)

          本文链接:https://www.haomeiwen.com/subject/wxwgbftx.html