fastjson 序列化部分源码解析②

在论述完基本概念和总体思路之后,我们来到整个程序最重要的部分-性能优化。之所以会有fastjson这个项目,主要问题是为了解决性能这一块的问题,将序列化工作提高到一个新的高度。我们提到,性能优化主要有两个方面,一个如何将处理后的数据追加到数据储存器,即outWriter中;二是如何保证处理过程中的速度。
    本篇从第一个性能优化方面来进行解析,主要的工作集中在类SerializeWriter上。

    首先,类的声明,继承了Writer类,实现了输出字符的基本功能,并且提供了拼接数据的基本功能。内部使用了一个buf数组和count来进行计数。这个类的实现结果和StringBuilder的工作模式差不多。但我们说为什么不使用StringBuilder,主要是因为StringBuilder没有针对json序列化提出更加有效率的处理方式,而且单就StringBuilder而言,内部是为了实现字符串拼接而生,因为很自然地使用了更加能够读懂的方式进行处理。相比,serializeWriter单处理json序列化数据传输,功能单一,因此在某些方面更加优化一些。
    在类声明中,这里有一个优化措施(笔者最开始未注意到,经作者指出之后才明白)。即是对buf数组的缓存使用,即在一次处理完毕之后,储存的数据容器并不销毁,而是留在当前线程变量中。以便于在当前线程中再次序列化json时使用。源码如下:

Java代码   收藏代码
  1. public SerializeWriter(){  
  2.         buf = bufLocal.get(); // new char[1024];  
  3.         if (buf == null) {  
  4.             buf = new char[1024];  
  5.         } else {  
  6.             bufLocal.set(null);  
  7.         }  
  8.     }  

 

 在初始构造时,会从当前线程变量中取buf数组并设置在对象属性buf中。而在每次序列化完成之后,会通过close方法,将此buf数组再次绑定在线程变量当中,如下所示:

Java代码   收藏代码
  1. /** 
  2.      * Close the stream. This method does not release the buffer, since its contents might still be required. Note: 
  3.      * Invoking this method in this class will have no effect. 
  4.      */  
  5.     public void close() {  
  6.         bufLocal.set(buf);  
  7.     }  

 

当然,buf重新绑定了,肯定计数器count应该置0。这是自然,count是对象属性,每次在新建时,自然会置0。

    在实现过程当中,很多具体的实现是借鉴了StringBuilder的处理模式的,在以下的分析中会说到。

    总体分类
   
    接上篇而言,我们说outWriter主要实现了五个方面的输出内容。
        1,提供writer的基本功能,输出字符,输出字符串
        2,提供对整形和长整形输出的特殊处理
        3,提供对基本类型数组输出的支持
        4,提供对整形+字符的输出支持
        5,提供对字符串+双(单)引号的输出方式
    五个方面主要体现在不同的作用域。第一个提供了最基本的writer功能,以及在输出字符上最基本的功能,即拼接字符数组(不是字符串);第二个针对最常用的数字进行处理;第三个,针对基本类型数组类处理;第四个针对在处理集合/数组时,最后一位的特殊处理,联合了输出数字和字符的双重功能,效率上比两个功能的实现原理上更快一些;第四个,针对字符串的特殊处理(主要是特殊字符处理)以及在json中,字符串的引号处理(即在json中,字符串必须以引号引起来)。

    实现思想

    数据输出最后都变成了拼接字符的功能,即将各种类型的数据转化为字符数组的形式,然后将字符数组拼接到buf数组当中。这中间主要逻辑如下:
        1    对象转化为字符数组
        2    准备装载空间,以容纳数据
        2.1    计数器增加
        2.2    扩容,字符数组扩容
        3    装载数据
        4    计数器计数最新的容量,完成处理
    这里面主要涉及到一个buf数组扩容的概念,其使用的扩容函数expandCapacity其内部实现和StringBuilder中一样。即(当前容量 + 1)* 2,具体可以见相应函数或StringBuilder.ensureCapacityImpl函数。

 

    实现解析

    基本功能
    基本功能有以下几个函数:

