Android性能優化典範 - 第3季

android_perf_patterns_season_3

Android性能優化典範的課程最近更新到第三季了,這次一共12個短視頻課程,包括的內容大致有:更高效的ArrayMap容器,使用Android系統提供的特殊容器來避免自動裝箱,避免使用枚舉類型,注意onLowMemory與onTrimMemory的回調,避免內存泄漏,高效的位置更新操作,重複layout操作的性能影響,以及使用Batching,Prefetching優化網絡請求,壓縮傳輸數據等等使用技巧。下面是對這些課程的總結摘要,認知有限,理解偏差的地方請多多交流指正!

1)Fun with ArrayMaps

程序內存的管理是否合理高效對應用的性能有着很大的影響,有的時候對容器的使用不當也會導致內存管理效率低下。Android爲移動操作系統特意編寫了一些更加高效的容器,例如SparseArray,今天要介紹的是一個新的容器,叫做ArrayMap

我們經常會使用到HashMap這個容器,它非常好用,但是卻很佔用內存。下圖演示了HashMap的簡要工作原理:

android_perf_3_arraymap_key_value

爲了解決HashMap更佔內存的弊端,Android提供了內存效率更高的ArrayMap。它內部使用兩個數組進行工作,其中一個數組記錄key hash過後的順序列表,另外一個數組按key的順序記錄Key-Value值,如下圖所示:

android_perf_3_arraymap_two_array

當你想獲取某個value的時候,ArrayMap會計算輸入key轉換過後的hash值,然後對hash數組使用二分查找法尋找到對應的index,然後我們可以通過這個index在另外一個數組中直接訪問到需要的鍵值對。如果在第二個數組鍵值對中的key和前面輸入的查詢key不一致,那麼就認爲是發生了碰撞衝突。爲了解決這個問題,我們會以該key爲中心點,分別上下展開,逐個去對比查找,直到找到匹配的值。如下圖所示:

android_perf_3_arraymap_binary_search

隨着數組中的對象越來越多,查找訪問單個對象的花費也會跟着增長,這是在內存佔用與訪問時間之間做權衡交換。

既然ArrayMap中的內存佔用是連續不間斷的,那麼它是如何處理插入與刪除操作的呢?請看下圖所示,演示了Array的特性:

android_perf_3_arraymap_del

android_perf_3_arraymap_add

很明顯,ArrayMap的插入與刪除的效率是不夠高的,但是如果數組的列表只是在一百這個數量級上,則完全不用擔心這些插入與刪除的效率問題。HashMap與ArrayMap之間的內存佔用效率對比圖如下:

android_perf_3_arraymap_memory_compare

與HashMap相比,ArrayMap在循環遍歷的時候也更加簡單高效,如下圖所示:

android_perf_3_arraymap_list

前面演示了很多ArrayMap的優點,但並不是所有情況下都適合使用ArrayMap,我們應該在滿足下面2個條件的時候才考慮使用ArrayMap:

  • 對象個數的數量級最好是千以內
  • 數據組織形式包含Map結構

我們需要學會在特定情形下選擇相對更加高效的實現方式。

2)Beware Autoboxing

有時候性能問題也可能是因爲那些不起眼的小細節引起的,例如在代碼中不經意的“自動裝箱”。我們知道基礎數據類型的大小:boolean(8 bits), int(32 bits), float(32 bits),long(64 bits),爲了能夠讓這些基礎數據類型在大多數Java容器中運作,會需要做一個autoboxing的操作,轉換成Boolean,Integer,Float等對象,如下演示了循環操作的時候是否發生autoboxing行爲的差異:

android_perf_3_autoboxing_for

android_perf_3_autoboxing_perf

Autoboxing的行爲還經常發生在類似HashMap這樣的容器裏面,對HashMap的增刪改查操作都會發生了大量的autoboxing的行爲。

android_perf_3_autoboxing_hashmap

爲了避免這些autoboxing帶來的效率問題,Android特地提供了一些如下的Map容器用來替代HashMap,不僅避免了autoboxing,還減少了內存佔用:

android_perf_3_autoboxing_sparse

3)SparseArray Family Ties

