Android官方開發文檔Training系列課程中文版:APP的內存管理

分類:IT技術 時間:2016-10-17

寫在開頭的話: 如果有同學對android性能比較關註的,可以閱讀這篇文章:Android性能優化建議

原文地址: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()方法進行這樣查詢。它會返回一個整型數值,這個數值以兆字節為單位,代表了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或者服務。


Tags: Android 運行環境 交換空間 垃圾回收 虛擬機

文章來源:


ads
ads

相關文章
ads

相關文章

ad