Android學習之 記憶體管理機制與應用記憶體優化

小呂-ICE發表於2015-01-05
    Random Access Memory(RAM)在任何軟體開發環境中都是一個很寶貴的資源。這一點在實體記憶體通常很有限的移動作業系統上,顯得尤為突出。儘管Android的Dalvik虛擬機器扮演了常規的垃圾回收的角色,但這並不意味著你可以忽視app的記憶體分配與釋放的時機與地點。於大多數apps來說,Dalvik的GC會自動把離開活動執行緒的物件進行回收。

一、Android系統是如何管理記憶體的
    Android並沒有提供記憶體的交換區(Swap space),但是它有使用paging(記憶體分頁)memory-mapping(mmapping)的機制來管理記憶體。這意味著任何你修改的記憶體(無論是通過分配新的物件還是訪問到mmaped pages的內容)都會貯存在RAM中,而且不能被paged out。因此唯一完整釋放記憶體的方法是釋放那些你可能hold住的物件的引用,這樣使得它能夠被GC回收。只有一種例外是:如果系統想要在其他地方reuse這個物件。

1> 記憶體共享
   Android通過下面幾個方式在不同的Process中來共享RAM:
   1.每一個app的process都是從同一個被叫做Zygote的程式中fork出來的。Zygote程式在系統啟動並且載入通用的framework的程式碼與資源之後開始啟動。為了啟動一個新的程式程式,系統會fork Zygote程式生成一個新的process,然後在新的process中載入並執行app的程式碼。這使得大多數的RAM pages被用來分配給framework的程式碼與資源,並在應用的所有程式中進行共享。
   2.大多數static的資料被mmapped到一個程式中。這不僅僅使得同樣的資料能夠在程式間進行共享,而且使得它能夠在需要的時候被paged out。例如下面幾種static的資料:
     ① Dalvik code (by placing it in a pre-linked .odex file for direct mmapping
     ② App resources (by designing the resource table to be a structure that can be mmapped and by aligning the zip entries of the APK)
     ③ Traditional project elements like native code in .so files.
   3.在許多地方,Android通過顯式的分配共享記憶體區域(例如ashmem或者gralloc)來實現一些動態RAM區域的能夠在不同程式間的共享。例如,window surfaces在app與screen compositor之間使用共享的記憶體,cursor buffers在content provider與client之間使用共享的記憶體。

2> 分配與回收記憶體
   這裡有下面幾點關於Android如何分配與回收記憶體的事實:
   1.每一個程式的Dalvik heap都有一個限制的虛擬記憶體範圍。這就是邏輯上講的heap size,它可以隨著需要進行增長,但是會有一個系統為它所定義的上限。
   2.邏輯上講的heap size和實際物理上使用的記憶體數量是不等的,Android會計算一個叫做Proportional Set Size(PSS)的值,它記錄了那些和其他程式進行共享的記憶體大小。(假設共享記憶體大小是10M,一共有20個Process在共享使用,根據權重,可能認為其中有0.3M才能真正算是你的程式所使用的)
   3.Dalvik heap與邏輯上的heap size不吻合,這意味著Android並不會去做heap中的碎片整理用來關閉空閒區域。Android僅僅會在heap的尾端出現不使用的空間時才會做收縮邏輯heap size大小的動作。但是這並不是意味著被heap所使用的實體記憶體大小不能被收縮。在垃圾回收之後,Dalvik會遍歷heap並找出不使用的pages,然後使用madvise把那些pages返回給kernal。因此,成對的allocations與deallocations大塊的資料可以使得實體記憶體能夠被正常的回收。然而,回收碎片化的記憶體則會使得效率低下很多,因為那些碎片化的分配頁面也許會被其他地方所共享到。

3> 限制應用的記憶體
   為了維持多工的功能環境,Android為每一個app都設定了一個硬性的heap size限制。準確的heap size限制隨著不同裝置的不同RAM大小而各有差異。如果你的app已經到了heap的限制大小並且再嘗試分配記憶體的話,會引起OutOfMemoryError的錯誤。

在一些情況下,你也許想要查詢當前裝置的heap size限制大小是多少,然後決定cache的大小。可以通過getMemoryClass()來查詢。這個方法會返回一個整數,表明你的app heap size限制是多少megabates。

4> 切換應用
   Android並不會在使用者切換不同應用時候做交換記憶體的操作。Android會把那些不包含foreground元件的程式放到LRU cache中。例如,當使用者剛開始啟動了一個應用,這個時候為它建立了一個程式,但是當使用者離開這個應用,這個程式並沒有離開。系統會把這個程式放到cache中,如果使用者後來回到這個應用,這個程式能夠被resued,從而實現app的快速切換。

如果你的應用有一個當前並不需要使用到的被快取的程式,它被保留在記憶體中,這會對系統的整個效能有影響。因此當系統開始進入低記憶體狀態時,它會由系統根據LRU的規則與其他因素選擇殺掉某些程式。

二、該如何管理/優化你的應用記憶體
   你應該在開發過程的每一個階段都考慮到RAM的有限性,甚至包括在開發開始之前的設計階段。有許多種設計與實現方式,他們有著不同的效率,儘管是對同樣一種技術的不斷組合與演變。

為了使得你的應用效率更高,你應該在設計與實現程式碼時,遵循下面的技術要點:
1、珍惜Services資源
   如果你的app需要在後臺使用service,除非它被觸發執行一個任務,否則其他時候都應該是非執行狀態。同樣需要注意當這個service已經完成任務後停止service失敗引起的洩漏。

當你啟動一個service,系統會傾向為了這個Service而一直保留它的Process。這使得process的執行代價很高,因為系統沒有辦法把Service所佔用的RAM讓給其他元件或者被Paged out。這減少了系統能夠存放到LRU快取當中的process數量,它會影響app之間的切換效率。它甚至會導致系統記憶體使用不穩定,從而無法繼續Hold住 所有目前正在執行的Service。

限制你的service的最好辦法是使用IntentService, 它會在處理完扔給它的intent任務之後儘快結束自己

2、當你的應用UI隱藏時 釋放記憶體 
   當使用者切換到其它app並且你的app UI不再可見時,你應該釋放應用檢視所佔的資源。在這個時候釋放UI資源可以顯著的增加系統cached process的能力,它會對使用者的質量體驗有著直接的影響。

為了能夠接收到使用者離開你的UI時的通知,你需要實現Activtiy類裡面的onTrimMemory()回撥方法。你應該使用這個方法來監聽到TRIM_MEMORY_UI_HIDDEN級別, 它意味著你的UI已經隱藏,你應該釋放那些僅僅被你的UI使用的資源。

請注意:你的app僅僅會在所有UI元件的被隱藏的時候接收到onTrimMemory()的回撥並帶有引數TRIM_MEMORY_UI_HIDDEN。這與onStop()的回撥是不同的,onStop會在activity的例項隱藏時會執行,例如當使用者從你的app的某個activity跳轉到另外一個activity時onStop會被執行。因此你應該實現onStop回撥,所以說你雖然實現了onStop()去釋放 activity 的資源例如網路連線或者未註冊的廣播接收者, 但是應該直到你收到 onTrimMemory(TRIM_MEMORY_UI_HIDDEN)才去釋放檢視資源否則不應該釋放檢視所佔用的資源。這確保了使用者從其他activity切回來時,你的UI資源仍然可用,並且可以迅速恢復activity。

3、當記憶體緊張時 釋放部分記憶體 
   在你的app生命週期的任何階段,onTrimMemory回撥方法同樣可以告訴你整個裝置的記憶體資源已經開始緊張。你應該根據onTrimMemory方法中的記憶體級別來進一步決定釋放哪些資源。

   ① TRIM_MEMORY_RUNNING_MODERATE:你的app正在執行並且不會被列為可殺死的。但是裝置正執行於低記憶體狀態下,系統開始啟用殺死LRU Cache中的Process的機制。
   ② TRIM_MEMORY_RUNNING_LOW:你的app正在執行且沒有被列為可殺死的。但是裝置正執行於更低記憶體的狀態下,你應該釋放不用的資源用來提升系統效能,這會直接影響了你的app的效能。
   ③ TRIM_MEMORY_RUNNING_CRITICAL:你的app仍在執行,但是系統已經把LRU Cache中的大多數程式都已經殺死,因此你應該立即釋放所有非必須的資源。如果系統不能回收到足夠的RAM數量,系統將會清除所有的LRU快取中的程式,並且開始殺死那些之前被認為不應該殺死的程式,例如那個程式包含了一個執行中的Service。

同樣,當你的app程式正在被cached時,你可能會接受到從onTrimMemory()中返回的下面的值之一:
   ① TRIM_MEMORY_BACKGROUND: 系統正執行於低記憶體狀態並且你的程式正處於LRU快取名單中最不容易殺掉的位置。儘管你的app程式並不是處於被殺掉的高危險狀態,系統可能已經開始殺掉LRU快取中的其他程式了。你應該釋放那些容易恢復的資源,以便於你的程式可以保留下來,這樣當使用者回退到你的app的時候才能夠迅速恢復。
   ② TRIM_MEMORY_MODERATE: 系統正執行於低記憶體狀態並且你的程式已經已經接近LRU名單的中部位置。如果系統開始變得更加記憶體緊張,你的程式是有可能被殺死的。
   ③ TRIM_MEMORY_COMPLETE: 系統正執行與低記憶體的狀態並且你的程式正處於LRU名單中最容易被殺掉的位置。你應該釋放任何不影響你的app恢復狀態的資源。

因為onTrimMemory()的回撥是在API 14才被加進來的,對於老的版本,你可以使用onLowMemory)回撥來進行相容。onLowMemory相當與TRIM_MEMORY_COMPLETE。

