天天看點

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

大多數使用者感覺到的卡頓等性能問題的最主要根源都是因為渲染性能。從設計師的角度,他們希望App能夠有更多的動畫,圖檔等時尚元素來實作流暢的使用者體驗。但是Android系統很有可能無法及時完成那些複雜的界面渲染操作。Android系統每隔16ms發出VSYNC信号,觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實作60fps,這意味着程式的大多數操作都必須在16ms内完成。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

  

  如果你的某個操作花費時間是24ms,系統在得到VSYNC信号的時候就無法進行正常渲染,這樣就發生了丢幀現象。那麼使用者在32ms内看到的會是同一幀畫面。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

  使用者容易在UI執行動畫或者滑動ListView的時候感覺到卡頓不流暢,是因為這裡的操作相對複雜,容易發生丢幀的現象,進而感覺卡頓。有很多原因可以導緻丢幀,也許是因為你的layout太過複雜,無法在16ms内完成渲染,有可能是因為你的UI上有層疊太多的繪制單元,還有可能是因為動畫執行的次數過多。這些都會導緻CPU或者GPU負載過重。

  我們可以通過一些工具來定位問題,比如可以使用HierarchyViewer來查找Activity中的布局是否過于複雜,也可以使用手機設定裡面的開發者選項,打開Show GPU Overdraw等選項進行觀察。你還可以使用TraceView來觀察CPU的執行情況,更加快捷的找到性能瓶頸。

(a)Understanding Overdraw

  Overdraw(過度繪制)描述的是螢幕上的某個像素在同一幀的時間内被繪制了多次。在多層次的UI結構裡面,如果不可見的UI也在做繪制的操作,這就會導緻某些像素區域被繪制了多次。這就浪費大量的CPU以及GPU資源。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

  當設計上追求更華麗的視覺效果的時候,我們就容易陷入采用越來越多的層疊元件來實作這種視覺效果的怪圈。這很容易導緻大量的性能問題,為了獲得最佳的性能,我們必須盡量減少Overdraw的情況發生。

  幸運的是,我們可以通過手機設定裡面的開發者選項,打開Show GPU Overdraw的選項,可以觀察UI上的Overdraw情況。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

  藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,我們的目标就是盡量減少紅色Overdraw,看到更多的藍色區域。

  Overdraw有時候是因為你的UI布局存在大量重疊的部分,還有的時候是因為非必須的重疊背景。例如某個Activity有一個背景,然後裡面的Layout又有自己的背景,同時子View又分别有自己的背景。僅僅是通過移除非必須的背景圖檔,這就能夠減少大量的紅色Overdraw區域,增加藍色區域的占比。這一措施能夠顯著提升程式性能。

(b)Understanding VSYNC

  為了了解App是如何進行渲染的,我們必須了解手機硬體是如何工作,那麼就必須了解什麼是VSYNC。

  在講解VSYNC之前,我們需要了解兩個相關的概念:

Refresh Rate:代表了螢幕在一秒内重新整理螢幕的次數,這取決于硬體的固定參數,例如60Hz。

Frame Rate:代表了GPU在一秒内繪制操作的幀數,例如30fps,60fps。

GPU會擷取圖形資料進行渲染,然後硬體負責把渲染後的内容呈現到螢幕上,他們兩者不停的進行協作。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

不幸的是,重新整理頻率和幀率并不是總能夠保持相同的節奏。如果發生幀率與重新整理頻率不一緻的情況,就會容易出現Tearing的現象(畫面上下兩部分顯示内容發生斷裂,來自不同的兩幀資料發生重疊)。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)
android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

通常來說,幀率超過重新整理頻率隻是一種理想的狀況,在超過60fps的情況下,GPU所産生的幀資料會因為等待VSYNC的重新整理資訊而被Hold住,這樣能夠保持每次重新整理都有實際的新的資料可以顯示。但是我們遇到更多的情況是幀率小于重新整理頻率。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

在這種情況下,某些幀顯示的畫面内容就會與上一幀的畫面相同。糟糕的事情是,幀率從超過60fps突然掉到60fps以下,這樣就會發生LAG,JANK,HITCHING等卡頓掉幀的不順滑的情況。這也是使用者感受不好的原因所在。

(c)Tool:Profile GPU Rendering

性能問題如此的麻煩,幸好我們可以有工具來進行調試。打開手機裡面的開發者選項,選擇Profile GPU Rendering,選中On screen as bars的選項。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

選擇了這樣以後,我們可以在手機畫面上看到豐富的GPU繪制圖形資訊,分别關于StatusBar,NavBar,激活的程式Activity區域的GPU Rending資訊。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

