原文鏈接:http://hukai.me/android-performance-patterns-season-4/
前言
本季內容大致有:優化網絡請求的行爲,優化安裝包的資源文件,優化數據傳輸的效率,性能優化的幾大基礎原理等等。
1)Cachematters for networking
想要使得Android系統上的網絡訪問操作更加的高效就必須做好網絡數據的緩存。這是提高網絡訪問性能最基礎的步驟之一。從手機的緩存中直接讀取數據肯定比從網絡上獲取數據要更加的便捷高效,特別是對於那些會被頻繁訪問到的數據,需要把這些數據緩存到設備上,以便更加快速的進行訪問。
Android系統上關於網絡請求的Http ResponseCache是默認關閉的,這樣會導致每次即使請求的數據內容是一樣的也會需要重複被調用執行,效率低下。我們可以通過下面的代碼示例開啓HttpResponseCache。
開啓Http Response Cache之後,Http操作相關的返回數據就會緩存到文件系統上,不僅僅是主程序自己編寫的網絡請求相關的數據會被緩存,另外引入的library庫中的網絡相關的請求數據也會被緩存到這個Cache中。網絡請求的場景有可以是普通的http請求,也可以打開某個URL去獲取數據,如下圖所示:
我們有兩種方式來清除HttpResponseCache的緩存數據:第一種方式是緩存溢出的時候刪除最舊最老的文件,第二種方式是通過Http返回Header中的Cache-Control字段來進行控制的。如下圖所示:
通常來說,HttpResponseCache會緩存所有的返回信息,包括實際的數據與Header的部分.一般情況下,這個Cache會自動根據協議返回Cache-Control的內容與當前緩存的數據量來決定哪些數據應該繼續保留,哪些數據應該刪除。但是在一些極端的情況下,例如服務器返回的數據沒有設置Cache廢棄的時間,或者是本地的Cache文件系統與返回的緩存數據有衝突,或者是某些特殊的網絡環境導致HttpResponseCache工作異常,在這些情況下就需要我們自己來實現Http的緩存Cache。
實現自定義的http緩存,需要解決兩個問題:第一個是實現一個DiskCacheManager,另外一個是制定Cache的緩存策略。關於DiskCacheManager,我們可以擴展Android系統提供的DiskLruCache來實現。而Cache的緩存策略,相對來說複雜一些,我們可能需要把部分JSON數據設計成不能緩存的,另外一些JSON數據設計成可以緩存幾天的,把縮略圖設計成緩存一兩天的等等,爲不同的數據類型根據他們的使用特點制定不同的緩存策略。
想要比較好的實現這兩件事情,如果全部自己從頭開始寫會比較繁瑣複雜,所幸的是,有不少著名的開源框架幫助我們快速的解決了那些問題。我們可以使用Volly,okHTTP,Picasso來實現網絡緩存。
實現好網絡緩存之後,我們可以使用Android Studio裏面的Network Traffic Tools來查看網絡數據的請求與返回情況,另外我們還可以使用AT&T ARO工具來抓取網絡數據包進行分析查看。
2)Optimizing Network Request Frequencies
應用程序的一個基礎功能是能夠保持確保界面上呈現的信息是即時最新的,例如呈現最新的新聞,天氣,信息流等等信息。但是,過於頻繁的促使手機客戶端應用去同步最新的服務器數據會對性能產生很大的負面影響,不僅僅使得CPU不停的在工作,內存,網絡流量,電量等等都會持續的被消耗,所以在進行網絡請求操作的時候一定要避免多度同步操作。
退到後臺的應用爲了能夠在切換回前臺的時候呈現最新的數據,會偷偷在後臺不停的做同步的操作。這種行爲會帶來很嚴重的問題,首先因爲網絡請求的行爲異常的耗電,其次不停的進行網絡同步會耗費很多帶寬流量。
爲了能夠儘量的減少不必要的同步操作,我們需要遵守下面的一些規則:
· 首先我們要對網絡行爲進行分類,區分需要立即更新數據的行爲和其他可以進行延遲的更新行爲,爲不同的場景進行差異化處理。
· 其次要避免客戶端對服務器的輪詢操作,這樣會浪費很多的電量與帶寬流量。解決這個問題,我們可以使用Google CloudMessage來對更新的數據進行推送。
· 然後在某些必須做同步的場景下,需要避免使用固定的間隔頻率來進行更新操作,我們應該在返回的數據無更新的時候,使用雙倍的間隔時間來進行下一次同步。
· 最後更進一步,我們還可以通過判斷當前設備的狀態來決定同步的頻率,例如判斷設備處於休眠,運動等不同的狀態設計各自不同時間間隔的同步頻率。
另外,我們還可以通過判斷設備是否連接上WiFi,是否正在充電來決定更新的頻率。爲了能夠方便的實現這個功能,Android爲我們提供了GCMNetworkManager來判斷設備當下的狀態,從而設計更加高效的網絡同步操作,如下圖所示:
3)Effective Prefetching
關於提升網絡操作的性能,除了避免頻繁的網絡同步操作之外,還可以使用捆綁批量訪問的方式來減少訪問的頻率,爲了達到這個目的,我們就需要了解Prefetching。
舉個例子,在某個場景下,一開始發出了網絡請求得到了某張圖片,隔了10s之後,發出第二次請求想要拿到另外一張圖片,再隔了6s發出第三張圖片的網絡請求。這會導致設備的無線蜂窩一直處於高消耗的狀態。Prefetching就是預先判定那些可能馬上就會使用到的網絡資源,捆綁一起集中進行網絡請求。這樣能夠極大的減少電量的消耗,提升設備的續航時間。
使用Prefetching的難點在於如何判斷事先獲取的數據量到底是多少,如果預取的數據量偏少,那麼就起不到什麼效果,但是如果預取過多,又可能導致訪問的時間過長。
那麼問題來了,到底預取多少才比較合適呢?一個比較普適的規則是,在3G網絡下可以預取1-5Mb的數據量,或者是按照提前預期後續1-2分鐘的數據作爲基線標準。在實際的操作當中,我們還需要考慮當前的網絡速度來決定預取的數據量,例如在同樣的時間下,4G網絡可以獲取到12張圖片的數據,而2G網絡則只能拿到3張圖片的數據。所以,我們還需要把當前的網絡環境情況添加到設計預取數據量的策略當中去。判斷當前設備的狀態與網絡情況,可以使用前面提到過的GCMNetworkManager。
4)Adapting to Latency
網絡延遲通常來說很容易被用戶察覺到,嚴重的網絡延遲會對用戶體驗造成很大的影響,用戶很容易抱怨應用程序寫的不好。一個典型的網絡操作行爲,通常包含以下幾個步驟:首先手機端發起網絡請求,到達網絡服務運營商的基站,再轉移到服務提供者的服務器上,經過解碼之後,接着訪問本地的存儲數據庫,獲取到數據之後,進行編碼,最後按照原來傳遞的路徑逐層返回。如下圖所示:
在上面的網絡請求鏈路當中的任何一個環節都有可能導致嚴重的延遲,成爲性能瓶頸,但是這些環節可能出現的問題,客戶端應用是無法進行調節控制的,應用能夠做的就只是根據當前的網絡環境選擇當下最佳的策略來降低出現網絡延遲的概率。主要的實施步驟有兩步:第1步檢測收集當前的網絡環境信息,第2步根據當前收集到的信息進行網絡請求行爲的調整。
關於第1步檢測當前的網絡環境,我們可以使用系統提供的API來獲取到相關的信息,如下圖所示:
通過上面的示例,我們可以獲取到移動網絡的詳細子類型,例如4G(LTE),3G等等,詳細分類見下圖,獲取到詳細的移動網絡類型之後,我們可以根據當前網絡的速率來調整網絡請求的行爲:
關於第2步根據收集到的信息進行策略的調整,通常來說,我們可以把網絡請求延遲劃分爲三檔:例如把網絡延遲小於60ms的劃分爲GOOD,大於220ms的劃分爲BAD,介於兩者之間的劃分爲OK(這裏的60ms,220ms會需要根據不同的場景提前進行預算推測)。如果網絡延遲屬於GOOD的範疇,我們就可以做更多比較激進的預取數據的操作,如果網絡延遲屬於BAD的範疇,我們就應該考慮把當下的網絡請求操作Hold住等待網絡狀況恢復到GOOD的狀態再進行處理。
前面提到說60ms,220ms是需要提前自己預測的,可是預測的工作相當複雜。首先針對不同的機器與網絡環境,網絡延遲的三檔閾值都不太一樣,出現的概率也不盡相同,我們會需要針對這些不同的用戶與設備選擇不同的閾值進行差異化處理:
Android官方爲了幫助我們設計自己的網絡請求策略,爲我們提供了模擬器的網絡流量控制功能來對實際環境進行模擬測量,或者還可以使用AT&T提供的AT&T Network Attenuator來幫助預估網絡延遲。
5)Minimizing Asset Payload
爲了能夠減小網絡傳輸的數據量,我們需要對傳輸的數據做壓縮的處理,這樣能夠提高網絡操作的性能。首先不同的網絡環境,下載速度以及網絡延遲是存在差異的,如下圖所示:
如果我們選擇在網速更低的網絡環境下進行數據傳輸,這就意味着需要執行更長的時間,而更長的網絡操作行爲,會導致電量消耗更加嚴重。另外傳輸的數據如果不做壓縮處理,也同樣會增加網絡傳輸的時間,消耗更多的電量。不僅如此,未經過壓縮的數據,也會消耗更多的流量,使得用戶需要付出更多的流量費。
通常來說,網絡傳輸數據量的大小主要由兩部分組成:圖片與序列化的數據,那麼我們需要做的就是減少這兩部分的數據傳輸大小,分下面兩個方面來討論。
· A)首先需要做的是減少圖片的大小,選擇合適的圖片保存格式是第一步。下圖展示了PNG,JPEG,WEBP三種主流格式在佔用空間與圖片質量之間的對比:
對於JPEG與WEBP格式的圖片,不同的清晰度對佔用空間的大小也會產生很大的影響,適當的減少JPG Quality,可以大大的縮小圖片佔用的空間大小。
另外,我們需要爲不同的使用場景提供當前場景下最合適的圖片大小,例如針對全屏顯示的情況我們會需要一張清晰度比較高的圖片,而如果只是顯示爲縮略圖的形式,就只需要服務器提供一個相對清晰度低很多的圖片即可。服務器應該支持到爲不同的使用場景分別準備多套清晰度不一樣的圖片,以便在對應的場景下能夠獲取到最適合自己的圖片。這雖然會增加服務端的工作量,可是這個付出卻十分值得!
· B)其次需要做的是減少序列化數據的大小。JSON與XML爲了提高可讀性,在文件中加入了大量的符號,空格等等字符,而這些字符對於程序來說是沒有任何意義的。我們應該使用Protocal Buffers,Nano-Proto-Buffers,FlatBuffer來減小序列化的數據的大小。
Android系統爲我們提供了工具來查看網絡傳輸的數據情況,打開Android Studio的Monitor,裏面有網絡訪問的模塊。或者是打開AT&T提供的ARO工具來查看網絡請求狀態。
6)Service Performance Patterns
Service是Android程序裏面最常用的基礎組件之一,但是使用Service很容易引起電量的過度消耗以及系統資源的未及時釋放。學會在何時啓用Service以及使用何種方式殺掉Service就顯得十分有必要了。
簡要過一下Service的特性:Service和UI沒有關聯,Service的創建,執行,銷燬Service都是需要佔用系統時間和內存的。另外Service是默認運行在UI線程的,這意味着Service可能會影響到系統的流暢度。
使用Service應該遵循下面的一些規則:
· 避免錯誤的使用Service,例如我們不應該使用Service來監聽某些事件的變化,不應該搞一個Service在後臺對服務器不斷的進行輪詢(應該使用Google CloudMessaging)
· 如果已經事先知道Service裏面的任務應該執行在後臺線程(非默認的主線程)的時候,我們應該使用IntentService或者結合HanderThread,AsycnTask Loader實現的Service。
Android系統爲我們提供了以下的一些異步相關的工具類
· GCM
· BroadcastReciever
· LocalBroadcastReciever
· WakefulBroadcastReciver
· HandlerThreads
· AsyncTaskLoaders
· IntentService
如果使用上面的諸多方案還是無法替代普通的Service,那麼需要注意的就是如何正確的關閉Service。
· 普通的Started Service,需要通過stopSelf()來停止Service
· 另外一種Bound Service,會在其他組件都unBind之後自動關閉自己
把上面兩種Service進行合併之後,我們可以得到如下圖所示的Service(相關知識,還可以參考http://hukai.me/android-notes-services/, http://hukai.me/android-notes-bound-services/)
7)Removing unused code
使用第三方庫(library)可以在不用自己編寫大量代碼的前提下幫助我們解決一些難題,節約大量的時間,但是這些引入的第三方庫很可能會導致主程序代碼臃腫冗餘。
如果我們處在人力,財力都相對匱乏的情況下,通常會傾向大量使用第三方庫來幫助編寫應用程序。這其實是無可厚非的,那些著名的第三方庫的可行性早就被很多應用所採用並實踐證明過。但是這裏面存在的問題是,如果我們因爲只需要某個library的一小部分功能而把整個library都導入自己的項目,這就會引起代碼臃腫。一旦發生代碼臃腫,用戶就會下載到安裝包偏大的應用程序,另外因爲代碼臃腫,還很有可能會超過單個編譯文件只能有65536個方法的上限。解決這個問題的辦法是使用MultiDex的方案,可是這實在是無奈之舉,原則上,我們還是應該儘量避免出現這種情況。
Android爲我們提供了Proguard的工具來幫助應用程序對代碼進行瘦身,優化,混淆的處理。它會幫助移除那些沒有使用到的代碼,還可以對類名,方法名進行混淆處理以避免程序被反編譯。舉個例子,Google I/O 2015這個應用使用了大量的library,沒有經過Proguard處理之前編譯出來的包是8.4Mb大小,經過處理之後的包僅僅是4.1Mb大小。
使用Proguard相當的簡單,只需要在build.gradle文件中配置minifEnable爲true即可,如下圖所示:
但是Proguard還是不足夠聰明到能夠判斷哪些類,哪些方法是不能夠被混淆的,針對這些情況,我們需要手動的把這些需要保留的類名與方法名添加到Proguard的配置文件中,如下圖所示:
在使用library的時候,需要特別注意這些library在proguard配置上的說明文檔,我們需要把這些配置信息添加到自己的主項目中。關於Proguard的詳細說明,請看官方文檔http://developer.android.com/tools/help/proguard.html
8)Removing unused resources
減少APK安裝包的大小也是Android程序優化中很重要的一個方面,我們不應該給用戶下載到一個臃腫的安裝包。假設這樣一個場景,我們引入了Google PlayService的library,是想要使用裏面的Maps的功能,但是裏面的登入等等其他功能是不需要的,可是這些功能相關的代碼與圖片資源,佈局資源如果也被引入我們的項目,這樣就會導致我們的程序安裝包臃腫。
所幸的是,我們可以使用Gradle來幫助我們分析代碼,分析引用的資源,對於那些沒有被引用到的資源,會在編譯階段被排除在APK安裝包之外,要實現這個功能,對我們來說僅僅只需要在build.gradle文件中配置shrinkResource爲true就好了,如下圖所示:
爲了輔助gradle對資源進行瘦身,或者是某些時候的特殊需要,我們可以通過tools:keep或者是tools:discard標籤來實現對特定資源的保留與廢棄,如下圖所示:
Gradle目前無法對values,drawable等根據運行時來決定使用的資源進行優化,對於這些資源,需要我們自己來確保資源不會有冗餘。
9)Perf Theory: Caching
當我們討論性能優化的時候,緩存是最常見最有效的策略之一。無論是爲了提高CPU的計算速度還是提高數據的訪問速度,在絕大多數的場景下,我們都會使用到緩存。關於緩存是如何提高效率的,這裏就不贅述了。
那麼在什麼地方,在何時應該利用好緩存來提高效率呢?請看下面的例子,很明顯的演示了在某些細節上是如何利用緩存的原理來提高代碼的執行效率的:
類似上面的例子採用緩存原理的地方還有很多,例如緩存到內存裏面的圖片資源,網絡請求返回數據的緩存等等。總之,使用緩存就是爲了減少不必要的操作,儘量複用已有的對象來提高效率。
10)Perf Theory: Approximation(近似法)
很多時候,我們都需要學會在性能更優與體驗更好之間做一定的權衡取捨。爲了獲取更好的表現性能,我們可能會需要犧牲一些用戶體驗,例如把某些細節做刪除或者是降級處理以便有更好的性能。例如,導航類的應用,如果在導航期間是不停的執行定位的操作,這樣能夠很及時的獲取到最新的位置信息以及當下位置相關的其他提示信息,但是這樣會導致網絡流量以及手機電量的過度消耗。所以我們可以做一定的降級處理,每隔固定的一段時間纔去獲取一次位置信息,損失一點及時性來換取更長的續航時間。
還有很多地方都會用到近似法則來優化程序的性能,例如使用一張比較接近實際大小的圖片來替代原圖,換取更快的加載速度。所以對於那些對計算結果要求不需要十分精確的場景,我們可以使用近似法則來提高程序的性能。
11)Perf Theory: Culling(遴選,挑選)
在以前的性能優化課程裏面,我們知道可以通過減少Overdraw來提高程序的渲染性能(主要手段有移除非必須的background,減少重疊的佈局,使用clipRect來提高自定義View的繪製性能),今天在這裏要介紹的另外一個提高性能的方法是逐步對數據進行過濾篩選,減小搜索的數據集,以此提高程序的執行性能。例如我們需要搜索到居住在某個地方,年齡是多少,符合某些特定條件的候選人,就可以通過逐層過濾篩選的方式來提高後續搜索的執行效率。
12)Perf Theory: Threading
使用多線程併發處理任務,從某種程度上可以快速提高程序的執行性能。對於Android程序來說,主線程通常也成爲UI線程,需要處理UI的渲染,響應用戶的操作等等。對於那些可能影響到UI線程的任務都需要特別留意是否有必要放到其他的線程來進行處理。如果處理不當,很有可能引起程序ANR。關於多線程的使用建議,可以參考官方的培訓課程http://developer.android.com/training/best-background.html
13)Perf Theory: Batching
關於Batching,在前幾季的性能優化課程裏面也不止一次提到,下面使用一張圖演示下Batching的原理:
網絡請求的批量執行是另外一個比較適合說明batching使用場景的例子,因爲每次發起網絡請求都相對來說比較耗時耗電,如果能夠做到批量一起執行,可以大大的減少電量的消耗。
14)Serialization performance
數據的序列化是程序代碼裏面必不可少的組成部分,當我們討論到數據序列化的性能的時候,需要了解有哪些候選的方案,他們各自的優缺點是什麼。首先什麼是序列化?用下面的圖來解釋一下:
數據序列化的行爲可能發生在數據傳遞過程中的任何階段,例如網絡傳輸,不同進程間數據傳遞,不同類之間的參數傳遞,把數據存儲到磁盤上等等。通常情況下,我們會把那些需要序列化的類實現Serializable接口(如下圖所示),但是這種傳統的做法效率不高,實施的過程會消耗更多的內存。
但是我們如果使用GSON庫來處理這個序列化的問題,不僅僅執行速度更快,內存的使用效率也更高。Android的XML佈局文件會在編譯的階段被轉換成更加複雜的格式,具備更加高效的執行性能與更高的內存使用效率。
下面介紹三個數據序列化的候選方案:
· Buffers:強大,靈活,但是對內存的消耗會比較大,並不是移動終端上的最佳選擇。
· Nano-Proto-Buffers:基於Protocal,爲移動終端做了特殊的優化,代碼執行效率更高,內存使用效率更佳。
· FlatBuffers:這個開源庫最開始是由Google研發的,專注於提供更優秀的性能。
上面這些方案在性能方面的數據對比如下圖所示:
爲了避免序列化帶來的性能問題,我們其實可以考慮使用SharedPreference或者SQLite來存儲那些數據,避免需要先把那些複雜的數據進行序列化的操作。
15)Smaller Serialized Data
數據呈現的順序以及結構會對序列化之後的空間產生不小的影響。通常來說,一般的數據序列化的過程如下圖所示:
上面的過程,存在兩個弊端,第一個是重複的屬性名稱:
另外一個是GZIP沒有辦法對上面的數據進行更加有效的壓縮,假如相似數據間隔了32k的數據量,這樣GZIP就無法進行更加有效的壓縮:
但是我們稍微改變下數據的記錄方式,就可以得到佔用空間更小的數據,如下圖所示:
通過優化,至少有三方面的性能提升,如下圖所示:
1)減少了重複的屬性名:
2)使得GZIP的壓縮效率更高:
3)同樣的數據類型可以批量優化:
16)Caching UI data
如今絕大多數的應用界面上呈現的數據都依賴於網絡請求返回的結果,如何做到在網絡數據返回之前避免呈現一個空白的等待頁面呢(當然這裏說的是非首次冷啓動的情況)?這就會涉及到如何緩存UI界面上的數據。
緩存UI界面上的數據,可以採用方案有存儲到文件系統,Preference,SQLite等等,做了緩存之後,這樣就可以在請求數據返回結果之前,呈現給用戶舊的數據,而不是使用正在加載的方式讓用戶什麼數據都看不到,當然在請求網絡最新數據的過程中,需要有正在刷新的提示。至於到底選擇哪個方案來對數據進行緩存,就需要根據具體情況來做選擇了。
17)CPU Frequency Scaling
調節CPU的頻率會執行的性能產生較大的影響,爲了最大化的延長設備的續航時間,系統會動態調整CPU的頻率,頻率越高執行代碼的速度自然就越快。
Android系統會在電量消耗與表現性能之間不斷的做權衡,當有需要的時候會迅速調整CPU的頻率到一個比較高負荷的狀態,當程序不需要高性能的時候就會降低頻率來確保更長的續航時間。
Android系統檢測到需要調整CPU的頻率到CPU頻率真的達到對應頻率會需要花費大概20ms的時間,在此期間很有可能會因爲CPU頻率不夠而導致代碼執行偏慢。
我們可以使用Systrace工具來導出CPU的執行情況,以便幫助定位性能問題。