Android效能優化典範第4季的課程學習筆記終於在2015年的最後一天完成了,文章共17個段落,包含的內容大致有:優化網路請求的行為,優化安裝包的資原始檔,優化資料傳輸的效率,效能優化的幾大基礎原理等等。因為學習認知水平有限,肯定存在不少理解偏差甚至錯誤的地方,請多多交流指正!
1)Cachematters for networking
想要使得Android系統上的網路訪問操作更加的高效就必須做好網路資料的快取。這是提高網路訪問效能最基礎的步驟之一。從手機的快取中直接讀取資料肯定比從網路上獲取資料要更加的便捷高效,特別是對於那些會被頻繁訪問到的資料,需要把這些資料快取到裝置上,以便更加快速的進行訪問。
Android系統上關於網路請求的Http Response Cache是預設關閉的,這樣會導致每次即使請求的資料內容是一樣的也會需要重複被呼叫執行,效率低下。我們可以通過下面的程式碼示例開啟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 Cloud Message來對更新的資料進行推送。
- 然後在某些必須做同步的場景下,需要避免使用固定的間隔頻率來進行更新操作,我們應該在返回的資料無更新的時候,使用雙倍的間隔時間來進行下一次同步。
- 最後更進一步,我們還可以通過判斷當前裝置的狀態來決定同步的頻率,例如判斷裝置處於休眠,運動等不同的狀態設計各自不同時間間隔的同步頻率。
另外,我們還可以通過判斷裝置是否連線上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 Cloud Messaging)
- 如果已經事先知道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 Play Service的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佈局檔案會在編譯的階段被轉換成更加複雜的格式,具備更加高效的執行效能與更高的記憶體使用效率。
下面介紹三個資料序列化的候選方案:
- Protocal 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的執行情況,以便幫助定位效能問題。