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

    下一篇,從數據處理過程對源碼進行分析,同時解析其中針對性能優化的處理部分。

相關文章
相關標籤/搜索