随着界面的重新整理,界面上會滾動顯示垂直的柱狀圖來表示每幀畫面所需要渲染的時間,柱狀圖越高表示花費的渲染時間越長。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

中間有一根綠色的橫線,代表16ms,我們需要確定每一幀花費的總時間都低于這條橫線,這樣才能夠避免出現卡頓的問題。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

每一條柱狀線都包含三部分,藍色代表測量繪制Display List的時間,紅色代表OpenGL渲染Display List所需要的時間,×××代表CPU等待GPU處理的時間。

(d)Why 60fps?

我們通常都會提到60fps與16ms,可是知道為何會是以程式是否達到60fps來作為App性能的衡量标準嗎?這是因為人眼與大腦之間的協作無法感覺超過60fps的畫面更新。

12fps大概類似手動快速翻動書籍的幀率,這明顯是可以感覺到不夠順滑的。24fps使得人眼感覺的是連續線性的運動,這其實是歸功于運動模糊的效果。24fps是電影膠圈通常使用的幀率,因為這個幀率已經足夠支撐大部分電影畫面需要表達的内容,同時能夠最大的減少費用支出。但是低于30fps是無法順暢表現絢麗的畫面内容的,此時就需要用到60fps來達到想要的效果,當然超過60fps是沒有必要的。

開發app的性能目标就是保持60fps,這意味着每一幀你隻有16ms=1000/60的時間來處理所有的任務。

(e)Android, UI and the GPU

了解Android是如何利用GPU進行畫面渲染有助于我們更好的了解性能問題。那麼一個最實際的問題是:activity的畫面是如何繪制到螢幕上的?那些複雜的XML布局檔案又是如何能夠被識别并繪制出來的?

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

Resterization栅格化是繪制那些Button,Shape,Path,String,Bitmap等元件最基礎的操作。它把那些元件拆分到不同的像素上進行顯示。這是一個很費時的操作,GPU的引入就是為了加快栅格化的操作。

CPU負責把UI元件計算成Polygons,Texture紋理,然後交給GPU進行栅格化渲染。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

然而每次從CPU轉移到GPU是一件很麻煩的事情,所幸的是OpenGL ES可以把那些需要渲染的紋理Hold在GPU Memory裡面,在下次需要渲染的時候直接進行操作。是以如果你更新了GPU所hold住的紋理内容,那麼之前儲存的狀态就丢失了。

在Android裡面那些由主題所提供的資源,例如Bitmaps,Drawables都是一起打包到統一的Texture紋理當中,然後再傳遞到GPU裡面,這意味着每次你需要使用這些資源的時候,都是直接從紋理裡面進行擷取渲染的。當然随着UI元件的越來越豐富,有了更多演變的形态。例如顯示圖檔的時候,需要先經過CPU的計算加載到記憶體中,然後傳遞給GPU進行渲染。文字的顯示更加複雜,需要先經過CPU換算成紋理,然後再交給GPU進行渲染,回到CPU繪制單個字元的時候,再重新引用經過GPU渲染的内容。動畫則是一個更加複雜的操作流程。

為了能夠使得App流暢,我們需要在每一幀16ms以内處理完所有的CPU與GPU計算,繪制,渲染等等操作。

(f)Invalidations, Layouts, and Performance

  順滑精妙的動畫是app設計裡面最重要的元素之一,這些動畫能夠顯著提升使用者體驗。下面會講解Android系統是如何處理UI元件的更新操作的。

通常來說,Android需要把XML布局檔案轉換成GPU能夠識别并繪制的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有将要交給GPU繪制到螢幕上的資料資訊。

在某個View第一次需要被渲染時,DisplayList會是以而被建立,當這個View要顯示到螢幕上時,我們會執行GPU的繪制指令來進行渲染。如果你在後續有執行類似移動這個View的位置等操作而需要再次渲染這個View時,我們就僅僅需要額外操作一次渲染指令就夠了。然而如果你修改了View中的某些可見元件,那麼之前的DisplayList就無法繼續使用了,我們需要回頭重新建立一個DisplayList并且重新執行渲染指令并更新到螢幕上。

需要注意的是:任何時候View中的繪制内容發生變化時,都會重新執行建立DisplayList,渲染DisplayList,更新到螢幕上等一系列操作。這個流程的表現性能取決于你的View的複雜程度,View的狀态變化以及渲染管道的執行性能。舉個例子,假設某個Button的大小需要增大到目前的兩倍,在增大Button大小之前,需要通過父View重新計算并擺放其他子View的位置。修改View的大小會觸發整個HierarcyView的重新計算大小的操作。如果是修改View的位置則會觸發HierarchView重新計算其他View的位置。如果布局很複雜,這就會很容易導緻嚴重的性能問題。我們需要盡量減少Overdraw。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

