alibaba fastjson(json序列化器)序列化部分源码解析-2-性能优化A

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

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

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数组再次绑定在线程变量当中,以下所示:web

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。json

    在实现过程中,不少具体的实现是借鉴了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函数。性能优化

 

    实现解析app

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

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中的方法,以免重复轮子(可能轮子更慢)。

    下一篇,从数据处理过程对源码进行分析,同时解析其中针对性能优化的处理部分。

相关文章
相关标签/搜索