Java代码   收藏代码
  1. public void write(int c)  
  2. public void write(char c)  
  3. public void write(char c[], int off, int len)  
  4. public void write(String str, int off, int len)  
  5. public SerializeWriter append(CharSequence csq)  
  6. public SerializeWriter append(CharSequence csq, int start, int end)  
  7. public SerializeWriter append(char c)  

 

     其中第一个函数,可以忽略,可以理解为实现writer中的writ(int)方法,在具体应用时未用到此方法。第2个方法和第7个方法为写单个字符,即往buf数组中写字符。第3,4,5,6,均是写一个字符数组(字符串也可以理解为字符数组)。因此,我们单就字符数组进行分析,源码如下:

Java代码   收藏代码
  1. public void write(char c[], int off, int len) {  
  2.         int newcount = count + len;//计算新计数量  
  3.         //扩容计算  
  4.         System.arraycopy(c, off, buf, count, len);//拼接字符数组  
  5.         count = newcount;//最终计数  
  6.     }  

 

从上注释可以看出,其处理流程和我们所说的标准处理逻辑一致。在处理字符拼接时,尽量使用最快的方法,如使用System.arrayCopy和字符串中的getChars方法。另外几个方法处理逻辑与此方法相同。
    警告:不要在正式应用中对有存在特殊字符的字符串(无特殊字符的字符串除外)使用以上的输出方式,请使用第5组方式进行json输出。对于字符数组的处理在以上处理方式中不会对特殊字符进行处理。如字符串 3\"'4,在使用以上方式输出时,只会输出 3"'4,其中的转义字符在转化为toChar时被删除掉。
    因此,在实际处理中,只有字符数组会使用以上方式进行输出。不要将字符串与字符数组相混合。字符数组不考虑转义问题,而字符串需要考虑转义。

    整形和长整形

    方法如下:

Java代码   收藏代码
  1. public void writeInt(int i)  
  2. public void writeLong(long i)  

 

    这两个方法,按照我们的逻辑,首先需要将整性和长整性转化为字符串(无特殊字符),然后以字符数组的形式输出即可。在进行处理时,主要参考了Integer和Long的toString实现方式和长度计算。首先看一个实现:

Java代码   收藏代码
  1. public void writeInt(int i) throws IOException {  
  2.         if (i == Integer.MIN_VALUE) {//特殊数字处理  
  3.             write("-2147483648");  
  4.             return;  
  5.         }  
  6.    
  7.         int size = (i < 0) ? IOUtils.stringSize(-i) + 1 : IOUtils.stringSize(i);//计算长度 A  
  8.         int newcount = count + size;  
  9.   //扩容计算  
  10.         IOUtils.getChars(i, newcount, buf);//写入buf数组 B  
  11.         count = newcount;//最终定count值  
  12.     }  

 

以上首先看特殊数字的处理,因为int的范围从-2147483648到2147483647,因此对于-2147483648这个特殊数字(不能转化为-号+正数的形式),进行特殊处理。这里调用了write(str)方法,实际上就是调用了在第一部分的public void write(String str, int off, int len),这里是安全的,因为没有特殊字符。
    其次是计算长度,两者都借鉴了jdk中的实现,分别为Integer.stringSize和Long.stringSize,这里就不再叙述。
    再写入buf数组,我们说都是将数字转化为字符数组,再定入buf数组中。这里的实现,即按照这个步骤在进行。这里在IOUtils中,借鉴了Integer.getChars(int i, int index, char[] buf)方法和Long.getChars(long i, int index, char[] buf)方法,这里也不再叙述。

    基本类型数组

Java代码   收藏代码
  1. public void writeBooleanArray(boolean[] array)  
  2. public void writeShortArray(short[] array)  
  3. public void writeByteArray(byte[] array)  
  4. public void writeIntArray(int[] array)  
  5. public void writeIntArray(Integer[] array)  
  6. public void writeLongArray(long[] array)  

 

     数组的形式,主要是将数组的每一部分输出出来,即可。在输出时,需要输出前缀“[”和后缀“]”以及每个数据之间的“,“。按照我们的逻辑,首先还是计算长度,其次是准备空间,再者是写数据,最后是定count值。因此,我们参考一个实现:

