Android性能優(yōu)化典范 - 第3季
AUG 11TH, 2015|COMMENTS
Android性能優(yōu)化典范的課程最近更新到第三季了霞赫,這次一共12個短視頻課程,包括的內容大致有:更高效的ArrayMap容器,使用Android系統(tǒng)提供的特殊容器來避免自動裝箱默勾,避免使用枚舉類型番电,注意onLowMemory與onTrimMemory的回調,避免內存泄漏默蚌,高效的位置更新操作冻晤,重復layout操作的性能影響,以及使用Batching绸吸,Prefetching優(yōu)化網絡請求鼻弧,壓縮傳輸數據等等使用技巧。下面是對這些課程的總結摘要锦茁,認知有限攘轩,理解偏差的地方請多多交流指正!
1)Fun with ArrayMaps
程序內存的管理是否合理高效對應用的性能有著很大的影響码俩,有的時候對容器的使用不當也會導致內存管理效率低下度帮。Android為移動操作系統(tǒng)特意編寫了一些更加高效的容器,例如SparseArray,今天要介紹的是一個新的容器笨篷,叫做ArrayMap瞳秽。
我們經常會使用到HashMap這個容器,它非常好用率翅,但是卻很占用內存练俐。下圖演示了HashMap的簡要工作原理:
為了解決HashMap更占內存的弊端,Android提供了內存效率更高的ArrayMap冕臭。它內部使用兩個數組進行工作腺晾,其中一個數組記錄key hash過后的順序列表,另外一個數組按key的順序記錄Key-Value值辜贵,如下圖所示:
當你想獲取某個value的時候悯蝉,ArrayMap會計算輸入key轉換過后的hash值,然后對hash數組使用二分查找法尋找到對應的index念颈,然后我們可以通過這個index在另外一個數組中直接訪問到需要的鍵值對泉粉。如果在第二個數組鍵值對中的key和前面輸入的查詢key不一致,那么就認為是發(fā)生了碰撞沖突榴芳。為了解決這個問題嗡靡,我們會以該key為中心點,分別上下展開窟感,逐個去對比查找讨彼,直到找到匹配的值。如下圖所示:
隨著數組中的對象越來越多柿祈,查找訪問單個對象的花費也會跟著增長哈误,這是在內存占用與訪問時間之間做權衡交換。
既然ArrayMap中的內存占用是連續(xù)不間斷的躏嚎,那么它是如何處理插入與刪除操作的呢蜜自?請看下圖所示,演示了Array的特性:
很明顯卢佣,ArrayMap的插入與刪除的效率是不夠高的重荠,但是如果數組的列表只是在一百這個數量級上,則完全不用擔心這些插入與刪除的效率問題虚茶。HashMap與ArrayMap之間的內存占用效率對比圖如下:
與HashMap相比戈鲁,ArrayMap在循環(huán)遍歷的時候也更加簡單高效,如下圖所示:
前面演示了很多ArrayMap的優(yōu)點嘹叫,但并不是所有情況下都適合使用ArrayMap婆殿,我們應該在滿足下面2個條件的時候才考慮使用ArrayMap:
對象個數的數量級最好是千以內
數據組織形式包含Map結構
我們需要學會在特定情形下選擇相對更加高效的實現方式。
2)Beware Autoboxing
有時候性能問題也可能是因為那些不起眼的小細節(jié)引起的罩扇,例如在代碼中不經意的“自動裝箱”婆芦。我們知道基礎數據類型的大小:boolean(8 bits), int(32 bits), float(32 bits),long(64 bits)寞缝,為了能夠讓這些基礎數據類型在大多數Java容器中運作癌压,會需要做一個autoboxing的操作伏社,轉換成Boolean矾屯,Integer浮创,Float等對象,如下演示了循環(huán)操作的時候是否發(fā)生autoboxing行為的差異:
Autoboxing的行為還經常發(fā)生在類似HashMap這樣的容器里面被啼,對HashMap的增刪改查操作都會發(fā)生了大量的autoboxing的行為。
為了避免這些autoboxing帶來的效率問題棠枉,Android特地提供了一些如下的Map容器用來替代HashMap浓体,不僅避免了autoboxing,還減少了內存占用:
3)SparseArray Family Ties
為了避免HashMap的autoboxing行為辈讶,Android系統(tǒng)提供了SparseBoolMap命浴,SparseIntMap,SparseLongMap贱除,LongSparseMap等容器生闲。關于這些容器的基本原理請參考前面的ArrayMap的介紹,另外這些容器的使用場景也和ArrayMap一致月幌,需要滿足數量級在千以內碍讯,數據組織形式需要包含Map結構。
4)The price of ENUMs
在StackOverFlow等問答社區(qū)常常出現關于在Android系統(tǒng)里面使用枚舉類型的性能討論扯躺,關于這一點捉兴,Android官方的Training課程里面有下面這樣一句話:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
關于enum的效率,請看下面的討論录语。假設我們有這樣一份代碼倍啥,編譯之后的dex大小是2556 bytes,在此基礎之上澎埠,添加一些如下代碼虽缕,這些代碼使用普通static常量相關作為判斷值:
增加上面那段代碼之后,編譯成dex的大小是2680 bytes失暂,相比起之前的2556 bytes只增加124 bytes彼宠。假如換做使用enum,情況如下:
使用enum之后的dex大小是4188 bytes弟塞,相比起2556增加了1632 bytes凭峡,增長量是使用static int的13倍。不僅僅如此决记,使用enum摧冀,運行時還會產生額外的內存占用,如下圖所示:
Android官方強烈建議不要在Android程序里面使用到enum。
5)Trimming and Sharing Memory
Android系統(tǒng)的一大特色是多任務索昂,用戶可以隨意在不同的app之間進行快速切換建车。為了確保你的應用在這種復雜的多任務環(huán)境中正常運行,我們需要了解下面的知識椒惨。
為了讓background的應用能夠迅速的切換到forground缤至,每一個background的應用都會占用一定的內存。Android系統(tǒng)會根據當前的系統(tǒng)內存使用情況康谆,決定回收部分background的應用內存领斥。如果background的應用從暫停狀態(tài)直接被恢復到forground,能夠獲得較快的恢復體驗沃暗,如果background應用是從Kill的狀態(tài)進行恢復月洛,就會顯得稍微有點慢。
Android系統(tǒng)提供了一些回調來通知應用的內存使用情況孽锥,通常來說嚼黔,當所有的background應用都被kill掉的時候,forground應用會收到onLowMemory()的回調惜辑。在這種情況下唬涧,需要盡快釋放當前應用的非必須內存資源,從而確保系統(tǒng)能夠穩(wěn)定繼續(xù)運行韵丑。Android系統(tǒng)還提供了onTrimMemory()的回調爵卒,當系統(tǒng)內存達到某些條件的時候,所有正在運行的應用都會收到這個回調撵彻,同時在這個回調里面會傳遞以下的參數钓株,代表不同的內存使用情況,下圖介紹了各種不同的回調參數:
關于每個參數的更多介紹陌僵,請參考這里http://hukai.me/android-training-managing_your_app_memory/轴合,另外onTrimMemory()的回調可以發(fā)生在Application,Activity碗短,Fragment受葛,Service,Content Provider偎谁。
從Android 4.4開始总滩,ActivityManager提供了isLowRamDevice()的API,通常指的是Heap Size低于512M或者屏幕大小<=800*480的設備巡雨。
6)DO NOT LEAK VIEWS
內存泄漏的概念闰渔,下面一張圖演示下:
通常來說,View會保持Activity的引用铐望,Activity同時還和其他內部對象也有可能保持引用關系冈涧。當屏幕發(fā)生旋轉的時候茂附,activity很容易發(fā)生泄漏,這樣的話督弓,里面的view也會發(fā)生泄漏营曼。Activity以及view的泄漏是非常嚴重的,為了避免出現泄漏愚隧,請?zhí)貏e留意以下的規(guī)則:
6.1)避免使用異步回調
異步回調被執(zhí)行的時間不確定蒂阱,很有可能發(fā)生在activity已經被銷毀之后,這不僅僅很容易引起crash狂塘,還很容易發(fā)生內存泄露蒜危。
6.2)避免使用Static對象
因為static的生命周期過長,使用不當很可能導致leak睹耐,在Android中應該盡量避免使用static對象。
6.3)避免把View添加到沒有清除機制的容器里面
假如把view添加到WeakHashMap部翘,如果沒有執(zhí)行清除操作硝训,很可能會導致泄漏。
7)Location & Battery Drain
開啟定位功能是一個相對來說比較耗電的操作新思,通常來說窖梁,我們會使用類似下面這樣的代碼來發(fā)出定位請求:
上面演示中有一個方法是setInterval()指的意思是每隔多長的時間獲取一次位置更新,時間相隔越短夹囚,自然花費的電量就越多纵刘,但是時間相隔太長,又無法及時獲取到更新的位置信息荸哟。其中存在的一個優(yōu)化點是假哎,我們可以通過判斷返回的位置信息是否相同,從而決定設置下次的更新間隔是否增加一倍鞍历,通過這種方式可以減少電量的消耗舵抹,如下圖所示:
在位置請求的演示代碼中還有一個方法是setFastestInterval(),因為整個系統(tǒng)中很可能存在其他的應用也在請求位置更新劣砍,那些應用很有可能設置的更新間隔時間很短惧蛹,這種情況下,我們就可以通過setFestestInterval的方法來過濾那些過于頻繁的更新刑枝。
通過GPS定位服務相比起使用網絡進行定位更加的耗電香嗓,但是也相對更加精準一些,他們的圖示關系如下:
為了提供不同精度的定位需求装畅,同時屏蔽實現位置請求的細節(jié)靠娱,Android提供了下面4種不同精度與耗電量的參數給應用進行設置調用,應用只需要決定在適當的場景下使用對應的參數就好了洁灵,通過LocationRequest.setPriority()方法傳遞下面的參數就好了饱岸。
8)Double Layout Taxation
布局中的任何一個View一旦發(fā)生一些屬性變化掺出,都可能引起很大的連鎖反應。例如某個button的大小突然增加一倍苫费,有可能會導致兄弟視圖的位置變化汤锨,也有可能導致父視圖的大小發(fā)生改變。當大量的layout()操作被頻繁調用執(zhí)行的時候百框,就很可能引起丟幀的現象闲礼。
例如,在RelativeLayout中铐维,我們通常會定義一些類似alignTop柬泽,alignBelow等等屬性,如圖所示:
為了獲得視圖的準確位置嫁蛇,需要經過下面幾個階段锨并。首先子視圖會觸發(fā)計算自身位置的操作,然后RelativeLayout使用前面計算出來的位置信息做邊界的調整的操作睬棚,如下面兩張圖所示:
經歷過上面2個步驟第煮,relativeLayout會立即觸發(fā)第二次layout()的操作來確定所有子視圖的最終位置與大小信息。
除了RelativeLayout會發(fā)生兩次layout操作之外抑党,LinearLayout也有可能觸發(fā)兩次layout操作包警,通常情況下LinearLayout只會發(fā)生一次layout操作,可是一旦調用了measureWithLargetChild()方法就會導致觸發(fā)兩次layout的操作底靠。另外害晦,通常來說,GridLayout會自動預處理子視圖的關系來避免兩次layout暑中,可是如果GridLayout里面的某些子視圖使用了weight等復雜的屬性壹瘟,還是會導致重復的layout操作。
如果只是少量的重復layout本身并不會引起嚴重的性能問題痒芝,但是如果它們發(fā)生在布局的根節(jié)點俐筋,或者是ListView里面的某個ListItem,這樣就會引起比較嚴重的性能問題严衬。如下圖所示:
我們可以使用Systrace來跟蹤特定的某段操作澄者,如果發(fā)現了疑似丟幀的現象,可能就是因為重復layout引起的请琳。通常我們無法避免重復layout粱挡,在這種情況下,我們應該盡量保持View Hierarchy的層級比較淺俄精,這樣即使發(fā)生重復layout询筏,也不會因為布局的層級比較深而增大了重復layout的倍數。另外還有一點需要特別注意竖慧,在任何時候都請避免調用requestLayout()的方法嫌套,因為一旦調用了requestLayout逆屡,會導致該layout的所有父節(jié)點都發(fā)生重新layout的操作。
9)Network Performance 101
在性能優(yōu)化第一季與第二季的課程里面都介紹過踱讨,網絡請求的操作是非常耗電的魏蔗,其中在移動蜂窩網絡情況下執(zhí)行網絡數據的請求則尤其比較耗電。關于如何減少移動網絡下的網絡請求的耗電量痹筛,有兩個重要的原則需要遵守:第一個是減少移動網絡被激活的時間與次數莺治,第二個是壓縮傳輸數據。
9.1)減少移動網絡被激活的時間與次數
通常來說帚稠,發(fā)生網絡行為可以劃分為如下圖所示的三種類型谣旁,一個是用戶主動觸發(fā)的請求,另外被動接收服務器的返回數據滋早,最后一個是數據上報榄审,行為上報,位置更新等等自定義的后臺操作杆麸。
我們絕對堅決肯定不應該使用Polling(輪詢)的方式去執(zhí)行網絡請求瘟判,這樣不僅僅會造成嚴重的電量消耗,還會浪費許多網絡流量角溃,例如:
Android官方推薦使用Google Cloud Messaging(在大陸,然并卵)篮撑,這個框架會幫助把更新的數據推送給手機客戶端减细,效率極高!我們應該遵循下面的規(guī)則來處理數據同步的問題:
首先赢笨,我們應該使用回退機制來避免固定頻繁的同步請求未蝌,例如,在發(fā)現返回數據相同的情況下茧妒,推遲下次的請求時間萧吠,如下圖所示:
其次,我們還可以使用Batching(批處理)的方式來集中發(fā)出請求桐筏,避免頻繁的間隔請求纸型,如下圖所示:
最后,我們還可以使用Prefetching(預取)的技術提前把一些數據拿到梅忌,避免后面頻繁再次發(fā)起網絡請求狰腌,如下圖所示:
Google Play Service中提供了一個叫做GCMNetworkManager的類來幫助我們實現上面的那些功能,我們只需要調用對應的API牧氮,設置一些簡單的參數琼腔,其余的工作就都交給Google來幫我們實現了。
9.2)壓縮傳輸數據
關于壓縮傳輸數據踱葛,我們可以學習以下的一些課程(真的夠喝好幾壺了):
CompressorHead:這系列的課程會介紹壓縮的基本概念以及一些常見的壓縮算法知識丹莲。
Image Compression:介紹關于圖片的壓縮知識光坝。
Texture Wranglin:介紹了游戲開發(fā)相關的知識。
Grabby:介紹了游戲開發(fā)相關的知識甥材。
10)Effective Network Batching
在性能優(yōu)化課程的第一季與第二季里面盯另,我們都有提到過下面這樣一個網絡請求與電量消耗的示意圖:
發(fā)起網絡請求與接收返回數據都是比較耗電的,在網絡硬件模塊被激活之后擂达,會繼續(xù)保持幾十秒的電量消耗土铺,直到沒有新的網絡操作行為之后,才會進入休眠狀態(tài)板鬓。前面一個段落介紹了使用Batching的技術來捆綁網絡請求悲敷,從而達到減少網絡請求的頻率。那么如何實現Batching技術呢俭令?通常來說后德,我們可以會把那些發(fā)出的網絡請求,先暫存到一個PendingQueue里面抄腔,等到條件合適的時候再觸發(fā)Queue里面的網絡請求瓢湃。
可是什么時候才算是條件合適了呢?最簡單粗暴的赫蛇,例如我們可以在Queue大小到10的時候觸發(fā)任務绵患,也可以是當手機開始充電,或者是手機連接到WiFi等情況下才觸發(fā)隊列中的任務悟耘。手動編寫代碼去實現這些功能會比較復雜繁瑣落蝙,Google為了解決這個問題,為我們提供了GCMNetworkManager來幫助實現那些功能暂幼,僅僅只需要調用API筏勒,設置觸發(fā)條件,然后就OK了旺嬉。
11)Optimizing Network Request Frequencies
前面的段落已經提到了應該減少網絡請求的頻率管行,這是為了減少電量的消耗。我們可以使用Batching邪媳,Prefetching的技術來避免頻繁的網絡請求捐顷。Google提供了GCMNetworkManager來幫助開發(fā)者實現那些功能,通過提供的API雨效,我們可以選擇在接入WiFi套菜,開始充電,等待移動網絡被激活等條件下再次激活網絡請求设易。
12)Effective Prefetching
假設我們有這樣的一個場景逗柴,最開始網絡請求了一張圖片,隔了10秒需要請求另外一張圖片顿肺,再隔6秒會請求第三張圖片戏溺,如下圖所示:
類似上面的情況會頻繁觸發(fā)網絡請求渣蜗,但是如果我們能夠預先請求后續(xù)可能會使用到網絡資源,避免頻繁的觸發(fā)網絡請求旷祸,這樣就能夠顯著的減少電量的消耗耕拷。可是預先獲取多少數據量是很值得考量的托享,因為如果預取數據量偏少骚烧,就起不到減少頻繁請求的作用,可是如果預取數據過多闰围,就會造成資源的浪費赃绊。
我們可以參考在WiFi,4G羡榴,3G等不同的網絡下設計不同大小的預取數據量碧查,也可以是按照圖片數量或者操作時間來作為閥值。這需要我們需要根據特定的場景校仑,不同的網絡情況設計合適的方案忠售。