Note: 當系統開始清除LRU快取中的程式時,儘管它首先按照LRU的順序來操作,但是它同樣會考慮程式的記憶體使用量。因此消耗越少的程式則越容易被留下來。

4、檢查你應該使用多少記憶體
   通過呼叫getMemoryClass()來獲取你的app的可用heap大小。
   在一些特殊的情景下,你可以通過在manifest的application標籤下新增largeHeap=true的屬性來宣告一個更大的heap空間。如果你這樣做,你可以通過getLargeMemoryClass())來獲取到一個更大的heap size。

然而,能夠獲取更大heap的設計本意是為了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因為你需要使用大量的記憶體而去請求一個大的heap size。只有當你清楚的知道哪裡會使用大量的記憶體並且為什麼這些記憶體必須被保留時才去使用large heap. 因此請儘量少使用large heap。使用額外的記憶體會影響系統整體的使用者體驗,並且會使得GC的每次執行時間更長。在任務切換時,系統的效能會變得大打折扣。

5、避免Bitmap的浪費
   當你載入一個bitmap時,僅僅需要保留適配當前螢幕裝置解析度的資料即可,如果原圖高於你的裝置解析度,需要做縮小的動作。請記住,增加bitmap的尺寸會對記憶體呈現出2次方的增加,因為X與Y都在增加。
   使用完Bitmap後 確定不會再使用 則需要注意Bitmap的記憶體回收(recycle())處理。

