天天看點

管理應用程式記憶體Android是怎樣管理記憶體的 應該如何管理應用程式記憶體 在整個開發階段, 你都應該考慮到記憶體的限制, 包括app設計(開發前). 這裡有幾條讓你的設計和代碼更高效的建議, 你在設計和實作app時應該遵循以下技巧以實作高效的記憶體使用. 當使用者界面隐藏時,釋放記憶體當記憶體緊張時釋放記憶體

在任何軟體開發環境中, RAM都是寶貴的資源,但在移動作業系統中更加珍貴, 因為這些裝置的實體記憶體通常都是受限的. 

盡管Dalvik虛拟機有例行地垃圾回收機制, 但這不能成為忽略記憶體配置設定和釋放的理由.

為了讓GC回收記憶體, 你要避免記憶體洩漏(通常因為全局成員變量引用對象引起),并且在适當的時候(生命周期回調方法中)

釋放對象引用. 對大多數app來說,垃圾回收負責剩下的:當相應的對象離開app活動線程範圍時, 系統回收記憶體. 

這篇文檔解釋了Android如何管理app程序和記憶體配置設定的, 并且如何主動減少記憶體占用. 更多用java程式設計時清理資源的資訊, 

可以參考關于管理資源引用的文檔和書籍. 如果你在找分析應用的記憶體占用的資訊, 可以參考Investigating Your RAM Usage.

Android是怎樣管理記憶體的

Android不提供交換空間, 而是使用頁和記憶體映射來管理記憶體。這意味着任何記憶體的修改,不論是配置設定給新對象還是映射記憶體頁,

都在記憶體中, 無法被移出.是以, 完成釋放記憶體的唯一方法是釋放對象引用, 把這塊記憶體交給GC. 但有個例外, 任何映射過來過來沒有

被修改的記憶體, 例如代碼段,  是可以被交換出記憶體的如果這塊記憶體需要被用在其他地方.

共享記憶體

為了适應記憶體中的所有需要,Android會跨程序共享記憶體頁.可以用下面的方式來實作:

1. 每個app程序都是從一個叫做Zygote程序fork出來的. Zygote程序在系統啟動時就啟動并且載入通用架構代碼和資源(如activity主題),

   為了啟動一個app程序, 系統fork出Zygote程序, 然後從一個新程序來載入運作app的代碼. 這使得大多數為Android架構代碼和資源

   可以跨所有app程序共享.

2. 大多數靜态的資料是被映射到一個程序裡. 這不僅允許相同的資料在程序間被共享, 同時也允許在需要的時候移出. 靜态資料例子:

    Dalvik代碼(在直接映射的.odex檔案中), app資源(通過設計一個資源表的結構,使得可以被映射和調整), native代碼的so檔案.

3. 在很多地方, android通過顯示地配置設定共享記憶體區域,來跨程序共享動态記憶體. 舉個例子, 視窗surface在app和screen compositor間

    共享記憶體, cursor在 Content Provider和用戶端間使用共享記憶體

由于大量共享記憶體的使用, 需要小心地确定你的應用用了多少記憶體. Investigating Your RAM Usage從技術上詳細地讨論了app記憶體使用.

配置設定和回收應用程式記憶體

以下是一些關于Android如何配置設定和釋放記憶體的情況

1. 每個程序的Dalvik堆受限于一個單一的虛拟記憶體區域. 這裡定義了可以根據需要自動增長的邏輯堆大小(但最大隻能到系統給每個app定義的上限)

2. 堆的邏輯大小, 跟堆使用的實體記憶體大小不同. 當檢查應用的堆時, Android計算一個叫PPS(比例設定大小)的值, 該值記錄了與其他程序共享的dirty

    和clean頁,但隻有多少應用共享記憶體的一個比例 。這個值就是系統認為了你的實體記憶體足迹,更多關于pps的資訊,參考Investigating Your RAM 

    Usage guide

