加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

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

发布时间:2020-12-16 19:29:40 所属栏目:百科 来源:网络整理
导读:接上篇,在论述完基本概念和总体思路之后,我们来到整个程序最重要的部分-性能优化。之所以会有fastjson这个项目,主要问题是为了解决性能这一块的问题,将序列化工作提高到一个新的高度。我们提到,性能优化主要有两个方面,一个如何将处理后的数据追加到数

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

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

Java代码
  1. publicSerializeWriter(){
  2. buf=bufLocal.get();//newchar[1024];
  3. if(buf==null){
  4. buf=newchar[1024];
  5. }else{
  6. bufLocal.set(null);
  7. }
  8. }

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

Java代码
  1. /**
  2. *Closethestream.Thismethoddoesnotreleasethebuffer,sinceitscontentsmightstillberequired.Note:
  3. *Invokingthismethodinthisclasswillhavenoeffect.
  4. */
  5. publicvoidclose(){
  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. publicvoidwrite(intc)
  2. publicvoidwrite(charc)
  3. publicvoidwrite(charc[],intoff,intlen)
  4. publicvoidwrite(Stringstr,intlen)
  5. publicSerializeWriterappend(CharSequencecsq)
  6. publicSerializeWriterappend(CharSequencecsq,intstart,intend)
  7. publicSerializeWriterappend(charc)

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

Java代码
  1. publicvoidwrite(charc[],intlen){
  2. intnewcount=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. publicvoidwriteInt(inti)
  2. publicvoidwriteLong(longi)

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

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

以上首先看特殊数字的处理,因为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,char[] buf)方法,这里也不再叙述。

基本类型数组

Java代码
  1. publicvoidwriteBooleanArray(boolean[]array)
  2. publicvoidwriteShortArray(short[]array)
  3. publicvoidwriteByteArray(byte[]array)
  4. publicvoidwriteIntArray(int[]array)
  5. publicvoidwriteIntArray(Integer[]array)
  6. publicvoidwriteLongArray(long[]array)

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

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

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

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

数字+字符输出

Java代码
  1. publicvoidwriteIntAndChar(inti,charc)
  2. publicvoidwriteLongAndChar(longi,charc)

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

Java代码
  1. publicvoidwriteIntAndChar(inti,charc)throwsIOException{
  2. //minValue处理
  3. //长度计算,长度为数字长度+字符长度
  4. intsize=(i<0)?IOUtils.stringSize(-i)+1:IOUtils.stringSize(i);
  5. intnewcount0=count+size;
  6. intnewcount1=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. publicvoidwriteStringWithDoubleQuote(Stringtext)
  2. publicvoidwriteStringWithSingleQuote(Stringtext)
  3. publicvoidwriteKeyWithDoubleQuote(Stringtext)
  4. publicvoidwriteKeyWithSingleQuote(Stringtext)
  5. publicvoidwriteStringArray(String[]array)
  6. publicvoidwriteKeyWithDoubleQuoteIfHashSpecial(Stringtext)
  7. publicvoidwriteKeyWithSingleQuoteIfHashSpecial(Stringtext)

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

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

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

总结:

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

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

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读