標籤:
原文地址:http://android.xsoftlab.net/training/articles/memory.html
隨機儲存空間(RAM)在任何運行環境中都是一塊非常重要的地區,尤其是在記憶體受限的移動作業系統上。儘管Android的Dalvik虛擬機器會對其進行記憶體回收,但是這不意味著APP就可以忽略申請及釋放的記憶體。
為了可以使記憶體回收行程能夠有效清理APP所佔用的記憶體空間,你需要防止記憶體流失發生,並需要在適當的時間將Reference對象釋放。對大多數APP來說,記憶體回收行程會在正確的對象使用完畢之後將其所佔用的記憶體回收釋放。
這節課將會學習Android如何管理APP進程以及記憶體空間、以及如何減少記憶體的佔用。
Android如何管理記憶體
Android並沒有提供專門的記憶體交換空間,但是它使用了paging及memory-mapping來管理記憶體。這意味著任何你所修改的記憶體——無論是否被對象分配所使用,或者是被記憶體映射所佔用——它們會一直遺留在記憶體中,不能被交換出去。所以完全釋放APP記憶體的唯一方式就是釋放任何你可能所持有的對象引用,這樣才可以使記憶體回收行程對其進行回收。不過這裡有一個例外:任何沒有被修改的檔案對應,比如代碼,在系統需要的時候會被移出RAM。
共用記憶體
為了可以在RAM中滿足一切要求,Android試著在進程間共用RAM頁面。它通過以下幾種方式實現:
- 每個APP進程都是由一個名為Zygote的進程fork出來的。Zygote進程在系統啟動載入通用架構代碼及資源(比如Activity的主題)時啟動。為了啟動新的APP進程,系統會先fork出Zygote進程,然後再在新的進程中載入、運行APP的代碼。這使得為Android架構代碼以及資源所分配的RAM頁面在APP進程間共用成為了可能。
- 大多數的待用資料都是被映射到進程中的。這種方式不僅可以在進程間共用資料,還可以在需要的時候將其移除頁面。待用資料包含:Dalvik代碼(放置在預連結的.odex檔案中),APP資源以及在.so檔案中的本地代碼。
- 在很多地方,Android通過顯式記憶體配置地區在不同的進程間共用同一塊RAM。比如,WindowSurface就在APP與螢幕合成器間使用了共用記憶體,CursorBuffer在內容提供者與用戶端之間也使用了共用記憶體。
由於大量使用了共用記憶體,所以檢查APP佔用的記憶體空間就顯得很有必要了。
記憶體的分配與回收
以下是Android記憶體回收與再分配的一些情況:
- Dalvik中每個進程的堆都有虛擬記憶體範圍限制。這個範圍取決於邏輯堆尺寸的定義,它可以隨著APP的需要隨之增長(不過最大隻會增長到系統為每個app所分配的記憶體大小)。
- 堆棧的邏輯尺寸並不等同於堆棧所使用的實體記憶體大小。當系統檢查APP的堆時,會計算一個名為Proportional Set Size(PSS)的值,PSS的意思是,與其他進程共用的,需要清理的頁面列表。有關更多PSS的相關資訊,請閱讀指南:Investigating Your RAM Usage。
- Dalvik堆棧對堆棧的邏輯空間並不是連續排布的,這句話的意思是Android並不會對堆空間進行磁碟重組。Android只有在已使用的空間到達堆棧的末端時才會整理堆棧的邏輯空間。不過這不意味著堆所使用的物理空間不能被整理。在垃圾收集之後,Dalvik會先掃描堆並找出無用頁面,然後使用madvise將這些頁面返回到kernel。所以,成對的分配、回收大段的記憶體可以使大量的記憶體能夠重複使用。然而,回收小段的記憶體的效率可能會很低,因為小段記憶體的頁面可能正在被使用,還沒有被釋放。
偵測應用記憶體
為了維持一個多任務執行環境,Android為每個APP的堆大小都設定了硬性限制。具體的堆大小都不相同,這取決於RAM的大小。如果APP已經將所分配的堆容量用完,並還要繼續申請更多的記憶體,那麼APP會收到一個OutOfMemoryError錯誤。
在一些情況中,你可能需要知道當前的裝置中還有多少堆記憶體可用。比如,檢查多大的資料緩衝空間在記憶體中是安全的。你可以通過getMemoryClass()方法進行這樣查詢。它會返回一個整型數值,這個數值以MB為單位,代表了APP堆記憶體的可用值。這項內容將會在下面進行詳細討論。
APP的切換
使用者在切換APP時並沒有使用交換空間,Android將切換到背景進程放置在一個LRU(最近最少使用)緩衝中。這麼說吧,使用者先開啟了一個APP,那麼會專門有個進程為它啟動,後來使用者離開了該APP,但這個APP的進程並沒有退出,那麼這時系統會將這個APP的進程緩衝下來,所以如果使用者再次返回了該APP,那麼剛剛緩衝的進程會被再次利用,以便完成快速切換。
如果APP含有一個緩衝進程,並且佔用了當前系統並不那麼需要的記憶體,那麼在使用者不再使用它時,它就會影響到系統的整體效能。所以,隨著系統的可用記憶體減少,系統可能會殺死LRU緩衝中最近最少使用到的進程。為了使APP儘可能緩衝的時間長,下面的章節會介紹何時應當釋放引用。
有關更多進程在後台如何緩衝以及Android是如何決定哪個進程應當被殺死的相關資訊,請參見:Processes and Threads。
APP應當如何管理記憶體
APP應當在每個開發階段考慮RAM的限制,包括APP的設計階段。下面將會列出幾種有效解決方案:
在開發時應當採用以下方式來增加記憶體的使用效率。
儘可能少的使用服務
如果APP需要使用服務在後台做一些工作,絕不要在服務內做不必要的工作。還要注意,在工作完成之後,如果服務停止失敗,則要當心服務的泄露。
當啟動服務時,系統會為該服務持有一個進程。這會使得系統的開銷非常高昂,因為服務所使用的記憶體不能作為它用。這會減少系統保持在LRU緩衝中的進程數量,並會使得APP的轉換效率低下。當記憶體非常緊張或者系統不能夠保證有足夠的進程來維持當前的服務數量時它甚至會引起系統的卡死。
對於以上問題最佳的解決方案就是使用IntentService來限制本地服務的數量。
當服務不再需要時,留下服務繼續運行是APP常見的一種非常糟糕的記憶體管理錯誤。所以不要貪圖使服務保持長時運行。不及時停止服務不但會增加APP RAM容量不夠用的風險,而且還會使使用者覺得該APP做的非常的爛,並順便將其卸載。
在UI不可見時釋放記憶體
當使用者切換到其它APP時,這時你的APP UI會變得不可見,所以應該釋放與UI相關的所有資源。及時釋放UI資源可以明顯的增長系統緩衝進程的能力,這會直接影響到使用者的體驗。
為了可以在使用者離開UI後還能收到系統通知,應當在Activity內實現onTrimMemory()方法。在該方法內監聽TRIM_MEMORY_UI_HIDDEN標誌,這個標誌代表了UI目前進入隱藏態,應當釋放UI所用到的所有資源。
這裡要注意,TRIM_MEMORY_UI_HIDDEN標誌代表的是APP內所有的UI組件對於使用者隱藏。這要與onStop()區分開,該方法是在Activity的執行個體變的不可見時調用,它是在APP內部Activity之間的切換時調用的。所以儘管在onStop()中釋放了Activity的資源比如網路連接,登出廣播接收器等等,但是一般不要在該方法內釋放UI資源。因為這可以使使用者在返回該Activity時,UI現場可以迅速恢複。
在記憶體緊張時釋放記憶體
在APP生命週期的任何階段,onTrimMemory()方法會告知當前裝置記憶體很緊張。你應當在收到以下標誌時進一步的釋放資源:
- TRIM_MEMORY_RUNNING_MODERATE APP目前處於運行態,暫時不會被殺死,但是裝置目前處於低記憶體運行態,並且系統正在殺死LRU緩衝中的進程。
- TRIM_MEMORY_RUNNING_LOW APP目前處於運行態,暫時不會被殺死,但是裝置目前處於極低記憶體運行態,所以你應當釋放無用的資源來增進系統的效能。
- TRIM_MEMORY_RUNNING_CRITICAL APP還處於運行態,但是系統已經準備將LRU緩衝中的大部分進程殺死,所以APP應當立即釋放所有不必要的資源。如果系統沒有獲得足夠數量的RAM空間,那麼系統會清除LRU中的所有進程,並會殺死一些主機進行中的服務。
還有,在APP處於緩衝狀態時,你可能會收到以下標誌:
- TRIM_MEMORY_BACKGROUND APP處於低記憶體運行態,APP的進程處於LRU列表的前端。儘管APP所面臨被殺死的風險還比較低,但是系統可能已經做好了殺死LRU進程中的準備。APP應當釋放那些易於恢複的資源,這樣的話,進程會繼續保留在緩衝列表中,並且會在使用者返回到APP時迅速恢複。
- TRIM_MEMORY_MODERATE APP處於低記憶體運行態,APP的進程處於LRU列表的中部。如果系統的記憶體進一步的降低,那麼APP的進程可能就會被殺死。
- TRIM_MEMORY_COMPLETE APP處於低記憶體運行態。如果系統沒有足夠記憶體的話,APP的進程首當其衝會被殺死。APP應當釋放在恢複APP時一切不重要的事物。
因為onTrimMemory()方法添加於API 14,所以可以使用onLowMemory()來相容老版本,它大致與TRIM_MEMORY_COMPLETE標誌是等價的。
Note: 當系統開始殺死LRU中的進程時,儘管它是自下而上工作的,但是系統還是會考慮這麼一種情況:哪個進程消耗的記憶體比較多,所以如果將該進程殺死後,將會獲得更多的記憶體。所以在APP處於LRU緩衝時,儘可能的消耗少量的記憶體,這樣一直維持在緩衝列表中的機會才大,才可以在切換回APP時迅速恢複狀態。
檢查應該使用多少記憶體
就像我們早期提到的,運行Android系統的裝置的RAM空間各有不同,所以提供給每個APP的堆空間也是不同的。你可以通過getMemoryClass()方法獲得APP的可用空間。如果APP試圖向系統申請比該方法傳回值大的記憶體空間的話,那麼它會收到一個OutOfMemoryError錯誤。
在一些特別特殊的環境中,你可以申請更大的堆空間,可以通過在資訊清單檔的< application>標籤中添加largeHeap=”true”屬性的方式來設定。在設定之後,可以通過getLargeMemoryClass()來查詢大尺寸的堆棧空間量。
然而,申請大堆空間的APP只有正常用途才應該申請,比如大相片編輯類APP。決不要是因為經常出現了OutOfMemory錯誤才這麼去做,你應該做的是解決那個OutOfMemory的問題。只有在你明確知道正常的堆空間不足以支撐APP的運行時才應該這麼做。使用額外的記憶體空間會嚴重損害整體的使用者體驗,因為垃圾收集器會在此消耗更長的時間,並且在任務切換或者執行其它並行作業時系統效能會明顯減慢。
此外,大堆空間的尺寸在所有的裝置上並不是相等的。當運行在某些RAM限制的裝置上,大堆空間的尺寸可能與常規的堆空間尺寸相等。所以,就算是申請了大堆空間,那麼還是應該使用getMemoryClass()來檢查一下常規堆空間大小,並盡量將記憶體的使用量控制在這個範圍以下。
避免浪費位元影像的記憶體
當載入一張圖片到記憶體時,最好是將該圖片適配到當前螢幕解析度大小之後再做記憶體緩衝,如果原圖本身解析度很高的話,最好將其縮小到適合螢幕解析度大小。要注意,隨著位元影像解析度的增加,所佔的相應記憶體也一併增加。
Note: 在Android 2.3.x之前,位元影像對象無論解析度是多大都是以相同的大小出現在堆中的,因為位元影像的實際像素資料被單獨的存放在了本地記憶體中。這使得位元影像記憶體配置的調試變得很困難,因為大多數的堆棧分析工具並不能探測到本地記憶體的分配。然而,自Android 3.0之後,位元影像的像素資料被分配與APP的Dalvik堆棧中,這樣增進了記憶體回收的效率以及調試能力。
使用最佳化過的資料容器
我們建議使用Android架構最佳化過的資料容器,比如SparseArray,SparseBooleanArray,以及LongSparseArray.常規的HashMap其實效率是很低的,因為它需要為每個映射建立單獨的實體。另外SparseArray的工作效率更高,因為它可以避免系統對鍵或值的自動裝箱功能。
要對記憶體的消耗有一定的意識
要充分瞭解你所使用的語言以及庫的記憶體開銷,並要一直保持有這種意識,包括在APP的設計階段。經常表面的事物看起來無傷大雅,但實際上它們所消耗的記憶體是很高的。比如:
- Java的枚舉類型需要佔用靜態常量的兩倍記憶體。你應該堅決制止在Android中使用枚舉。
- Java中的每個類,包含匿名內部類的代碼需要佔用500個位元組。
- 每個類的執行個體需要佔用12-16個位元組的RAM空間。
- 將每個執行個體放入HashMap需要格外花費32個位元組的空間。
雖然以上的內容只是會消耗幾個位元組的空間,但是它們會在程式的內部迅速的積累增加,成為一個巨無霸級的開銷。它會使你在分析記憶體問題的時候讓你處於一個非常尷尬的境地,因為這些眾多的小對象消耗了大量的記憶體。
當心抽象代碼
經常開發人員會使用抽象代碼實現一種良好的程式設計結構,因為抽象代碼可以增進代碼的靈活性與可維護性。不過,抽象代碼會有不菲的開銷:通常它們需要更多的執行代碼,需要花費更多的時間以及更多的RAM空間來將這些抽象代碼映射到記憶體。所以,如果不是必須的話,最好遠離它們。
為序列化資料使用納米級緩衝協議
Protocol buffers是一種由Google設計的序列化結構的資料。它與語言無關、與平台無關的、可擴充。與XML類似,但是體積更小,速度更快、也更簡單。如果你決定要使用該納米級緩衝協議,那麼就應當在用戶端代碼中一直使用它。常規的protobufs會產生非常冗長的代碼,這會引出相當多的問題:增加記憶體的消耗,增長APK的體積,減緩執行效率並會迅速接近DEX標誌的限制。
避免依賴註解架構
使用Guice、RoboGuice這類註解依賴架構是相當方便的,因為這些架構可以簡化代碼的書寫,以及提供了相應的測試環境。然而,這些架構在掃描碼的註解時會執行大量的初始化工作,這會使得大量的代碼映射到RAM中,儘管你不需要降這些代碼載入記憶體。這些被映射的頁面會一直駐留在記憶體中,雖然系統可以將它們清除,但是只有在這些頁面長時間駐留在記憶體中才會執行清理。
使用第三方庫要當心
第三方代碼通常不是專門為行動裝置而寫。當這些代碼運行在移動用戶端時往往執行效率很低。在決定使用第三方庫之前,應該假設正在執行一項很重要的移植工作,並將要負擔為行動裝置的維護、最佳化工作。在決定使用之前要分析該庫的大小以及RAM的佔用。
就算是某些庫是專門為Android所設計的,但是它們還是存在隱患的,因為每個庫所做的事不同。舉個栗子,一個庫可能使用了納米級的protobufs,而另一個庫則使用了毫米級的protobufs。那麼現在在APP中使用了兩個層級的protobufs。這兩種差異可能會發生在日誌、解析、映像載入架構、緩衝以及其它任何你不期望的事情上。
還要當心掉入共用庫的陷阱,這種共用庫有一個共同的特點就是,你只使用了該庫所提供的很小的功能,你並不希望將其它用不到的大量代碼也一併放入你的工程內。在最後,如果你不是特別的需要這個第三方庫的話,那麼最好的方式就是自己實現一個。
最佳化整體效能
有關APP整體效能最佳化的建議都列在了Best Practices for Performance中。這些建議還包括了CPU的效能最佳化,除此之外還包括了記憶體的最佳化,比如減少布局對象的數量。
你還應該讀一讀有關optimizing your UI的文章,文章內包含了布局調試工具以及lint tool中所提示的一些布局最佳化建議。
使用ProGuard篩除無用代碼
ProGuard工具可以通過移除無用代碼以及以一種無意義的名稱重新命名類名,屬性,方法的方式來達到一種精簡、最佳化、模糊的效果。接下來還必須使用zipalign工具對重新命名後的代碼進行調整。如果不做這一步將會大大增加RAM的使用量,因為類似於資源這些事物不會再由APK映射到記憶體。
作者PS: 這段話摘自於zipalign的介紹,相當於是說Zipalign的原理與優勢: Specifically, it causes all uncompressed data within the .apk, such as images or raw files, to be aligned on 4-byte boundaries. This allows all portions to be accessed directly with mmap() even if they contain binary data with alignment restrictions. The benefit is a reduction in the amount of RAM consumed when running the application.
分析RAM的使用狀況
一旦APP達到一個相對穩定的程度,那麼接下來就需要分析APP在各個生命週期的RAM使用方式了。有關如何分析APP的RAM使用方式,請參見: Investigating Your RAM Usage。
使用多進程
如果它適用於你的APP,那麼另一項可能協助你管理APP記憶體的升級建議就是將組件部署到不同的進程中。使用這項建議必須總是特別的小心,並且大部分APP不應該使用這項技術,如果處理不當的話它會迅速的增加RAM的消耗。這項技術對於那些運行在背景工作與前台的工作一樣重要的APP極為有用,並且可以單獨管理這些操作。
使用多進程最適合的情境就是音樂播放器。如果整個APP運行在單一的進程中,那麼Activity UI所執行的大部分記憶體配置都會和音樂的播放保持相同的時間,甚至是使用者切換到了其它APP。那麼像這樣的APP就應該擁有兩個進程:一個進程負責UI,而另一個的工作就是持續不斷的運行後台服務。
你可以在資訊清單檔中需要執行單獨進程的組件裡添加android:process屬性來實現獨立進程。比如,你可以在需要執行單獨進程的服務中添加該屬性,並聲明該進程的名稱”background”(你可以命名任何你想命名的名稱):
<service android:name=".PlaybackService" android:process=":background" />
進程的名稱應該以冒號’:’開頭,以便確保該進程屬於你APP的私人進程。
在決定建立一個新進程之前,你應該瞭解一下記憶體的影響。為了示範每個進程的執行效果,首先要考慮到一個不做任何事情的進程需要佔用大約1.4MB的記憶體空間,下面顯示了空態下的記憶體資訊堆:
adb shell dumpsys meminfo com.example.android.apis:empty** MEMINFO in pid 10172 [com.example.android.apis:empty] ** Pss Pss Shared Private Shared Private Heap Heap Heap Total Clean Dirty Dirty Clean Clean Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 0 0 0 0 0 0 1864 1800 63 Dalvik Heap 764 0 5228 316 0 0 5584 5499 85 Dalvik Other 619 0 3784 448 0 0 Stack 28 0 8 28 0 0 Other dev 4 0 12 0 0 4 .so mmap 287 0 2840 212 972 0 .apk mmap 54 0 0 0 136 0 .dex mmap 250 148 0 0 3704 148 Other mmap 8 0 8 8 20 0 Unknown 403 0 600 380 0 0 TOTAL 2417 148 12480 1392 4832 152 7448 7299 148
Note: 如何閱讀這些資訊請參見Investigating Your RAM Usage。這裡的關鍵資料是Private Dirty及Private Clean所指示的記憶體。它們分別說明了這個進程使用了大概1.4MB左右的非交換頁記憶體,而另外150K RAM則是被映射到記憶體之後將要執行的代碼所佔用的空間。
瞭解空進程狀態下的記憶體佔用是相當重要的,它會隨著工作的開始迅速增長。比如,下面是一個顯示了一些文本的Activity的記憶體佔用情況:
** MEMINFO in pid 10226 [com.example.android.helloactivity] ** Pss Pss Shared Private Shared Private Heap Heap Heap Total Clean Dirty Dirty Clean Clean Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 0 0 0 0 0 0 3000 2951 48 Dalvik Heap 1074 0 4928 776 0 0 5744 5658 86 Dalvik Other 802 0 3612 664 0 0 Stack 28 0 8 28 0 0 Ashmem 6 0 16 0 0 0 Other dev 108 0 24 104 0 4 .so mmap 2166 0 2824 1828 3756 0 .apk mmap 48 0 0 0 632 0 .ttf mmap 3 0 0 0 24 0 .dex mmap 292 4 0 0 5672 4 Other mmap 10 0 8 8 68 0 Unknown 632 0 412 624 0 0 TOTAL 5169 4 11832 4032 10152 8 8744 8609 134
現在進程使用了剛剛的三倍記憶體,將近4MB,只是在UI中展示了一段文本而已。這可以推出一個非常重要的結論:如果你將APP的功能放在多個進程中執行,只有一個進程用於響應UI,而另外的進程則應當避免與UI接觸,因為這會迅速的增加RAM的消耗。一旦UI被繪製,那麼幾乎就很難將記憶體的用量降下來。
另外,當運行超過一個進程時,非常重要的一點是,應當使代碼儘可能的精簡,因為任何不必要的開銷都是因為相同的實現被複製到了每個進程中。比如,如果你正在使用枚舉(儘管不應該使用枚舉),所有進程的RAM都需要建立並且初始化這些複製到每個進程中的常量,其它任何的抽象適配器、常量或者其它佔用記憶體的都會被複製。
使用多進程的另外一個擔憂就是它們之間的依賴關係。比如,如果APP內含有ContentProvider,並且該ContentProvider運行於顯示UI的進程,那麼另一個後台進程的代碼需要使用這個ContentProvider時,這就需要該UI進程也載入進RAM中。如果你的服務是一個與UI進程相當權重的後台服務,那麼該服務就不應該依賴UI進程中的ContentProvider或者服務。
Android官方開發文檔Training系列課程中文版:APP的記憶體管理