我們可以通過前面介紹的Monitor GPU Rendering來檢視渲染的表現性能如何,另外也可以通過開發者選項裡面的Show GPU view updates來檢視視圖更新的操作,最後我們還可以通過HierarchyViewer這個工具來檢視布局,使得布局盡量扁平化,移除非必需的UI元件,這些操作能夠減少Measure,Layout的計算時間。

(g)Overdraw, Cliprect, QuickReject

引起性能問題的一個很重要的方面是因為過多複雜的繪制操作。我們可以通過工具來檢測并修複标準UI元件的Overdraw問題,但是針對高度自定義的UI元件則顯得有些力不從心。

有一個竅門是我們可以通過執行幾個APIs方法來顯著提升繪制操作的性能。前面有提到過,非可見的UI元件進行繪制更新會導緻Overdraw。例如Nav Drawer從前置可見的Activity滑出之後,如果還繼續繪制那些在Nav Drawer裡面不可見的UI元件,這就導緻了Overdraw。為了解決這個問題,Android系統會通過避免繪制那些完全不可見的元件來盡量減少Overdraw。那些Nav Drawer裡面不可見的View就不會被執行浪費資源。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

但是不幸的是,對于那些過于複雜的自定義的View(重寫了onDraw方法),Android系統無法檢測具體在onDraw裡面會執行什麼操作,系統無法監控并自動優化,也就無法避免Overdraw了。但是我們可以通過canvas.clipRect()來幫助系統識别那些可見的區域。這個方法可以指定一塊矩形區域,隻有在這個區域内才會被繪制,其他的區域會被忽視。這個API可以很好的幫助那些有多組重疊元件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約CPU與GPU資源,在clipRect區域之外的繪制指令都不會被執行,那些部分内容在矩形區域内的元件,仍然會得到繪制。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

除了clipRect方法之外,我們還可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,進而跳過那些非矩形區域内的繪制操作。做了那些優化之後,我們可以通過上面介紹的Show GPU Overdraw來檢視效果。

(h)Memory Churn and performance

雖然Android有自動管理記憶體的機制,但是對記憶體的不恰當使用仍然容易引起嚴重的性能問題。在同一幀裡面建立過多的對象是件需要特别引起注意的事情。

Android系統裡面有一個Generational Heap Memory的模型,系統會根據記憶體中不同的記憶體資料類型分别執行不同的GC操作。例如,最近剛配置設定的對象會放在Young Generation區域,這個區域的對象通常都是會快速被建立并且很快被銷毀回收的,同時這個區域的GC操作速度也是比Old Generation區域的GC操作速度更快的。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

除了速度差異之外,執行GC操作的時候,所有線程的任何操作都會需要暫停,等待GC操作完成之後,其他操作才能夠繼續運作。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

通常來說,單個的GC并不會占用太多時間,但是大量不停的GC操作則會顯著占用幀間隔時間(16ms)。如果在幀間隔時間裡面做了過多的GC操作,那麼自然其他類似計算,渲染等操作的可用時間就變得少了。

導緻GC頻繁執行有兩個原因:

Memory Churn記憶體抖動,記憶體抖動是因為大量的對象被建立又在短時間内馬上被釋放。

瞬間産生大量的對象會嚴重占用Young Generation的記憶體區域,當達到閥值,剩餘空間不夠的時候,也會觸發GC。即使每次配置設定的對象占用了很少的記憶體,但是他們疊加在一起會增加Heap的壓力,進而觸發更多其他類型的GC。這個操作有可能會影響到幀率,并使得使用者感覺到性能問題。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

解決上面的問題有簡潔直覺方法,如果你在Memory Monitor裡面檢視到短時間發生了多次記憶體的漲跌,這意味着很有可能發生了記憶體抖動。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

同時我們還可以通過Allocation Tracker來檢視在短時間内,同一個棧中不斷進出的相同對象。這是記憶體抖動的典型信号之一。

當你大緻定位問題之後,接下去的問題修複也就顯得相對直接簡單了。例如,你需要避免在for循環裡面配置設定對象占用記憶體,需要嘗試把對象的建立移到循環體之外,自定義View中的onDraw方法也需要引起注意,每次螢幕發生繪制以及動畫執行過程中,onDraw方法都會被調用到,避免在onDraw方法裡面執行複雜的操作,避免建立對象。對于那些無法避免需要建立對象的情況,我們可以考慮對象池模型,通過對象池來解決頻繁建立與銷毀的問題,但是這裡需要注意結束使用之後,需要手動釋放對象池中的對象。