爲了避免HashMap的autoboxing行爲,Android系統提供了SparseBoolMap,SparseIntMap,SparseLongMap,LongSparseMap等容器。關於這些容器的基本原理請參考前面的ArrayMap的介紹,另外這些容器的使用場景也和ArrayMap一致,需要滿足數量級在千以內,數據組織形式需要包含Map結構。

4)The price of ENUMs

在StackOverFlow等問答社區常常出現關於在Android系統裏面使用枚舉類型的性能討論,關於這一點,Android官方的Training課程裏面有下面這樣一句話:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

android_perf_3_enum

關於enum的效率,請看下面的討論。假設我們有這樣一份代碼,編譯之後的dex大小是2556 bytes,在此基礎之上,添加一些如下代碼,這些代碼使用普通static常量相關作爲判斷值:

android_perf_3_enum_static

增加上面那段代碼之後,編譯成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。假如換做使用enum,情況如下:

android_perf_3_enum_enum

使用enum之後的dex大小是4188 bytes,相比起2556增加了1632 bytes,增長量是使用static int的13倍。不僅僅如此,使用enum,運行時還會產生額外的內存佔用,如下圖所示:

android_perf_3_enum_memory

Android官方強烈建議不要在Android程序裏面使用到enum。

5)Trimming and Sharing Memory

Android系統的一大特色是多任務,用戶可以隨意在不同的app之間進行快速切換。爲了確保你的應用在這種複雜的多任務環境中正常運行,我們需要了解下面的知識。

爲了讓background的應用能夠迅速的切換到forground,每一個background的應用都會佔用一定的內存。Android系統會根據當前的系統內存使用情況,決定回收部分background的應用內存。如果background的應用從暫停狀態直接被恢復到forground,能夠獲得較快的恢復體驗,如果background應用是從Kill的狀態進行恢復,就會顯得稍微有點慢。

android_perf_3_memory_bg_2_for

Android系統提供了一些回調來通知應用的內存使用情況,通常來說,當所有的background應用都被kill掉的時候,forground應用會收到onLowMemory()的回調。在這種情況下,需要儘快釋放當前應用的非必須內存資源,從而確保系統能夠穩定繼續運行。Android系統還提供了onTrimMemory()的回調,當系統內存達到某些條件的時候,所有正在運行的應用都會收到這個回調,同時在這個回調裏面會傳遞以下的參數,代表不同的內存使用情況,下圖介紹了各種不同的回調參數:

android_perf_3_memory_ontrimmemory

關於每個參數的更多介紹,請參考這裏 http://hukai.me/android-training-managing_your_app_memory/,另外onTrimMemory()的回調可以發生在Application,Activity,Fragment,Service,Content Provider。

從Android 4.4開始,ActivityManager提供了isLowRamDevice()的API,通常指的是Heap Size低於512M或者屏幕大小<=800*480的設備。

6)DO NOT LEAK VIEWS

內存泄漏的概念,下面一張圖演示下:

android_perf_3_leak

通常來說,View會保持Activity的引用,Activity同時還和其他內部對象也有可能保持引用關係。當屏幕發生旋轉的時候,activity很容易發生泄漏,這樣的話,裏面的view也會發生泄漏。Activity以及view的泄漏是非常嚴重的,爲了避免出現泄漏,請特別留意以下的規則:

6.1)避免使用異步回調

異步回調被執行的時間不確定,很有可能發生在activity已經被銷燬之後,這不僅僅很容易引起crash,還很容易發生內存泄露。

android_perf_3_leak_asyncback

6.2)避免使用Static對象

因爲static的生命週期過長,使用不當很可能導致leak,在Android中應該儘量避免使用static對象。

android_perf_3_leak_static

6.3)避免把View添加到沒有清除機制的容器裏面

假如把view添加到WeakHashMap,如果沒有執行清除操作,很可能會導致泄漏。

android_perf_3_leak_map

7)Location & Battery Drain

開啓定位功能是一個相對來說比較耗電的操作,通常來說,我們會使用類似下面這樣的代碼來發出定位請求:

android_perf_3_location_request

