天天看點

管理應用的記憶體

轉自:    http://hukai.me/android-training-course-in-chinese/performance/memory.html

Random Access Memory(RAM)在任何軟體開發環境中都是一個很寶貴的資源。這一點在實體記憶體通常很有限的移動作業系統上,顯得尤為突出。盡管Android的Dalvik虛拟機扮演了正常的垃圾回收的角色,但這并不意味着你可以忽視app的記憶體配置設定與釋放的時機與地點。

為了GC能夠從你的app中及時回收記憶體,你需要避免Memory Leaks(通常由于在全局成員變量中持有對象引用而導緻)并且在适當的時機(下面會講到的lifecycle callbacks)來釋放引用。對于大多數apps來說,Dalvik的GC會自動把離開活動線程的對象進行回收。

這篇文章會解釋Android是如何管理app的程序與記憶體配置設定,以及在開發Android應用的時候如何主動的減少記憶體的使用。關于Java的資源管理機制,請參加其它書籍或者線上材料。如果你正在尋找如何分析你的記憶體使用情況的文章,請參考這裡Investigating Your RAM Usage。

第1部分:Android是如何管理記憶體的

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

1)共享記憶體

Android通過下面幾個方式在不同的Process中來共享RAM:

  • 每一個app的process都是從同一個被叫做Zygote的程序中fork出來的。Zygote程序在系統啟動并且載入通用的framework的代碼與資源之後開始啟動。為了啟動一個新的程式程序,系統會fork Zygote程序生成一個新的process,然後在新的process中加載并運作app的代碼。這使得大多數的RAM pages被用來配置設定給framework的代碼與資源,并在應用的所有程序中進行共享。
  • 大多數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.
  • 在許多地方,Android通過顯式的配置設定共享記憶體區域(例如ashmem或者gralloc)來實作一些動态RAM區域的能夠在不同程序間的共享。例如,window surfaces在app與screen compositor之間使用共享的記憶體,cursor buffers在content provider與client之間使用共享的記憶體。

關于如何檢視app所使用的共享記憶體,請檢視Investigating Your RAM Usage

2)配置設定與回收記憶體

這裡有下面幾點關于Android如何配置設定與回收記憶體的事實:

  • 每一個程序的Dalvik heap都有一個限制的虛拟記憶體範圍。這就是邏輯上講的heap size,它可以随着需要進行增長,但是會有一個系統為它所定義的上限。
  • 邏輯上講的heap size和實際實體上使用的記憶體數量是不等的,Android會計算一個叫做Proportional Set Size(PSS)的值,它記錄了那些和其他程序進行共享的記憶體大小。(假設共享記憶體大小是10M,一共有20個Process在共享使用,根據權重,可能認為其中有0.3M才能真正算是你的程序所使用的)
  • 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的規則與其他因素選擇殺掉某些程序,為了保持你的程序能夠盡可能長久的被cached,請參考下面的章節學習何時釋放你的引用。

更對關于不在foreground的程序是Android是如何決定kill掉哪一類程序的問題,請參考Processes and Threads.

第2部分:你的應用該如何管理記憶體

你應該在開發過程的每一個階段都考慮到RAM的有限性,甚至包括在開發開始之前的設計階段。有許多種設計與實作方式,他們有着不同的效率,盡管是對同樣一種技術的不斷組合與演變。

為了使得你的應用效率更高,你應該在設計與實作代碼時,遵循下面的技術要點。

1)珍惜Services資源

如果你的app需要在背景使用service,除非它被觸發執行一個任務,否則其他時候都應該是非運作狀态。同樣需要注意當這個service已經完成任務後停止service失敗引起的洩漏。

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

限制你的service的最好辦法是使用IntentService, 它會在處理完扔給它的intent任務之後盡快結束自己。更多資訊,請閱讀Running in a Background Service.

當一個service已經不需要的時候還繼續保留它,這對Android應用的記憶體管理來說是最糟糕的錯誤之一。是以千萬不要貪婪的使得一個Service持續保留。不僅僅是因為它會使得你的app因RAM的限制而性能糟糕,而且使用者會發現那些行為奇怪的app并且解除安裝它。

2)當你的UI隐藏時釋放記憶體

當使用者切換到其它app并且你的app UI不再可見時,你應該釋放你的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回調,并且在此回調裡面釋放activity的資源,例如網絡連接配接,unregister廣播接收者。除非接收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN))的回調,否者你不應該釋放你的UI資源。這確定了使用者從其他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)檢查你應該使用多少的記憶體

正如前面提到的,每一個Android裝置都會有不同的RAM總大小與可用空間,是以不同裝置為app提供了不同大小的heap限制。你可以通過調用getMemoryClass())來擷取你的app的可用heap大小。如果你的app嘗試申請更多的記憶體,會出現OutOfMemory的錯誤。

在一些特殊的情景下,你可以通過在manifest的application标簽下添加largeHeap=true的屬性來聲明一個更大的heap空間。如果你這樣做,你可以通過getLargeMemoryClass())來擷取到一個更大的heap size。

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

