Android性能模式 第三季

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

1. Fun with ArrayMaps

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

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

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

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

這裏寫圖片描述

這裏寫圖片描述

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

這裏寫圖片描述

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

    // ArrayMap
    for (int i = 0; i < map.size(); i++) {
        Object keyObj = map.keyAt(i);
        Object valueObj = map.valueAt(i);
        ......
    }

    // HashMap
    for (Iterator<Object> it = map.keySet().iterator(); it.hasNext(); ) {
        Object object = it.hasNext();
        ......
    }

前面演示了很多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行爲的差異:

這裏寫圖片描述

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

注:上圖實際上是有問題的,SparseBoolMap,SparseIntMap和SparseLongMap的key的類型都是int。

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.

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

    public static final int VALUE1 = 1;
    public static final int VALUE2 = 2;
    public static final int VALUE3 = 3;

    int func(int value) {
        switch (value) {
            case VALUE1:
                return -1;
            case VALUE2:
                return -2;
            case VALUE3:
                return -3;
        }
        return 0;
    }

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

    public static enum Value {
        VALUE1,
        VALUE2,
        VALUE3
    }

    int func(Value value) {
        switch (value) {
            case VALUE1:
                return -1;
            case VALUE2:
                return -2;
            case VALUE3:
                return -3;
        }
        return 0;
    }

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

這裏寫圖片描述

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

5. Trimming and Sharing Memory

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

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

這裏寫圖片描述

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

Paramete Note
TRIM_MEMORY_RUNNING_MODERATE this is your first warning
TRIM_MEMORY_RUNNING_LOW This is like yellow light. It is your second warning to begin to trim resource to improve performance.
TRIM_MEMORY_RUNNING_CRITICAL This is like red light. If you keep on executing without clearing up memory resource, the system is going to begin killing background processes to get more memory for you. Unfortunately that will lower the performance of your application.
TRIM_MEMORY_UI_HIDDEN Your application was just moved off the screen, so this is a good time to release large UI resources. Now your application is on the list off cached applications. If there are memory problems, your processes may be killed.
TRIM_MEMORY_BACKGROUND Being a background app – release as much as you can so that your app can resume faster than a pure restart.
TRIM_MEMORY_MODERATE You are a background app, but in the middle
TRIM_MEMORY_COMPLETE You are a background app, but about to be killed.

onTrimMemory()的回調可以發生在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同時還和其他內部對象也有可能保持引用關係。當屏幕發生旋轉的時候,activity很容易發生泄漏,這樣的話,裏面的view也會發生泄漏。Activity以及view的泄漏是非常嚴重的,爲了避免出現泄漏,請特別留意以下的規則:

1) 避免使用異步回調

這裏寫圖片描述

2) 避免使用Static對象

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

這裏寫圖片描述

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

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

這裏寫圖片描述

7. Location & Battery Drain

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

這裏寫圖片描述

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

這裏寫圖片描述

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

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

這裏寫圖片描述

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

這裏寫圖片描述

8. Double Layout Taxation

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

這裏寫圖片描述

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

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

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

這裏寫圖片描述

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

這裏寫圖片描述

9. Network Performance 101

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

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

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

這裏寫圖片描述

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

這裏寫圖片描述

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

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

這裏寫圖片描述

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

這裏寫圖片描述

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

這裏寫圖片描述

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

2) 壓縮傳輸數據

10. Effective Network Batching

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

這裏寫圖片描述

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

這裏寫圖片描述

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

11. Optimizing Network Request Frequencies

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

12. Effective Prefetching

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

這裏寫圖片描述

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

這裏寫圖片描述

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

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