上面演示中有一個方法是setInterval()指的意思是每隔多長的時間獲取一次位置更新,時間相隔越短,自然花費的電量就越多,但是時間相隔太長,又無法及時獲取到更新的位置信息。其中存在的一個優化點是,我們可以通過判斷返回的位置信息是否相同,從而決定設置下次的更新間隔是否增加一倍,通過這種方式可以減少電量的消耗,如下圖所示:

android_perf_3_location_reduce

在位置請求的演示代碼中還有一個方法是setFastestInterval(),因爲整個系統中很可能存在其他的應用也在請求位置更新,那些應用很有可能設置的更新間隔時間很短,這種情況下,我們就可以通過setFestestInterval的方法來過濾那些過於頻繁的更新。

通過GPS定位服務相比起使用網絡進行定位更加的耗電,但是也相對更加精準一些,他們的圖示關係如下:

android_perf_3_location_provider

爲了提供不同精度的定位需求,同時屏蔽實現位置請求的細節,Android提供了下面4種不同精度與耗電量的參數給應用進行設置調用,應用只需要決定在適當的場景下使用對應的參數就好了,通過LocationRequest.setPriority()方法傳遞下面的參數就好了。

android_perf_3_location_accuracy

8)Double Layout Taxation

佈局中的任何一個View一旦發生一些屬性變化,都可能引起很大的連鎖反應。例如某個button的大小突然增加一倍,有可能會導致兄弟視圖的位置變化,也有可能導致父視圖的大小發生改變。當大量的layout()操作被頻繁調用執行的時候,就很可能引起丟幀的現象。

android_perf_3_layout_double

例如,在RelativeLayout中,我們通常會定義一些類似alignTop,alignBelow等等屬性,如圖所示:

android_perf_3_layout_relative

爲了獲得視圖的準確位置,需要經過下面幾個階段。首先子視圖會觸發計算自身位置的操作,然後RelativeLayout使用前面計算出來的位置信息做邊界的調整的操作,如下面兩張圖所示:

android_perf_3_layout_first_cal

android_perf_3_layout_first_adjust

經歷過上面2個步驟,relativeLayout會立即觸發第二次layout()的操作來確定所有子視圖的最終位置與大小信息。

除了RelativeLayout會發生兩次layout操作之外,LinearLayout也有可能觸發兩次layout操作,通常情況下LinearLayout只會發生一次layout操作,可是一旦調用了measureWithLargetChild()方法就會導致觸發兩次layout的操作。另外,通常來說,GridLayout會自動預處理子視圖的關係來避免兩次layout,可是如果GridLayout裏面的某些子視圖使用了weight等複雜的屬性,還是會導致重複的layout操作。

如果只是少量的重複layout本身並不會引起嚴重的性能問題,但是如果它們發生在佈局的根節點,或者是ListView裏面的某個ListItem,這樣就會引起比較嚴重的性能問題。如下圖所示:

android_perf_3_layout_hierachy

我們可以使用Systrace來跟蹤特定的某段操作,如果發現了疑似丟幀的現象,可能就是因爲重複layout引起的。通常我們無法避免重複layout,在這種情況下,我們應該儘量保持View Hierarchy的層級比較淺,這樣即使發生重複layout,也不會因爲佈局的層級比較深而增大了重複layout的倍數。另外還有一點需要特別注意,在任何時候都請避免調用requestLayout()的方法,因爲一旦調用了requestLayout,會導致該layout的所有父節點都發生重新layout的操作。

android_perf_3_layout_request

9)Network Performance 101

在性能優化第一季與第二季的課程裏面都介紹過,網絡請求的操作是非常耗電的,其中在移動蜂窩網絡情況下執行網絡數據的請求則尤其比較耗電。關於如何減少移動網絡下的網絡請求的耗電量,有兩個重要的原則需要遵守:第一個是減少移動網絡被激活的時間與次數,第二個是壓縮傳輸數據。

9.1)減少移動網絡被激活的時間與次數

通常來說,發生網絡行爲可以劃分爲如下圖所示的三種類型,一個是用戶主動觸發的請求,另外被動接收服務器的返回數據,最後一個是數據上報,行爲上報,位置更新等等自定義的後臺操作。