6、使用優化的資料容器
   利用Android Framework裡面優化過的容器類,例如SparseArray,SparseBooleanArray, 與LongSparseArray. 通常的HashMap的實現方式更加消耗記憶體,因為它需要一個額外的例項物件來記錄Mapping操作。另外,SparseArray更加高效在於他們避免了對key與value的autobox自動裝箱,並且避免了裝箱後的解箱。

7、請注意記憶體開銷(資源的開啟關閉 如資料庫查詢過程遊標的關閉、IO流的關閉、執行緒池的使用)

8、請注意程式碼編寫優化 (Java程式碼的效能優化)

9、為序列化的資料使用nano protobufs  【小呂 暫未接觸過這點,】

10、Avoid dependency injection frameworks
   使用類似Guice或者RoboGuice等framework injection包是很有效的,因為他們能夠簡化你的程式碼。然而,那些框架會通過掃描你的程式碼執行許多初始化的操作,這會導致你的程式碼需要大量的RAM來map程式碼。但是mapped pages會長時間的被保留在RAM中。

11、謹慎使用external libraries
   很多External library的程式碼都不是為行動網路環境而編寫的,在移動客戶端則顯示的效率不高。至少,當你決定使用一個external library的時候,你應該針對行動網路做繁瑣的porting與maintenance的工作。

12、優化整體效能
   官方有列出許多優化整個app效能的文章:Best Practices for Performance. 這篇文章就是其中之一。有些文章是講解如何優化app的CPU使用效率,有些是如何優化app的記憶體使用效率。
其他 如 layout佈局優化。同樣還應該關注lint工具所提出的建議,進行優化。

13、使用ProGuard來剔除不需要的程式碼
   ProGuard能夠通過移除不需要的程式碼,重新命名類,域與方法等方對程式碼進行壓縮,優化與混淆。使用ProGuard可以是的你的程式碼更加緊湊,這樣能夠使用更少mapped程式碼所需要的RAM。

14、對最終的APK使用zipalign
   在編寫完所有程式碼,並通過編譯系統生成APK之後,你需要使用zipalign對APK進行重新校準。如果你不做這個步驟,會導致你的APK需要更多的RAM,因為一些類似圖片資源的東西不能被mapped。

15、分析你的RAM使用情況
   一旦你獲取到一個相對穩定的版本後,需要分析你的app整個生命週期內使用的記憶體情況,並進行優化,更多細節請參考Investigating Your RAM Usage.

16、使用多程式(注意是多程式,別看成多執行緒了啊!!!)
   如果合適的話,有一個更高階的技術可以幫助你的app管理記憶體使用:通過把你的app元件切分成多個元件,執行在不同的程式中。這個技術必須謹慎使用,大多數app都不應該執行在多個程式中。因為如果使用不當,它會顯著增加記憶體的使用,而不是減少。當你的app需要在後臺執行與前臺一樣的大量的任務的時候,可以考慮使用這個技術。

一個典型的例子是建立一個可以長時間後臺播放的Music Player。如果整個app執行在一個程式中,當後臺播放的時候,前臺的那些UI資源也沒有辦法得到釋放。類似這樣的app可以切分成2個程式:一個用來操作UI,另外一個用來後臺的Service.

在各個應用的 manifest 檔案中為各個元件申明 android:process 屬性就可以分隔為不同的程式.例如你可以指定你一執行的服務從主程式中分隔成一個新的程式來並取名為"background"(當然名字可以任意取)
<service android:name=".PlaybackService"
         android:process=":background" />

程式名字必須以冒號開頭":"以確保該程式屬於你應用中的私有程式。


原文:  http://developer.android.com/training/articles/memory.html


相關文章