3. Dalvik堆壓縮堆的邏輯大小, 這意味着Android不會碎片整理以縮小堆. 隻有當堆的末端有未使用的空間時, Android才會收縮堆的邏輯大小. 但這

   并不意味着堆用的實體記憶體不能被收縮. 在GC後, Dalvik周遊堆裡無用的頁, 并且用madvise傳回給核心. 是以, 配置設定和釋放大塊記憶體成對出現,

   就會釋放幾乎所有的實體記憶體. 然而, 小塊配置設定記憶體的釋放沒有那麼明顯的效果, 因為這塊記憶體可能仍然被其他沒有釋放的子產品共享.

限制應用程式記憶體

為了支援多任務環境, Android給每個app限制了堆大小. 具體限制大小根據裝置不同以及記憶體大小不同而變動. 如果你的app達到了這個限制還是嘗試

配置設定更多記憶體,就會報OutOfMemoryError. 因為某些原因, 你可能需要通過查詢系統判斷目前裝置上堆限制的大小, 比如, 判斷緩存裡放多少資料

才是安全的. 可以通過調用getMemoryClass()來得到這個數字. 它傳回一個整數表示你的app的堆可用大小(M).這将在下面作進一步讨論.

切換應用程式

當使用者切換App時, Android并沒有把之前的App放到交換空間裡, 而是把所有非前台的應用元件放到一個LRU緩存裡. 舉例來說, 使用者第一次運作app時, 

為它建立了一個程序, 但當使用者離開這個app時, 程序并沒有退出. 系統緩存了這個程序, 是以當使用者回到這個應用時, 程序可以重用以達到快速切回.

當你的應用緩存了程序, 并且保留目前沒有用的記憶體時, 即使使用者沒有在用,也限制了系統的整體性能. 是以, 當系統記憶體較低時, 它會清理LRU緩存裡

最近最少用的那一部分程序, 但也會考慮哪個程序最需要記憶體. 為了讓你的程序緩存得盡可能久, 遵從下面幾條關于何時釋放引用的建議.

更多關于程序在沒有運作在前台時如何被緩存以及Android決定殺死哪個程序的資訊,參考Processes and Threads guide.

應該如何管理應用程式記憶體

在整個開發階段, 你都應該考慮到記憶體的限制, 包括app設計(開發前). 這裡有幾條讓你的設計和代碼更高效的建議,

你在設計和實作app時應該遵循以下技巧以實作高效的記憶體使用.

謹慎使用Service 如果你的應用需要一個Service在背景執行任務, 除非它需要保持活動做一件事情, 否則不要讓它運作. 同樣不要忘記在它結束工作時關閉Service.

當你啟動一個Service時, 系統更傾向于保持這個程序以讓Service繼續運作. 這讓程序變得非常昂貴, 因為被它使用的記憶體無法被其他程序使用, 

也不能分頁出去. 這減少了系統可以緩存在LRU裡程序的數量, 讓App切換效能差一些. 它甚至可能會導緻系統在可用記憶體緊張, 無法維持足夠的

程序供所有目前運作的Service執行. 

限制Service生命期限的最佳方法是使用IntentService, 它會在處理完啟動它的Intent後, 自動結束自己. 更多資訊, 閱讀 Running in a Background Service

在不需要的時候還讓Service運作是最糟糕的記憶體管理之一.是以, 不要貪婪地想用保持Service運作來保持App運作. 這不僅會增加在達到記憶體限制時風險,

使用者也會因為這樣的作弊行為而解除安裝它.

當使用者界面隐藏時,釋放記憶體

當使用者導航到其他app, 你的ui不再可見時, 你應該釋放那些隻有UI使用的資源. 在這個時候釋放資源可以大大增加系統緩存程序的能力, 這跟使用者體驗直接相關.

為了當使用者退出ui時得到通知, 在Activity裡實作onTrimMemory()回調, 你應該在這個方法裡監聽TRIM_MEMORY_UI_HIDDEN的級别, 它表示你的UI從視圖中

隐藏了,你需要釋放隻有UI使用的資源, 注意, 隻有當應用的所有ui都隐藏時,才會收到onTrimMemory()回調. 這跟onStop()回調不同, 它在Activity隐藏時就會回調,

