本文由騰訊WeTest團隊提供,未經受權嚴禁轉載!更多資訊可直接戳連接查看:http://wetest.qq.com/lab/
微信號:TencentWeTestjava
文/張蓓c++
引言
靜態代碼分析是指無需運行被測代碼,經過詞法分析、語法分析、控制流、數據流分析等技術對程序代碼進行掃描,找出代碼隱藏的錯誤和缺陷,如參數不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現的空指針引用等等。統計證實,在整個軟件開發生命週期中,30% 至 70% 的代碼邏輯設計和編碼缺陷是能夠經過靜態代碼分析來發現和修復的。web
在C++項目開發過程當中,由於其爲編譯執行語言,語言規則要求較高,開發團隊每每要花費大量的時間和精力發現並修改代碼缺陷。因此C++ 靜態代碼分析工具可以幫助開發人員快速、有效的定位代碼缺陷並及時糾正這些問題,從而極大地提升軟件可靠性並節省開發成本。數組
靜態代碼分析工具的優點 :安全
1.自動執行靜態代碼分析,快速定位代碼隱藏錯誤和缺陷。微信
2. 幫助代碼設計人員更專一於分析和解決代碼設計缺陷。函數
3. 減小在代碼人工檢查上花費的時間,提升軟件可靠性並節省開發成本。工具
2業界主流靜態代碼掃描工具概況
目前市場上的C++ 靜態代碼分析工具種類繁多且各有千秋,本文將分別介紹TSC團隊自主研發的tscancode工具和當前4種主流C++靜態代碼分析工具(cppcheck、coverity、clang、pclint),並從功能、效率、易用性等方面對它們進行分析和比較,以期幫助 C++開發人員更清晰靜態代碼分析工具的工做效果、適用場景和擴展空間,同時在其對應項目特徵中選擇合適的工具應用到項目開發環節中。測試
如下爲工具在付費價格、規則數量、準確率、掃描效率、編譯依賴、IDE支持、跨平臺支持、可擴展開發方面的對比數據。注:本次競品分析的選擇了3款遊戲項目(約500萬行代碼)。 ui
在可擴展性上,TSC有專人維護,按期根據用戶需求擴展規則或新增功能特性,cppcheck和clang是開源工具,工具更新較慢,但若是用戶有特殊需求能夠本身擴展開發,pclint和coverity是商業軟件,難以進行功能擴展。
同時,TSC有完整代碼質量管理閉環平臺QOC支持; coverity和clang可用web端的結果展現,但沒法自行管理問題流,須要進行二次開發; cppcheck和pclint缺乏web端結果展現。
如下重點比較具體檢查規則和有效問題報錯率。
3檢查規則大比拼
3.1規則大類
針對業內大量掃描工具在實際項目中掃描結果的影響比較,咱們將代碼質量問題分爲如下幾大類:
① 致命類:可能致使程序宕機、無響應等影響範圍極大的錯誤;
② 邏輯類:可能形成程序不能達到預期邏輯結果的錯誤;
③ 編碼規範及其餘類:可能形成程序的可讀性、可維護性較差的錯誤(不可達代碼,無效的變量聲明等);
3.2 規則大類分佈
根據3大影響分類,其嚴重程度分別爲高、中、低,各種型規則數量分佈爲:
從規則分類佔比來看:
① TSC針對互聯網產品高效開發修復原則,工具定位爲針對致命和邏輯類問題,相對傳統、軍事、安全領域,並不關注編碼規範及編譯錯誤;
② coverity做爲商業化軟件,在付費後添加規則上,達到覆蓋率最全面,除致命和邏輯類規則外,還有大量編碼規範、安全和針對其餘語言(如java,C#)的規則;
③ cppcheck做爲開源工具,應用範圍普遍,根據開源社區場景蒐集,在各方面都有規則添加,但場景較爲粗獷,場景雖多,但有效率不高。例如:cppcheck在初始化檢查上有5個子規則,樣本代碼共掃描出312個問題,其中有效問題僅8個,有效率僅爲3%。
④ pclint做爲商業化軟件,在付費後添加規則上,達到覆蓋率最全面,除致命和邏輯類規則外,還有大量編碼規範、安全的規則;
⑤ clang做爲開源軟件,規則較少,但規則類型分佈較爲均勻,在致命、邏輯類,還有編碼規範、安全類都有規則添加。
3.3規則報錯數量
總體規則數量上:pclint[915]>coverity[515]>cppcheck[245]>clang[74]>TSC[67]
能夠看出pclint和coverity規則最多,TSC和clang規則最少,緣由有以下3點:
① pclint和coverity做爲商業化軟件,需求來源於傳統軟件、軍事、安全各個領域,其規則總數最多,其編碼規範類規則數量分別高達646條和382條;排除掉低價值的編碼規範類規則,規則數量排序爲:pclint[269]>cppcheck[151]>coverity[133]>TSC[67]>clang[44]
② 在規則實際報錯數量上,以3款遊戲500萬行代碼的結果覆蓋度來看;
注:規則總數指工具全部的規則總數,報錯規則數指開啓工具全部規則狀況下,掃描樣本代碼所覆蓋的規則數量。
從實際項目掃描結果來看:
掃描出問題的規則數/規則總數:TSC[60%]>cppcheck[27%]>clang[19%]>coverity[10%]>pclint[9%]
pclint、coverity、cppcheck雖然規則數量不少,但由於其定製加入的大部分規則廣泛適用度不高,大量規則可能在多個項目中都沒法掃描出問題。有些規則卻在多個項目中掃描出大量非核心的問題,如:函數沒有被調用、未使用的變量、存在多餘的頭文件等。
③ 規則數量多來源於兩個方面,一方面是規則覆蓋更全面,另外一方面是規則粒度劃分得更細;
經過對具體規則進行分析,發如今規則劃分粒度由細到出排序爲[pclint,coverity,cppcheck,clang,TSC]
pclint和coverity劃分粒度最細,cppcheck,clang次之,TSC最粗。
例如:coverity的除0報錯分爲整型除0,浮點數除0,取模除0;數組下標越界也細分爲訪問越界、讀越界、寫越界。Pclint和cppcheck初始化分爲變量未初始化、結構體成員未初始化、類成員未初始化、string未初始化、data未初始化、union未初始化、全局靜態變量未初始化等;而TSC則合併了一些過細的規則,未初始化上只分爲變量未初始化和成員未初始化。
粒度劃分越細既有優勢也有缺點:
優勢:能夠針對細分規則靈活配置開關,關掉準確率低的規則
缺點:規則數量太多, 用戶配置至關麻煩,新用戶很難理解多個類似的規則以前的區別。
TSC爲下降用戶配置難度,在規則粒度劃分上相對粗獷,但會從中提取出其中準確率低的場景,做爲單獨規則,從而達到能夠關掉低準確率規則的目的。
4同類規則效果對比分析
本文針對每一個工具在關鍵報錯項,如:空指針、越界、變量未初始化、內存泄露、邏輯上的報錯結果進行分析。
樣本代碼——3款遊戲項目(約500萬行代碼)代碼
測試對象——tscancode2.0、coverity7.五、cppcheck1.6八、pclint9.0、clang3.4
有效報錯數——某類規則在3款遊戲項目的有效報錯數總和
準確率——某類規則在3款遊戲項目的平均準確率,準確率=有效報錯數/報錯總數*100%
綜合評分——綜合有效報錯數和準確率的評分,有效報錯數和準確率的權值暫定爲45:55,綜合評分=有效報錯/最大有效報錯數*100*45%+準確率*100*55%
4.1空指針規則
空指針檢查規則主要檢查是否存在對賦值爲空的指針解引用的狀況,空指針是c/c++中最大的問題,常常形成程序崩潰的致命錯誤。所以,C++靜態代碼分析工具對空指針的檢查能力顯得尤其重要。
圖爲五個工具對樣本代碼掃描結果:
從報錯數量和準確率來看:
有效報錯數:TSC [401] >coverity[219]>>clang[57] >cppcheck[20]>pclint[14]
準確率: coverity[95%]≈TSC[92%] ≈clang[90%]>>cppcheck[28%]>pclint[14%]
綜合評分: TSC[96分] >coverity[77分] >clang[56分]>cppcheck[18分]>pclint[8分]
1. 從準確率來看,在空指針檢查方面,不考慮掃描效率和掃描環境搭建複雜度,TSC、coverity和clang都很優秀,三者準確率都很高。cppcheck, pclint在結果準確率上和數量上都較差,不推薦使用。
2. 從空指針規則細分程度來看,TSC和coverity至關,細分場景挖掘更多,cppcheck規則並未細分空指針規則,從實際項目結果來看,只能檢查出dereferenceBeforeCheck場景的錯誤。Clang和pclint在空指針細分上維度跟TSC和coverity不一樣,好比:它們區分是參數指針解引用仍是局部變量解引用,細分粒度不夠且覆蓋場景較少,其覆蓋場景基本都被TSC和coverity包含。
cppcheck掃描出來的問題存在大量誤報,誤報主要是冗餘的判空,並不會引發實際問題,具體誤報場景以下:
3. 從有效報錯數量上,TSC有效報錯數量更多,細分場景挖掘更多,無疑是掃描空指針最佳選擇;clang覆蓋的場景較少,其有效報錯基本都能被coverity和TSC覆蓋,不過因爲其準確率較高且免費,與TSC搭配使用也是不錯的選擇;而coverity雖然覆蓋場景多但由於只會報徹底可信的問題,所以會漏掉部分有效報錯,例如:指針變量來源於函數返回值,而函數返回值是否爲NULL依賴於用戶輸入,在靜態分析中coverity沒法判斷其是否會爲NULL,爲保證準確率會漏掉該指針報錯。若項目對空指針漏報容忍度較高,且有足夠預算採購商業軟件,能夠選擇coverity;而cppcheck和pclint檢查出的有效問題極少並伴隨大量誤報,同上結論,不宜使用。
4. 在易用性上,coverity和clang編譯環境構建複雜,編譯時長增長較多;TSC在易用性上也有一個缺點,即爲提升準確率,在個別項目存在一次性配置工做。緣由是個別項目存在自定義判空宏,但因爲不依賴編譯,TSC掃描的代碼可能並不完整,致使個別自定義判空宏找不到,須要在cfg.ini中配置自定義判空宏。固然,若是掃描的代碼完整度同編譯環境,則無此問題。
4.2越界規則
越界通常來說是指數組下標越界,或者緩衝區讀寫越界。這類錯誤會致使非法內存的訪問,引起程序崩潰或者錯誤。
下圖是五個工具對樣本代碼掃描結果:
注:越界對誤報斷定的規則比較嚴格,即便場景識別自己無誤,可是經過代碼邏輯能夠推斷該場景不會越界的也斷定爲誤報。
例如:
這裏由found變量間接推斷出data[region_index]不會越界,將其斷定爲誤報。
從報錯數量和準確率來看:
有效報錯數:coverity[98]>>TSC [18]>pclint[16] >cppcheck[6]> clang[4]
準確率: clang[100%] >coverity[80%]>TSC[70%] >cppcheck[67%]>>pclint[2%]
綜合評分:coverity[90分] >TSC[54分]≈clang[55分]>cppcheck[40分]>pclint[1分]
1. 在報錯數量上,coverity在越界檢查上有較大的優點,由於coverity有較強的符號查找和場景識別能力,能識別相對複雜的越界場景。其餘四個工具同coverity相比還有差距,其中pclint存在大量誤報,表現最差。如:TSC和cppcheck只能識別數組變量自己越界,但若是是一個指針p指向數組的第一個元素,經過p[i]訪問時的越界,TSC和cppcheck都沒法檢查,而coverity能找到p所指向的數組定義,獲得數組大小,從而判斷p[i]是否越界。
2. clang越界這塊的準確率雖然最高爲100%,但其覆蓋的場景單一(strncpy使用越界報了4條),其報錯都被TSC和coverity覆蓋,數量上和其餘工具備較大差距。TSC越界檢查結果要略好於cppcheck,clang和pclint,TSC增長了對變量取值範圍的推斷,檢測出是否存在越界的風險。好比:
(TSC越界有效報錯場景)
對於數組下標iCountry的斷定存在風險,代碼執行到當前上下文時,iCountry可能取值爲MAX_QT_COUNTRY_JIFEN_ITEM_CNT,而這正是數組m_astDataInDB的長度,也就是說在這種邊界狀況下會形成了數組訪問越界。對於如上場景,應該將代碼修改成iCountry>= MAX_QT_COUNTRY_JIFEN_ITEM_CNT。
4.3變量未初始化規則
變量未初始化顧名思義:變量聲明後沒有賦初值,其分配的內存值是隨機的。這也是代碼中容易出現的問題,會致使不肯定的程序行爲,形成嚴重的後果。
下圖是五個工具對樣本代碼掃描結果:
注:結果排除了3個工具都有的檢查項——構造函數中是否存在未初始化成員變量。在實際項目中發現,C++類構造函數中對成員變量不作初始化的狀況是廣泛的,不少代碼會採用「延遲初始化」,即在實際用到該對象的時候調用相似Initialize的方法進行初始化。所以在這次對比中並無把這條規則歸入進來。
從報錯數量和準確率來看:
有效報錯數:coverity[75]>>pclint[25] >TSC [9]>cppcheck[8]> clang[1]
準確率: TSC[75%] >coverity[68%]>pclint[26%] > clang[17%] >cppcheck[3%]
綜合評分:coverity[82分] > TSC[47分] >pclint[30分] > clang[10分] >cppcheck[6分]
1. 在報錯數量上,coverity初始化檢查場景覆蓋比其餘四個工具要全,TSC爲保持準確率,規則覆蓋上比較保守,而cppcheck存在比較嚴重的誤報問題,準確率僅爲3%。pclint的誤報也相對很高,clang在初始化這塊顯得無能爲力。從上圖能夠很容易發現cppcheck的誤報數量至關得高,cppcheck會將以下的場景斷定爲未初始化:
(cppcheck誤報場景)
SMD_POS是一個簡單的結構體,它包含了一個空的構造函數,cppcheck依據這點斷定這是一個未初始化的錯誤。但這樣的場景不會有什麼問題,算是一個誤報。這致使了cppcheck在未初始化規則的結果可信度大大下降。
2. coverity在未初始化這塊的場景覆蓋比較全,特別是對結構體對象的字段的初始化狀況的檢測,由於其基於編譯可對變量作路徑跟蹤,例如:構造函數裏面調用了init()函數,coverity會繼續跟蹤init()函數中是否有對變量的賦值,因此掃描覆蓋場景最全。coverity的誤報主要分爲兩類:一類是對幾種未初始化場景的識別上存在問題,如:,變量在某個分支的確沒有初始化,但用了一個狀態標識其未初始化,當使用這個變量前會使用狀態標記來判斷其是否沒有初始化,保證使用的變量都是初始化過了的。另外一類就是上面提到的「低價值報錯」,即經過代碼邏輯或者作了代碼保護,保證變量不會由於沒有初始化而產生實際的問題。如:一個表示時間的結構體,裏面字段有year,month,day,hour,min,day這個字段沒有初始化,但實際代碼中也沒有用到這個字段,所以並不會產生任何問題。
TSC在未初始化變量的檢查因不具有路徑分析能力,而以分支做用域檢查特定變量在各個代碼分支的初始化狀況,誤報率保持在相對低的一個水平。但場景覆蓋較少,沒有針對結構體字段的初始化場景作覆蓋。由於對結構字段的初始化方式相對比較多樣:逐個字段初始化,函數調用初始化,構造函數初始化等。
4.4內存/資源泄露規則
內存泄漏指因爲疏忽或錯誤形成程序未能釋放已經再也不使用的內存,從而形成了內存浪費的狀況。內存泄漏是靜態下很難檢測的一種錯誤,通常須要動態分析工具進行檢測,如valgrind工具會捕獲malloc()/free()/new/delete的調用,監控內存分配和釋放,從動態上檢測程序是否存在內存泄漏。所以,靜態代碼分析能檢查的內存泄漏就很是有限了,當前各工具主要是從代碼寫法上檢查內存分配和釋放是否配對使用。好比:fopen打開文件後在退出函數前是否有執行fclose,new[]和delete[]是否配對使用等。
下圖是五個工具對樣本代碼掃描結果:
注:以上數據排除了cppcheck35個低價值報錯,這裏排除的cppcheck35個報錯都是基本數據類型的new和delete不匹配(如char* p=new char[100];delete p;)雖然這種寫法不規範,但因爲實際上不會形成內存泄漏,不少項目不會對此進行修復。
從報錯數量和準確率來看:
有效報錯數:pclint[55] >TSC[40]>coverity [29]>cppcheck[28]> clang[0]
準確率: coverity[100%]=cppcheck[100%] >TSC[73%]>pclint[23%] > clang[N/A]
綜合評分:coverity[79分] ≈ TSC [73分]≈cppcheck[77分]>pclint[57分]>clang[0分]
從報錯數量上看出,在內存泄漏檢查方面,pclint雖然發現有效問題最多,但誤報很高,不推薦使用。TSC的有效錯誤數比coverity和cppcheck多,但誤報也相對較高。clang則不具有泄露類場景的檢測能力。
注:因爲靜態掃描能檢查的內存泄露場景都很是明確,所以通常都不會出現問題,TSC的15個誤報也非場景識別有誤而是工具底層bug致使,後續會對底層bug進行修復。如:#ifdef 和#else分支中各有一個fopen,實際編譯時只會走其中1個分支識別1次fopen,但因爲底層bug識別了2次fopen,致使誤報。
4.5邏輯錯誤規則
邏輯錯誤:指可能存在的邏輯問題,如if不一樣分支內容相同,在switch內缺乏break等,對指針使用sizeof進行空間分配等問題。
下圖是五個工具對樣本代碼掃描結果:
注:這些報錯中剔除了一些無修改意義且結果數量不少規則:如:coverity掃描存在7484條Logically dead code(邏輯代碼不可達)報錯。cppcheck存在2246條unusedFunction(函數未被使用)報錯。
從報錯數量和準確率來看
有效數量:TSC[293]>coverity[164]>clang[142] >cppcheck [120]>pclint[116]
準確率:clang[97%] >TSC[93%]>coverity(88%)>pclint[72%] >cppcheck[55%]
綜合評分:coverity[94分] > TSC[86分] > clang[80分] >cppcheck[63分] >pclint[27分]
從報錯數量和準確率上能夠看出TSC能夠更有效的發現邏輯類問題。但各工具邏輯類場景各有特點,互爲互補,能夠一同選擇掃描,但cppcheck和pclint準確率較低,能夠較少選擇。clang的準確率最高,但clang掃描出來的邏輯錯誤中有一大半爲低價值的邏輯錯誤,好比clang掃描出來的142條邏輯錯誤中就有140條「變量賦值但沒有使用」錯誤。
1. TSC,coverity具有較強宏展開能力
以DuplicateExpression規則爲例,TSC發現DuplicateExpression規則報錯32條, cppcheck發現DuplicateExpression規則報錯12條。由於TSC能夠對宏進行更有效展開,例如:
這種報錯TSC能夠準確的識別出來,宏MAX_TASK_TAB_SIZE和MAX_TASK_RES_NUM爲相同的數值,而cppcheck沒法區分發現這類問題,只能進行簡單的文本匹配。coverity在推斷能力上也不差,在這點也明顯優於cppcheck。
2. TSC規則類型更有效
通過篩選,TSC只保留價值更高的推斷和有效規則;
Ø 增長一些函數檢查規則,如:MemsetZeroBytes,這種錯誤的Memset寫法:memset(ctYear, sizeof(ctYear), 0);可疑的數組下標使用等這些規則在coverity邏輯類檢查中並無體現,而coverity只會報出很是準確的報錯如:if分支徹底相同等檢查項。
Ø 剔除價值低的無效規則,如coverity規則Logically dead code,指一些邏輯上不可達的廢棄代碼;cppcheck規則memsetClassFloatc指對存在Float類型成員變量的Class使用Memset,當時代碼中發現基本都是Memset爲0,並不會有數據丟失等問題。故這類規則發現有效問題很低,在數量較大的狀況下,須要耗費大量的人力來確認,性價比不高,TSC已經將這種規則剔除。
總的來講,TSC在發現問題和準確率方面表現都不錯,能夠節省大量的人力在鎖定邏輯類型錯誤。
TSC在某些細小規則的推斷能力上比coverity要稍微弱一些,如規則Missing break in switch:coverity發現所有準確的報錯,TSC存在必定的誤報,這些複雜場景須要較強的動態計算如:
5 常見誤報場景
5.1 空指針常見誤報場景
誤報場景一(cppcheck)
以上538行代碼報quiz_set_ptt存在空指針訪問。
誤報緣由:538行只是指針的比較,並無解引用,這是一個比較低級的誤報。
誤報場景二(coverity)
以上119行代碼報actor存在空指針訪問,斷定邏輯以下:112行對actor進行了判空,說明actor在當前上下文可能爲空。因此119行actor可能爲空。
誤報緣由:xy_assert_retval是個宏,展開後包含有return語句,即若是actor爲空115行就返回了,119行actor不會爲空。
5.2 越界常見誤報場景
誤報場景一(TSC)
以上83行代碼報第數組訪問可能越界,斷定邏輯以下:第61行的if語句對req_list.num的取值範圍做了限制,req_list.num在當前上下文的最大值能夠是MAX_RECRUIT_REQ_LIST_SIZE(4);83行req_list.數組對象用req_list.num做爲其數組訪問的下標,當req_list.num取值爲MAX_RECRUIT_REQ_LIST_SIZE時發生越界(req_list.數組的長度爲MAX_RECRUIT_REQ_LIST_SIZE(4))。
誤報緣由:第79行的if條件保證了以後的代碼req_list.num的值不會等於MAX_RECRUIT_REQ_LIST_SIZE,因此這是一個誤報。
誤報場景二(cppcheck)
以上第691行代碼報t_index_map可能取值-1越界,斷定邏輯以下:665行聲明t_index_map並賦值爲-1,t_index_map的賦值在681行,但681行在for循環裏面,而for循環存在不能進入的可能性,因此在691行使用t_index_map可能未初始化。
誤報緣由:進入691行代碼的前提條件是found變量爲true,而found爲true保證了t_index_map被賦值了。
誤報場景三(coverity)
以上第146行代碼報src_index + 1可能取值爲4越界,斷定邏輯以下:139行對src_idx的取值範圍進行了限定:0, 3,所以146行src_idx + 1可能爲4致使對team_ptr->team_member訪問越界。
誤報緣由:144行對src_idx的取值範圍進行了過濾,保證了src_idx+1不會越界。
5.3未初始化常見誤報場景
誤報場景一(cppcheck)
以上第462行代碼報ret未初始化錯誤,斷定邏輯以下:ret變量在第434行聲明,在switch中的兩個case中均有初始化代碼,可是在default分支中沒有對ret進行初始化,所以斷定462行可能會返回一個沒有初始化的ret。
誤報緣由:default分支中的xy_assert_retval是一個宏,由於cppcheck宏查找策略的緣由致使該宏沒有展開。實際上宏展開包含了return語句,也就是說若是進入default分支就函數就直接返回而不會執行到462行代碼。
誤報場景二(coverity)
以上第284行代碼報careers未初始化錯誤,斷定邏輯以下:careers數組在第278行聲明,但在for循環對每一個數組成員進行了初始化。這可能形成careers徹底沒有初始化,或者只初始化了一部分。所以在284行使用careers存在未初始化錯誤。
誤報緣由:經過代碼邏輯可知,career_num表明的是careers被初始化的長度,在訪問careers數組元素的時候,經過career_num進行了保護,所以不會出現未初始化的錯誤。
5.4泄露類常見誤報場景
誤報場景一(TSC)
以上第63行代碼報fp存在資源泄露風險錯誤,斷定邏輯以下:xy_assert_retnone宏展開後,含有return語句,也就是說fp在調用fclose以前可能返回,存在泄露風險。
誤報緣由:實際上代碼邏輯決定了函數return的前提條件fp爲空。這個時候是沒有必要調用fclose的,不存在泄露風險。
誤報場景二(pclint)
以上第139行代碼(~CGIProcessor(), 析構函數)報存在資源泄露風險錯誤,由於沒有釋放_cgiContainer。斷定邏輯以下:_cgiContainer做爲CGIProcessor的一個指針成員(第149行),須要在析構函數中進行釋放,不然爲內存泄露。
誤報緣由:CGIProcessor對象並不own _cgiContainer指向的對象,不須要它來釋放。
5.5邏輯類常見誤報場景
誤報場景一(cppcheck)
以上4596行代碼報「對包含有float成員的對象調用memset方法」錯誤。
誤報緣由:利用memset對一個對象的數據字段清零是比較常見的作法,float成員清零後值也爲0,不會形成什麼問題。
本文由騰訊WeTest團隊提供,未經受權嚴禁轉載!更多資訊可直接戳連接查看:http://wetest.qq.com/lab/ 微信號:TencentWeTest --------------------- 做者:騰訊WeTest 來源:CSDN 原文:https://blog.csdn.net/wetest_tencent/article/details/51516347 版權聲明:本文爲博主原創文章,轉載請附上博文連接!