(i)Garbage Collection in Android

JVM的回收機制給開發人員帶來很大的好處,不用時刻處理對象的配置設定與回收,可以更加專注于更加進階的代碼實作。相比起Java,C與C++等語言具備更高的執行效率,他們需要開發人員自己關注對象的配置設定與回收,但是在一個龐大的系統當中,還是免不了經常發生部分對象忘記回收的情況,這就是記憶體洩漏。

原始JVM中的GC機制在Android中得到了很大程度上的優化。Android裡面是一個三級Generation的記憶體模型,最近配置設定的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最後到Permanent Generation區域。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

每一個級别的記憶體區域都有固定的大小,此後不斷有新的對象被配置設定到此區域,當這些對象總的大小快達到這一級别記憶體區域的閥值時,會觸發GC的操作,以便騰出空間來存放其他新的對象。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

前面提到過每次GC發生的時候,所有的線程都是暫停狀态的。GC所占用的時間和它是哪一個Generation也有關系,Young Generation的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和目前Generation中的對象數量有關,周遊查找20000個對象比起周遊50個對象自然是要慢很多的。

雖然Google的工程師在盡量縮短每次GC所花費的時間,但是特别注意GC引起的性能問題還是很有必要。如果不小心在最小的for循環單元裡面執行了建立對象的操作,這将很容易引起GC并導緻性能問題。通過Memory Monitor我們可以檢視到記憶體的占用情況,每一次瞬間的記憶體降低都是因為此時發生了GC操作,如果在短時間内發生大量的記憶體上漲與降低的事件,這說明很有可能這裡有性能問題。我們還可以通過Heap and Allocation Tracker工具來檢視此時記憶體中配置設定的到底有哪些對象。

(j)Performance Cost of Memory Leaks

雖然Java有自動回收的機制,可是這不意味着Java中不存在記憶體洩漏的問題,而記憶體洩漏會很容易導緻嚴重的性能問題。

記憶體洩漏指的是那些程式不再使用的對象無法被GC識别,這樣就導緻這個對象一直留在記憶體當中,占用了寶貴的記憶體空間。顯然,這還使得每級Generation的記憶體區域可用空間變小,GC就會更容易被觸發,進而引起性能問題。

尋找記憶體洩漏并修複這個漏洞是件很棘手的事情,你需要對執行的代碼很熟悉,清楚的知道在特定環境下是如何運作的,然後仔細排查。例如,你想知道程式中的某個activity退出的時候,它之前所占用的記憶體是否有完整的釋放幹淨了?首先你需要在activity處于前台的時候使用Heap Tool擷取一份目前狀态的記憶體快照,然後你需要建立一個幾乎不這麼占用記憶體的空白activity用來給前一個Activity進行跳轉,其次在跳轉到這個空白的activity的時候主動調用System.gc()方法來確定觸發一個GC操作。最後,如果前面這個activity的記憶體都有全部正确釋放,那麼在空白activity被啟動之後的記憶體快照中應該不會有前面那個activity中的任何對象了。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

如果你發現在空白activity的記憶體快照中有一些可疑的沒有被釋放的對象存在,那麼接下去就應該使用Alocation Track Tool來仔細查找具體的可疑對象。我們可以從空白activity開始監聽,啟動到觀察activity,然後再回到空白activity結束監聽。這樣操作以後,我們可以仔細觀察那些對象,找出記憶體洩漏的真兇。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

(k)Memory Performance

通常來說,Android對GC做了大量的優化操作,雖然執行GC操作的時候會暫停其他任務,可是大多數情況下,GC操作還是相對很安靜并且高效的。但是如果我們對記憶體的使用不恰當,導緻GC頻繁執行,這樣就會引起不小的性能問題。

為了尋找記憶體的性能問題,Android Studio提供了工具來幫助開發者。

Memory Monitor:檢視整個app所占用的記憶體,以及發生GC的時刻,短時間内發生大量的GC操作是一個危險的信号。

Allocation Tracker:使用此工具來追蹤記憶體的配置設定,前面有提到過。

Heap Tool:檢視目前記憶體快照,便于對比分析哪些對象有可能是洩漏了的,請參考前面的Case。

(l)Tool - Memory Monitor

Android Studio中的Memory Monitor可以很好的幫助我們檢視程式的記憶體使用情況。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)
android app性能優化大彙總(google官方Android性能優化典範 - 第1季)
android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

(m)Battery Performance

電量其實是目前手持裝置最寶貴的資源之一,大多數裝置都需要不斷的充電來維持繼續使用。不幸的是,對于開發者來說,電量優化是他們最後才會考慮的的事情。但是可以确定的是,千萬不能讓你的應用成為消耗電量的大戶。