包括切換到同一個app的不同Activity.是以, 盡管你應該實作onStop()釋放activity的資源,像網絡連接配接,或者解注冊廣播接收器, 你通常不應該釋放你的UI資源,

直到收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)回調. 這確定了當使用者導緻到應用内的其他activity再回來時,ui資源仍舊可用并能快速恢複.

當記憶體緊張時釋放記憶體

在App生命周期的任何一個階段, onTrimMemory()同樣告訴你什麼時候整個裝置的可用記憶體變低. 此時, 你應該根據onTrimMemory傳入的幾個 記憶體級别釋放記憶體:

TRIM_MEMORY_RUNNING_MODERATE

你的應用正在運作, 并且不會被殺死, 但裝置已經處于低記憶體狀态, 并且開始殺死LRU緩存裡的記憶體.

TRIM_MEMORY_RUNNING_LOW

你的應用正在運作, 并且不會被殺死, 但裝置處于記憶體更低的狀态, 是以你應該釋放無用資源以提高系統性能(直接影響app性能)

TRIM_MEMORY_RUNNING_CRITICAL

你的應用還在運作, 但系統已經殺死了LRU緩存裡的大多數程序, 是以你應該在此時釋放所有非關鍵的資源. 如果系統無法回收足夠的記憶體,  它會清理掉所有LRU緩存, 并且開始殺死之前優先保持的程序, 像那些運作着service的.同時, 當你的app程序目前被緩存, 你可能會從 onTrimMemory()收到下面的幾種level.

TRIM_MEMORY_BACKGROUND

系統運作在低記憶體狀态, 并且你的程序已經接近LRU清單的頂端(即将被清理). 雖然你的app程序還沒有很高的被殺死風險, 系統可能已經清理 LRU裡的程序, 你應該釋放那些容易被恢複的資源, 如此可以讓你的程序留在緩存裡, 并且當使用者回到app時快速恢複.

TRIM_MEMORY_MODERATE

系統運作在低記憶體狀态, 你的程序在LRU清單中間附近. 如果系統變得記憶體緊張, 可能會導緻你的程序被殺死.

TRIM_MEMORY_COMPLETE

系統運作在低記憶體狀态, 如果系統沒有恢複記憶體, 你的程序是首先被殺死的程序之一. 你應該釋放所有不重要的資源來恢複你的app狀态.

因為onTrimMemory()是在API 14裡添加的, 你可以在老版本裡使用onLowMemory()回調,大緻跟TRIM_MEMORY_COMPLETE事件相同.

确定可以使用多少記憶體

之前提過, 每個Android裝置有不同的可用記憶體, 是以給每個app提供不同的堆限制. 你可以調用getMemoryClass()來确定你的app能用多少M堆大小.

如果你的app嘗試配置設定更多的記憶體,會收到OutOfMemoryError.

在非常特殊的情況下, 你可以請求一個較大的堆, 通過在Androidmanifest.xml的application标簽裡設定largeHeap屬性為true. 如果你這麼做, 

你可以調用 getLargeMemoryClass()來得到較大堆的大小. 然而,這個特性隻是給一小部分需要消耗大量記憶體的app(像大圖檔編輯app)準備的. 

不要僅僅因為你的應用報了OOM, 需要快速修複而設定large heap, 你應該在知道所有的記憶體用在哪, 為什麼需要保留時才設定. 即使你很自信能

證明你的app需要這麼多記憶體, 你也應該盡可能避免使用額外的記憶體, 會影響使用者體驗, 因為垃圾收回需要更長的時間, 當切換任務或執行其他普通操作時,

系統性能也會變慢. 另外large heap的大小并不是在所有機器上都一樣, 并且, 當運作在限制RAM的裝置上時, large heap大小可能會跟正常的heap大小相同.

是以, 即使你請求了large heap, 也應該調用 getMemoryClass()來得到heap大小, 并努力讓記憶體在這個限制之内.

避免在圖檔上浪費記憶體