android_perf_3_network_three_type

我們絕對堅決肯定不應該使用Polling(輪詢)的方式去執行網絡請求,這樣不僅僅會造成嚴重的電量消耗,還會浪費許多網絡流量,例如:

android_perf_3_network_polling

Android官方推薦使用Google Cloud Messaging(在大陸,然並卵),這個框架會幫助把更新的數據推送給手機客戶端,效率極高!我們應該遵循下面的規則來處理數據同步的問題:

首先,我們應該使用回退機制來避免固定頻繁的同步請求,例如,在發現返回數據相同的情況下,推遲下次的請求時間,如下圖所示:

android_perf_3_network_backoff

其次,我們還可以使用Batching(批處理)的方式來集中發出請求,避免頻繁的間隔請求,如下圖所示:

android_perf_3_network_batching

最後,我們還可以使用Prefetching(預取)的技術提前把一些數據拿到,避免後面頻繁再次發起網絡請求,如下圖所示:

android_perf_3_network_prefetching

Google Play Service中提供了一個叫做GCMNetworkManager的類來幫助我們實現上面的那些功能,我們只需要調用對應的API,設置一些簡單的參數,其餘的工作就都交給Google來幫我們實現了。

android_perf_3_network_gcm_network_manager

9.2)壓縮傳輸數據

關於壓縮傳輸數據,我們可以學習以下的一些課程(真的夠喝好幾壺了):

10)Effective Network Batching

在性能優化課程的第一季與第二季裏面,我們都有提到過下面這樣一個網絡請求與電量消耗的示意圖:

android_perf_3_batching_networking

發起網絡請求與接收返回數據都是比較耗電的,在網絡硬件模塊被激活之後,會繼續保持幾十秒的電量消耗,直到沒有新的網絡操作行爲之後,纔會進入休眠狀態。前面一個段落介紹了使用Batching的技術來捆綁網絡請求,從而達到減少網絡請求的頻率。那麼如何實現Batching技術呢?通常來說,我們可以會把那些發出的網絡請求,先暫存到一個PendingQueue裏面,等到條件合適的時候再觸發Queue裏面的網絡請求。

android_perf_3_batching_queue

可是什麼時候纔算是條件合適了呢?最簡單粗暴的,例如我們可以在Queue大小到10的時候觸發任務,也可以是當手機開始充電,或者是手機連接到WiFi等情況下才觸發隊列中的任務。手動編寫代碼去實現這些功能會比較複雜繁瑣,Google爲了解決這個問題,爲我們提供了GCMNetworkManager來幫助實現那些功能,僅僅只需要調用API,設置觸發條件,然後就OK了。

11)Optimizing Network Request Frequencies

前面的段落已經提到了應該減少網絡請求的頻率,這是爲了減少電量的消耗。我們可以使用Batching,Prefetching的技術來避免頻繁的網絡請求。Google提供了GCMNetworkManager來幫助開發者實現那些功能,通過提供的API,我們可以選擇在接入WiFi,開始充電,等待移動網絡被激活等條件下再次激活網絡請求。

12)Effective Prefetching

假設我們有這樣的一個場景,最開始網絡請求了一張圖片,隔了10秒需要請求另外一張圖片,再隔6秒會請求第三張圖片,如下圖所示:

android_perf_3_prefetching

類似上面的情況會頻繁觸發網絡請求,但是如果我們能夠預先請求後續可能會使用到網絡資源,避免頻繁的觸發網絡請求,這樣就能夠顯著的減少電量的消耗。可是預先獲取多少數據量是很值得考量的,因爲如果預取數據量偏少,就起不到減少頻繁請求的作用,可是如果預取數據過多,就會造成資源的浪費。

android_perf_3_prefetching_over

我們可以參考在WiFi,4G,3G等不同的網絡下設計不同大小的預取數據量,也可以是按照圖片數量或者操作時間來作爲閥值。這需要我們需要根據特定的場景,不同的網絡情況設計合適的方案。


本文轉自:http://hukai.me/android-performance-patterns-season-3/


首發於CSDN:Android性能優化典範(三)


知識共享許可協議:本站作品由HuKai創作,採用知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章