另外, large heap并不一定能夠擷取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。是以即使你申請了large heap,你還是應該通過執行getMemoryClass()來檢查實際擷取到的heap大小。

5)避免bitmaps的浪費

當你加載一個bitmap時,僅僅需要保留适配目前螢幕裝置分辨率的資料即可,如果原圖高于你的裝置分辨率,需要做縮小的動作。請記住,增加bitmap的尺寸會對記憶體呈現出2次方的增加,因為X與Y都在增加。

Note:在Android 2.3.x (API level 10)及其以下, bitmap對象的pixel data是存放在native記憶體中的,它不便于調試。然而,從Android 3.0(API level 11)開始,bitmap pixel data是配置設定在你的app的Dalvik heap中, 這提升了GC的工作效率并且更加容易Debug。是以如果你的app使用bitmap并在舊的機器上引發了一些記憶體問題,切換到3.0以上的機器上進行Debug。

6)使用優化的資料容器

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

7)請注意記憶體開銷

對你所使用的語言與庫的成本與開銷有所了解,從開始到結束,在設計你的app時謹記這些資訊。通常,表面上看起來無關痛癢(innocuous)的事情也許實際上會導緻大量的開銷。例如:

  • Enums的記憶體消耗通常是static constants的2倍。你應該盡量避免在Android上使用enums。
  • 在Java中的每一個類(包括匿名内部類)都會使用大概500 bytes。
  • 每一個類的執行個體花銷是12-16 bytes。
  • 往HashMap添加一個entry需要額一個額外占用的32 bytes的entry對象。

8)請注意代碼“抽象”

通常, 開發者使用抽象簡單的作為"好的程式設計實踐",因為抽象能夠提升代碼的靈活性與可維護性。然而,抽象會導緻一個顯著的開銷:通常他們需要同等量的代碼用于可執行。那些代碼會被map到記憶體中。是以如果你的抽象沒有顯著的提升效率,應該盡量避免他們。

9)為序列化的資料使用nano protobufs

Protocol buffers是由Google為序列化結構資料而設計的,一種語言無關,平台無關,具有良好擴充性的協定。類似XML,卻比XML更加輕量,快速,簡單。如果你需要為你的資料實作協定化,你應該在用戶端的代碼中總是使用nano protobufs。通常的協定化操作會生成大量繁瑣的代碼,這容易給你的app帶來許多問題:增加RAM的使用量,顯著增加APK的大小,更慢的執行速度,更容易達到DEX的字元限制。

關于更多細節,請參考protobuf readme的"Nano version"章節。

10)Avoid dependency injection frameworks

使用類似Guice或者RoboGuice等framework injection包是很有效的,因為他們能夠簡化你的代碼。

RoboGuice 2 smoothes out some of the wrinkles in your Android development experience and makes things simple and fun. Do you always forget to check for null when you getIntent().getExtras()? RoboGuice 2 will help you. Think casting findViewById() to a TextView shouldn’t be necessary? RoboGuice 2 is on it. RoboGuice 2 takes the guesswork out of development. Inject your View, Resource, System Service, or any other object, and let RoboGuice 2 take care of the details.

然而,那些架構會通過掃描你的代碼執行許多初始化的操作,這會導緻你的代碼需要大量的RAM來map代碼。但是mapped pages會長時間的被保留在RAM中。

11)謹慎使用external libraries

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

即使是針對Android而設計的library,也可能是很危險的,因為每一個library所做的事情都是不一樣的。例如,其中一個lib使用的是nano protobufs, 而另外一個使用的是micro protobufs。那麼這樣,在你的app裡面就有2種protobuf的實作方式。這樣的沖突同樣可能發生在輸出日志,加載圖檔,緩存等等子產品裡面。

同樣不要陷入為了1個或者2個功能而導入整個library的陷阱。如果沒有一個合适的庫與你的需求相吻合,你應該考慮自己去實作,而不是導入一個大而全的解決方案。

12)優化整體性能

官方有列出許多優化整個app性能的文章:Best Practices for Performance. 這篇文章就是其中之一。有些文章是講解如何優化app的CPU使用效率,有些是如何優化app的記憶體使用效率。

你還應該閱讀optimizing your UI來為layout進行優化。同樣還應該關注lint工具所提出的建議,進行優化。

13)使用ProGuard來剔除不需要的代碼

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

14)對最終的APK使用zipalign

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

Notes::Google Play不接受沒有經過zipalign的APK。

15)分析你的RAM使用情況

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

16)使用多程序

如果合适的話,有一個更進階的技術可以幫助你的app管理記憶體使用:通過把你的app元件切分成多個元件,運作在不同的程序中。這個技術必須謹慎使用,大多數app都不應該運作在多個程序中。因為如果使用不當,它會顯著增加記憶體的使用,而不是減少。當你的app需要在背景運作與前台一樣的大量的任務的時候,可以考慮使用這個技術。

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

你可以通過在manifest檔案中聲明'android:process'屬性來實作某個元件運作在另外一個程序的操作。

<service android:name=".PlaybackService"
         android:process=":background" />
           

更多關于使用這個技術的細節,請參考原文,連結如下。http://developer.android.com/training/articles/memory.html