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的執行情況,以便幫助定位性能問題。