Java代码   收藏代码
  1. public void writeIntArray(int[] array) throws IOException {  
  2.         int[] sizeArray = new int[array.length];//性能优化,用于保存每一位数字长度  
  3.         int totalSize = 2;//初始长度,即[]  
  4.         for (int i = 0; i < array.length; ++i) {  
  5.             if (i != 0) {totalSize++;}//追加,长度  
  6.             int val = array[i];  
  7. //针对每一个数字取长度,此处有部分删除。分别针对minValue和普通value运算  
  8.             int size = (val < 0) ? IOUtils.stringSize(-val) + 1 : IOUtils.stringSize(val);  
  9.             sizeArray[i] = size;  
  10.             totalSize += size;  
  11.         }  
  12. //扩容计算  
  13.         buf[count] = '[';//追加起即数组字符  
  14.    
  15.         int currentSize = count + 1;//记录当前位置,以在处理数字时,调用Int的getChars方法  
  16.         for (int i = 0; i < array.length; ++i) {  
  17.             if (i != 0) {buf[currentSize++] = ',';} //追加数字分隔符  
  18.    
  19. //追加当前数字的字符形式,分别针对minValue和普通数字作处理  
  20.             int val = array[i];  
  21.                 currentSize += sizeArray[i];  
  22.                 IOUtils.getChars(val, currentSize, buf);  
  23.         }  
  24.         buf[currentSize] = ']';//追加结尾数组字符  
  25.         count = newcount;//最终count定值  
  26.     }  

 

    此处有关于性能优化的地方,主要有几个地方。首先将minValue和普通数字分开计算,以避免可能出现的问题;在计算长度时,尽量调用前面使用stringToSize方法,此方法最快;在进行字符追加时,利用getChars方法进行处理。
    对于仍有优化的地方,比如对于boolArray,在处理时,又有了特殊优化,主要还是在上面的两点,计算长度时,尽量地快,以及在字符追加时也尽量的快。以下为对于boolean数据的两个优化点:

Java代码   收藏代码
  1. //计算长度,直接取值,不需要进行计算  
  2. if (val) {  
  3.           size = 4// "true".length();  
  4.          } else {}  
  5. //追加字符时,不需要调用默认的字符拼接,直接手动拼接,减少中间计算量  
  6. boolean val = array[i];  
  7.             if (val) {  
  8.                 // System.arraycopy("true".toCharArray(), 0, buf, currentSize, 4);  
  9.                 buf[currentSize++] = 't';  
  10.                 buf[currentSize++] = 'r';  
  11.                 buf[currentSize++] = 'u';  
  12.                 buf[currentSize++] = 'e';  
  13.             } else {/** 省略 **/}  

 

数字+字符输出

Java代码   收藏代码
  1. public void writeIntAndChar(int i, char c)  
  2. public void writeLongAndChar(long i, char c)  

 

    以上两个方法主要在处理以下情况下使用,在不知道要进行序列化的对象的长度的情况下,要尽量避免进行buf数据扩容的情况出现。尽管这种情况很少发生,但还是尽量避免。特殊是在输出集合数据的情况下,在集合数据输出下,各个数据的长度未定,因此不能计算出总输出长度,只能一个对象一个对象输出,在这种情况下,先要输出一个对象,然后再输出对象的间隔符或结尾符。如果先调用输出数据,再调用输出间隔符或结尾符,远不如将两者结合起来,一起进行计算和输出。
    此方法基于以下一个事实:尽量在已知数据长度的情况下进行字符拼接,这样有利于快速的为数据准备数据空间。
    在具体实现时,此方法只是减少了数据扩容的计算,其它方法与基本实现和组合是一致的,以writeIntAndChar为例:

Java代码   收藏代码
  1. public void writeIntAndChar(int i, char c) throws IOException {  
  2.         //minValue处理  
  3. //长度计算,长度为数字长度+字符长度  
  4.         int size = (i < 0) ? IOUtils.stringSize(-i) + 1 : IOUtils.stringSize(i);  
  5.         int newcount0 = count + size;  
  6.         int newcount1 = newcount0 + 1;  
  7. //扩容计算  
  8.         IOUtils.getChars(i, newcount0, buf);//输出数字  
  9.         buf[newcount0] = c;//输出字符  
  10.         count = newcount1;//最终count定值  
  11.     }  

 