Purdue University研究了最受歡迎的一些應用的電量消耗,平均隻有30%左右的電量是被程式最核心的方法例如繪制圖檔,擺放布局等等所使用掉的,剩下的70%左右的電量是被上報資料,檢查位置資訊,定時檢索背景廣告資訊所使用掉的。如何平衡這兩者的電量消耗,就顯得非常重要了。

有下面一些措施能夠顯著減少電量的消耗:

我們應該盡量減少喚醒螢幕的次數與持續的時間,使用WakeLock來處理喚醒的問題,能夠正确執行喚醒操作并根據設定及時關閉操作進入睡眠狀态。

某些非必須馬上執行的操作,例如上傳歌曲,圖檔處理等,可以等到裝置處于充電狀态或者電量充足的時候才進行。

觸發網絡請求的操作,每次都會保持無線信号持續一段時間,我們可以把零散的網絡請求打包進行一次操作,避免過多的無線信号引起的電量消耗。關于網絡請求引起無線信号的電量消耗,還可以參考這裡http://hukai.me/android-training-course-in-chinese/connectivity/efficient-downloads/efficient-network-access.html

我們可以通過手機設定選項找到對應App的電量消耗統計資料。我們還可以通過Battery Historian Tool來檢視詳細的電量消耗。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

如果發現我們的App有電量消耗過多的問題,我們可以使用JobScheduler API來對一些任務進行定時處理,例如我們可以把那些任務重的操作等到手機處于充電狀态,或者是連接配接到WiFi的時候來處理。 關于JobScheduler的更多知識可以參考http://hukai.me/android-training-course-in-chinese/background-jobs/scheduling/index.html

(n)Understanding Battery Drain on Android

高效的保留更多的電量與不斷促使使用者使用你的App會消耗電量,這是沖突的選擇題。不過我們可以使用一些更好的辦法來平衡兩者。

假設你的手機裡面裝了大量的社交類應用,即使手機處于待機狀态,也會經常被這些應用喚醒用來檢查同步新的資料資訊。Android會不斷關閉各種硬體來延長手機的待機時間,首先螢幕會逐漸變暗直至關閉,然後CPU進入睡眠,這一切操作都是為了節約寶貴的電量資源。但是即使在這種睡眠狀态下,大多數應用還是會嘗試進行工作,他們将不斷的喚醒手機。一個最簡單的喚醒手機的方法是使用PowerManager.WakeLock的API來保持CPU工作并防止螢幕變暗關閉。這使得手機可以被喚醒,執行工作,然後回到睡眠狀态。知道如何擷取WakeLock是簡單的,可是及時釋放WakeLock也是非常重要的,不恰當的使用WakeLock會導緻嚴重錯誤。例如網絡請求的資料傳回時間不确定,導緻本來隻需要10s的事情一直等待了1個小時,這樣會使得電量白白浪費了。這也是為何使用帶逾時參數的wakelock.acquice()方法是很關鍵的。但是僅僅設定逾時并不足夠解決問題,例如設定多長的逾時比較合适?什麼時候進行重試等等?

解決上面的問題,正确的方式可能是使用非精準定時器。通常情況下,我們會設定一個時間進行某個操作,但是動态修改這個時間也許會更好。例如,如果有另外一個程式需要比你設定的時間晚5分鐘喚醒,最好能夠等到那個時候,兩個任務捆綁一起同時進行,這就是非精确定時器的核心工作原理。我們可以定制計劃的任務,可是系統如果檢測到一個更好的時間,它可以推遲你的任務,以節省電量消耗。

android app性能優化大彙總(google官方Android性能優化典範 - 第1季)

這正是JobScheduler API所做的事情。它會根據目前的情況與任務,組合出理想的喚醒時間,例如等到正在充電或者連接配接到WiFi的時候,或者集中任務一起執行。我們可以通過這個API實作很多免費的排程算法。

從Android 5.0開始釋出了Battery History Tool,它可以檢視程式被喚醒的頻率,又誰喚醒的,持續了多長的時間,這些資訊都可以擷取到。

請關注程式的電量消耗,使用者可以通過手機的設定選項觀察到那些耗電量大戶,并可能決定解除安裝他們。是以盡量減少程式的電量消耗是非常有必要的。

<a href="http://www.cnblogs.com/yezhennan/p/5431738.html">http://www.cnblogs.com/yezhennan/p/5431738.html</a>

本文轉自 知止内明 51CTO部落格,原文連結:http://blog.51cto.com/357712148/2046041,如需轉載請自行聯系原作者

繼續閱讀