標籤:聲明 set proc 最好 協助 apk bytes nts 客戶
http://hukai.me/android-training-managing_your_app_memory/
Random Access Memory(RAM)在任何軟體開發環境中都是一個很寶貴的資源。這一點在實體記憶體通常很有限的移動作業系統上,顯得尤為突出。儘管Android的Dalvik虛擬機器扮演了常規的記憶體回收的角色,但這並不意味著你可以忽視app的記憶體配置與釋放的時機與地點。
為了GC能夠從app中及時回收記憶體,我們需要注意避免記憶體泄露(通常由於在全域成員變數中持有對象引用而導致)並且在適當的時機(下面會講到的lifecycle callbacks)來釋放引用對象。對於大多數app來說,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回收了。只有一種例外是:如果系統想要在其他地方重用這個對象。
1) 共用記憶體
Android通過下面幾個方式在不同的進程中來實現共用RAM:
每一個app的進程都是從一個被叫做Zygote的進程中fork出來的。Zygote進程在系統啟動並且載入通用的framework的代碼與資源之後開始啟動。為了啟動一個新的程式進程,系統會fork Zygote進程產生一個新的進程,然後在新的進程中載入並運行app的代碼。這使得大多數的RAM pages被用來分配給framework的代碼,同時使得RAM資源能夠在應用的所有進程中進行共用。
大多數static的資料被mmapped到一個進程中。這不僅僅使得同樣的資料能夠在進程間進行共用,而且使得它能夠在需要的時候被paged out。例如下面幾種static的資料:
- Dalvik 代碼 (放在一個預連結好的 .odex 檔案中以便直接mapping)
- App resources (通過把資源表結構設計成便於mmapping的資料結構,另外還可以通過把APK中的檔案做aligning的操作來最佳化)
- 傳統項目元素,比如 .so 檔案中的本地代碼.
- 在很多情況下,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()來查詢。這個方法會返回一個整數,表明你的應用的heap size限制是多少Mb(megabates)。
4) 切換應用
Android並不會在使用者切換不同應用時候做交換記憶體的操作。Android會把那些不包含foreground組件的進程放到LRU cache中。例如,當使用者剛開始啟動了一個應用,系統會為它建立了一個進程,但是當使用者離開這個應用,此進程並不會立即被銷毀。系統會把這個進程放到cache中,如果使用者後來再回到這個應用,此進程就能夠被完整恢複,從而實現應用的快速切換。
如果你的應用中有一個被緩衝的進程,這個進程會佔用暫時不需要使用到的記憶體,這個暫時不需要使用的進程,它被保留在記憶體中,這會對系統的整體效能有影響。因此當系統開始進入低記憶體狀態時,它會由系統根據LRU的規則與其他因素選擇綜合考慮之後決定殺掉某些進程,為了保持你的進程能夠儘可能長久的被緩衝,請參考下面的章節學習何時釋放你的引用。
對於那些不在foreground的進程,Android是如何決定kill掉哪一類進程的問題,請參考Processes and Threads.
第2部分: 你的應用該如何管理記憶體
你應該在開發過程的每一個階段都考慮到RAM的有限性,甚至包括在開始編寫代碼之前的設計階段就應該考慮到RAM的限制性。我們可以使用多種設計與實現方式,他們有著不同的效率,即使這些方式只是相同技術的不斷組合與演變。
為了使得你的應用效能效率更高,你應該在設計與實現代碼時,遵循下面的技術要點。
1) 珍惜Services資源
如果你的應用需要在後台使用service,除非它被觸發並執行一個任務,否則其他時候service都應該是停止狀態。另外需要注意當這個service完成任務之後因為停止service失敗而引起的記憶體流失。
當你啟動一個service,系統會傾向為了保留這個service而一直保留service所在的進程。這使得進程的運行代價很高,因為系統沒有辦法把service所佔用的RAM空間騰出來讓給其他組件,另外service還不能被paged out。這減少了系統能夠存放到LRU緩衝當中的進程數量,它會影響app之間的切換效率。它甚至會導致系統記憶體使用量不穩定,從而無法繼續保持住所有目前正在啟動並執行service。
限制你的service的最好辦法是使用IntentService, 它會在處理完交代給它的intent任務之後儘快結束自己。更多資訊,請閱讀Running in a Background Service.
當一個Service已經不再需要的時候還繼續保留它,這對Android應用的記憶體管理來說是最糟糕的錯誤之一。因此千萬不要貪婪的使得一個Service持續保留。不僅僅是因為它會使得你的應用因為RAM空間的不足而效能糟糕,還會使得使用者發現那些有著常駐後台行為的應用並且可能卸載它。
2) 當UI隱藏時釋放記憶體
當使用者切換到其它應用並且你的應用 UI不再可見時,你應該釋放你的應用UI上所佔用的所有記憶體資源。在這個時候釋放UI資源可以顯著的增加系統緩衝進程的能力,它會對使用者體驗有著很直接的影響。
為了能夠接收到使用者離開你的UI時的通知,你需要實現Activtiy類裡面的onTrimMemory()回調方法。你應該使用這個方法來監聽到TRIM_MEMORY_UI_HIDDEN層級的回調,此時意味著你的UI已經隱藏,你應該釋放那些僅僅被你的UI使用的資源。
請注意:你的應用僅僅會在所有UI組件的被隱藏的時候接收到onTrimMemory()的回調並帶有參數TRIM_MEMORY_UI_HIDDEN。這與onStop()的回調是不同的,onStop會在activity的執行個體隱藏時會執行,例如當使用者從你的app的某個activity跳轉到另外一個activity時前面activity的onStop()會被執行。因此你應該實現onStop回調,並且在此回調裡面釋放activity的資源,例如釋放網路連接,登出監聽廣播接收者。除非接收到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) 避免使用依賴注入架構
使用類似Guice或者RoboGuice等framework injection包是很有效,因為他們能夠簡化你的代碼。
Notes:RoboGuice 2 通過依賴注入改變代碼風格,讓Android開發時的體驗更好。你在調用 getIntent().getExtras() 時經常忘記檢查 null 嗎?RoboGuice 2 可以幫你做。你認為將 findViewById() 的傳回值強制轉換成 TextView 是本不必要的工作嗎? RoboGuice 2 可以幫你。RoboGuice 把這些需要猜測性的工作移到Android開發以外去了。RoboGuice 2 會負責注入你的 View, Resource, System Service或者其他對象等等類似的細節。
然而,那些架構會通過掃描你的代碼執行許多初始化的操作,這會導致你的代碼需要大量的RAM來mapping代碼,而且mapped pages會長時間的被保留在RAM中。
11) 謹慎使用第三方libraries
很多開源的library代碼都不是為移動網路環境而編寫的,如果運用在行動裝置上,,這樣的效率並不高。當你決定使用一個第三方library的時候,你應該針對移動網路做繁瑣的遷移與維護的工作。
即使是針對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’屬性來實現某個組件運行在另外一個進程的操作。
12 |
<service android:name=".PlaybackService" android:process=":background" />
|
更多關於使用這個技術的細節,請參考原文,連結如下。 http://developer.android.com/training/articles/memory.html
文章學習自http://developer.android.com/training/articles/memory.html
Android Training - 管理應用的記憶體