載入圖檔時, 應該僅僅在記憶體中保留适應目前裝置螢幕大小的圖檔. 如果原圖分辨率高, 做下縮小. 謹記, 圖檔分辨率的增加伴随着記憶體占用的增加, 

因為x,y的值都增加. 提示:在Android 2.3.x(API level 10)或更低版本, bitmap對象總是跟app的堆大小相同, 而不管分辨率圖檔真實分辨率(實際像素數

據分開儲存在native記憶體中,非dalvik). 這使得調試圖檔記憶體配置設定更加困難, 因為大多數heap分析工具. 看不到native的記憶體配置設定.然而, 從Android 3.0 

(API level 11)開始, 圖檔像素資料在app的Dalvik heap裡配置設定, 改善了垃圾回收和可調試性. 是以, 如果你的app使用圖檔,在老裝置上要找到app使用

記憶體有些困難時, 你可以切換到高版本的裝置來調試. 更多關于位圖的提示,閱讀 Managing Bitmap Memory.

使用優化的資料容器

利用Android架構裡的優化過的容器, 像SparseArray, SparseBooleanArray和 LongSparseArray.一般的HashMap實作記憶體方面效率較差,

因為它需要為每個映射分開對象條目. 另外, SparseArray更高效因為它們避免了系統對key(value有時也行)做自動封裝, 不要擔心使用原始的數組.

注意記憶體開銷

了解你用的使用和庫的成本和開銷, 并且謹記這些限制,  從應用設計的開始到結束. 經常, 表面上看起來無害的東西會帶來龐大的開銷, 例子包括:

枚舉比靜态常量需要大于兩倍的記憶體, 在Android裡, 應該嚴格避免使用枚舉.

Java裡每個類(包括匿名和内部類) 使用大約500的位元組的代碼.

每個類執行個體有12-16位元組的開銷.

放單一的條目到HashMap, 需要建立額外的entry對象, 花費32位元組(參考上一節 使用優化的資料容器)

積少成多, app設計會被這些開銷所影響. 在記憶體裡的一大堆小對象裡分析, 找到問題, 并非易事.

小心使用代碼抽象

經常, 開發者們使用抽象, 僅僅是因為”良好的程式設計實踐”, 因為抽象可以提高代碼可擴充性, 可維護性. 然而, 抽象意味着成本:通常它們需要

一定數量的額外代碼, 需要更多的時間, 更多的記憶體把那部分代碼映射到記憶體. 是以, 如果你的抽象沒有實作的作用, 你應該避免使用.

使用nano protobufs序列化資料

Protocol buffers是一個由google設計的語言中立, 平台中立, 可擴充機制, 用來序列化結構資料.像xml,但更小, 更快, 更簡單.如果你決定使用protobufs,

你應該在用戶端代碼使用nano protobufs. 普通的protobufs生成非常冗長的代碼, 可能會給app帶來各自問題:增加記憶體占用, apk體積增長, 執行慢,

并且很快會達到dex的符号表限制.更多資訊,參見 protobuf readme裡的Nano version一節.

避免依賴注入架構

使用像Guice或RoboGuice的注入架構, 可能很吸引人, 因為可以簡化你寫的代碼, 提供一個用于測試或其他配置的有用的可适配的環境. 然而, 這些架構傾向于在初始化

時掃描注解執行一大堆的方法, 這意味着大量的代碼被映射到記憶體包括你不需要的. 這些映射頁被配置設定到clean記憶體裡, android可以扔掉他們, 但在一個很長的周期内

不會被移除.

小心使用外部庫

外部庫通常不是為移動環境寫的, 在移動用戶端上使用可能會導緻效能低. 至少, 當你決定要用一個外部庫時, 你應該承擔起移植維護并為移動優化的工作.

在決定使用前, 為這些工作作計劃, 分析庫大小, 記憶體占用. 即使為Android設計的庫, 也可能會帶來風險, 因為每個庫做不同的事情. 比如, 一個庫使用