字符串处理

    作为在业务系统中最常用的类型,字符串是一个必不可少的元素之一。在json中,字符串是以双(单)引号,引起来使用的。因此在输出时,即要在最终的数据上追加双(单)引号。否则,js会将其作为变量使用而报错。而且在最新的json标准中,对于json中的key,也要求必须追加双(单)引号以示区分了。字符串处理方法有以下几种:

Java代码   收藏代码
  1. public void writeStringWithDoubleQuote(String text)  
  2. public void writeStringWithSingleQuote(String text)  
  3. public void writeKeyWithDoubleQuote(String text)  
  4. public void writeKeyWithSingleQuote(String text)  
  5. public void writeStringArray(String[] array)  
  6. public void writeKeyWithDoubleQuoteIfHashSpecial(String text)  
  7. public void writeKeyWithSingleQuoteIfHashSpecial(String text)  

 

     其中第1,2方法表示分别用双引号和单引号将字符串包装起来,第3,4方法表示在字符串输出完毕之后,再输出一个冒号,第5方法表示输出一个字符串数组,使用双引号包装字符串。第7,8方法未知(不明真相的方法?)
    字符串是可以知道长度的,所以第一步确定长度即OK了。 在第一步扩容计算之后,需要处理一个在字符串中特殊的问题,即转义字符处理。如何处理转义字符,以及避免不必要的扩容计算,是必须要考虑的。在fastjson中,采取了首先将其认定为全非特殊字符,然后再一个个字符判断,对特殊字符再作处理的方法。在一定程序上避免了在一个个判断时,扩容计算的问题。我们就其中一个示例进行分析:

Java代码   收藏代码
  1. public void writeStringWithDoubleQuote(String text) {  
  2. //null处理,直接追加null字符即可,不需要双引号  
  3.         int len = text.length();  
  4.         int newcount = count + len + 2;//初始计算长度为字符串长度+2(即双引号)  
  5. //初步扩容计算  
  6.    
  7.         int start = count + 1;  
  8.         int end = start + len;  
  9.         buf[count] = '\"';//追加起始双引号  
  10.         text.getChars(0, len, buf, start);  
  11.         count = newcount;//初步定count值  
  12. /** 以下代码为处理特殊字符 */  
  13.         for (int i = start; i < end; ++i) {  
  14.             char ch = buf[i];  
  15.             if (ch == '\b' || ch == '\n' || ch == '\r' || ch == '\f' || ch == '\\' || ch == '/' || ch == '"') {//判断是否为特殊字符  
  16. //这里需要修改count值,以及扩容判断,省略之  
  17.                 System.arraycopy(buf, i + 1, buf, i + 2, end - i - 1);//数据移位,从当前处理点往后移  
  18.                 buf[i] = '\\';//追加特殊字符标记  
  19.                 buf[++i] = replaceChars[(int) ch];//追加原始的特殊字符为\b写为b,最终即为\\b的形式,而不是\\\b  
  20.                 end++;  
  21.             }  
  22.         }  
  23.    
  24.         buf[newcount - 1] = '\"';//转出结尾双引号  
  25.     }  

 

    在处理字符串上,特殊的即在特殊字符上。因为在输出时,要输出时要保存字符串的原始模式,如\"的格式,要输出时,要输出为\ + "的形式,而不能直接输出为\",后者在输出时就直接输出为",而省略了\,这在js端是会报错的。

    总结:

    在针对输出优化时,主要利用了最有效率的手段进行处理。如针对数字和boolean时的处理方式。同时,在处理字符串时,也采取了先处理最常用字符,再处理特殊字符的形式。在针对某些经常碰到的场景时,使用了联合处理的手段(如writeIntAndChar),而不再是分开处理。
    整个处理的思想,即是在处理单个数据时,采取最优方式;在处理复合数据时,避免扩容计算;尽量使用jdk中的方法,以避免重复轮子(可能轮子更慢)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值