nano protobufs另一個庫使用micro protobufs. 現在你有兩種不同的protobuf實作. 這些是不可預料的. ProGuard救不了你, 因為這些都是你需要的庫的

特性需要的低級别的依賴. 當你從庫中使用一個Activity的子類(會有大片的依賴)或使用反射或其他, 更容易出問題. 也要小心不要掉入為幾十個特性中的

一兩個特性使用一個共享庫的陷阱. 你不需要增加一大堆你不會用的代碼開銷. 找了很久也沒找到一個與你的需求非常符合的現有實作, 自己實作也許是最好的.

優化整體性能

在Best Practices for Performance裡列出了各種各樣的關于優化整體性能方法. 很多這些文檔包括了cpu性能優化建議, 但很多建議同時對優化記憶體也有幫助,

像減少UI的layout對象數量.同時,你也應該閱讀關于用布局調試工具優化UI,利用lint提供的優化建議.

使用ProGuard除去無用的代碼

ProGuard通過移除無用的代碼, 語義無關地重命名類, 字段, 方法來收縮, 優化, 混淆你的代碼. 使用ProGuard能使你的代碼更加緊湊, 減少映射的ram頁.

在最終的apk上使用zipalign

如果你對系統建構的apk(包括用最終産品簽名的)做後處理, 你必須運作zipalign, 讓其重新對齊. 否則會導緻你的app需要更多的記憶體, 因為像資源這些東西

可能不必從apk映射. 提示:Google Play商店不接受沒有zipaligne過的APK

分析你的記憶體使用

一旦你實作了一個相對穩定的版本,就要開始分析貫穿整個生命周期的記憶體占用。關于如何分析app,閱讀Investigating Your RAM Usage

使用多程序

如果合适, 一項可以幫助你管理應用記憶體的進階技術是拆分你的應用元件到多個程序. 這項技術使用時必須小心, 并且大多數應用不應該使用, 因為如果錯誤使用,

它很容易造成記憶體占用大幅增長. 它主要适用于那些在背景執行跟前台一樣的重要工作, 并且能分開管理這些操作的app. 一個适用多程序的例子是,建構一個音樂

播放器,有一個Service長期在背景播放音樂. 如果整個app運作在一個程序, 為Activity UI配置設定的很多記憶體必須保持跟播放音樂一樣長的時間, 

即使目前使用者已經跳到另一個app,但Service仍在播放. 一個類似這樣的app, 可以拆分成兩個程序, 一個用作UI, 一個用于背景服務.

可以通過聲明android:process屬性給每個元件指定一個單獨的程序.  比如, 你可以指定service運作在一個與app主程序不同的叫”background”(随你喜歡,任意)的程序.

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

你的程序名應該以冒号開始, 來確定這個程序對app私有.

在你決定建立一個新程序時, 你必須知道對記憶體的影響. 為了展示每個程序的影響, 給大家展示下dump出來的記憶體資訊, 一個空的基本上不做什麼的程序要消耗額外的1.4M記憶體.

簡單地在UI上顯示一些文本,程序占的記憶體幾乎達到了3倍,4MB.這引出了一個重要的結論:如果你想要拆分你的app到多個程序,應該隻有一個負責UI,其他程序應該避免UI操作,

因為這會程序需要的記憶體快速增長(特别是你開始加載圖檔或其他資源時).一旦UI被畫出來,要減少記憶體使用就很難或幾乎不可能.

此外, 當運作多個程序時, 盡可能保持代碼精簡就更加重要.因為在程序裡會有一些沒必要重複的記憶體開銷,如果你用枚舉(盡管你不應該用枚舉),每個程序都需要建立和初始化

這些常量。其他擴充卡和臨時變量裡的抽象或其他開銷也會重複.

另一個需要關心的多程序問題是他們之間存在的依賴. 如果你的app有一個在預設程序裡的Content Provider, 這個程序同時也處理UI, 背景程序裡的代碼要通路Content Provider

就需要UI程序也留在記憶體裡. 如果你的目标是讓背景程序可以獨立于重量級的UI程序, 它就不能依賴UI程序裡的Content Provider或Service.