優化Android應用內存的若干方法

轉載自:http://my.oschina.net/chaselinfo/blog/198172


使用保守的Service

如果你的應用需要使用 service 在後臺執行業務功能, 除非是一直在進行活動的工作(比如每隔幾秒向服務器端請求數據之類)否則不要讓它一直保持在後臺運行. 並且, 當你的 service 執行完成但是停止失敗時要小心 service 導致的內存泄露問題.

當你啓動 service 時, 系統總是優先保持服務的運行. 這會導致內存應用效率非常低, 因爲被該服務使用的內存不能做其它事情. 也會減少系統一直保持的LRU緩存處理數目, 使不同的app切換效率降低. 當前所有 service 的運行會導致內存不夠不能維持正常系統的運行時, 系統會發生卡頓的現象嚴重時能導致系統不斷重啓.

最好的方式是使用 IntentSevice 控制 service 的生命週期, 當使用 intent 開始任務後, 該 service 執行完所有的工作時會自動停止.

在android應用中當不需要使用常駐 service 執行業務功能而去使用一個常駐 service 是最糟糕的內存管理方式之一. 所以不要貪婪的使用 service 使你的應用一直運行狀態. 這樣不僅使你因爲內存的限制提高了應用運行的風險, 也會導致用戶發現這些異常行爲後而卸載應用.


當視圖變爲隱藏狀態後釋放內存

當用戶跳轉到不同的應用並且你的視圖不再顯示時, 你應該釋放應用視圖所佔的資源. 這時釋放所佔用的資源能顯著的提高系統的緩存處理容量, 並且對用戶的體驗質量有直接的影響.

當實現當前 Activity 類的 onTrimMemory() 回調方法後, 用戶離開視圖時會得到通知. 使用該方法可以監聽 TRIM_MEMORY_UI_HIDDEN 級別, 當你的視圖元素從父視圖中處於隱藏狀態時釋放視圖所佔用的資源.

注意只有當你應用的所有視圖元素變爲隱藏狀態時你的應用才能收到 onTrimMemory() 回調方法的 TRIM_MEMORY_UI_HIDDEN . 這個和 onStop() 回調方法不同, 該方法只有當 Activity 的實例變爲隱藏狀態, 或者有用戶移動到應用中的另外的 activity 纔會引發. 所以說你雖然實現了 onStop() 去釋放 activity 的資源例如網絡連接或者未註冊的廣播接收者, 但是應該直到你收到 onTrimMemory(TRIM_MEMORY_UI_HIDDEN)纔去釋放視圖資源否則不應該釋放視圖所佔用的資源. 這裏可以確定的是如果用戶通過後退鍵從另外的 activity 進入到你的應用中, 視圖資源會一直處於可用的狀態可以用來快速的恢復 activity.


內存資源緊張時釋放內存

在應用生命週期的任何階段 onTrimMemory() 回調方法都可以告訴你設備的內存越來越低的情況, 你可以根據該方法推送的內存緊張級別來釋放資源.

  • TRIM_MEMORY_RUNNING_CRITICAL

應用處於運行狀態並且不會被殺掉, 設備使用的內存比較低, 系統級會殺掉一些其它的緩存應用.

  • TRIM_MEMORY_RUNNING_LOW

應用處於運行狀態並且不會被殺掉, 設備可以使用的內存非常低, 可以把不用的資源釋放一些提高性能(會直接影響程序的性能)

  • TRIM_MEMORY_RUNNING_CRITICAL

應用處於運行狀態但是系統已經把大多數緩存應用殺掉了, 你必須釋放掉不是非常關鍵的資源, 如果系統不能回收足夠的運行內存, 系統會清除所有緩存應用並且會把正在活動的應用殺掉.

還有, 當你的應用被系統正緩存時, 通過 onTrimMemory() 回調方法可以收到以下幾個內存級別:

  • TRIM_MEMORY_BACKGROUND

系統處於低內存的運行狀態中並且你的應用處於緩存應用列表的初級階段.  雖然你的應用不會處於被殺的高風險中, 但是系統已經開始清除緩存列表中的其它應用, 所以你必須釋放資源使你的應用繼續存留在列表中以便用戶再次回到你的應用時能快速恢復進行使用.

  • TRIM_MEMORY_MODERATE

系統處於低內存的運行狀態中並且你的應用處於緩存應用列表的中級階段. 如果系運行內存收到限制, 你的應用有被殺掉的風險.

  • TRIM_MEMORY_COMPLETE

系統處於低內存的運行狀態中如果系統現在沒有內存回收你的應用將會第一個被殺掉. 你必須釋放掉所有非關鍵的資源從而恢復應用的狀態.

因爲 onTrimMemory() 是在級別14的android api中加入的, 所以低版本的要使用 onLowMemory() 方法, 該方法大致相當於 TRIM_MEMORY_COMPLETE 事件.

注意: 當系統開始清除緩存應用列表中的應用時, 雖然系統的主要工作機制是自下而上, 但是也會通過殺掉消費大內存的應用從而使系統獲得更多的內存, 所以在緩存應用列表中消耗更少的內存將會有更大的機會留存下來以便用戶再次使用時進行快速恢復.

檢查可以使用多大的內存

前面提到, 不同的android設備系統擁有的運行內存各自都不同, 從而不同的應用堆內存的限制大小也不一樣. 你可以通過調用 ActivityManager 中的 getMemoryClass() 函數可以通過以兆爲單位獲取當前應用可用的內存大小, 如果你想獲取超過最大限度的內存則會發生 OutOfMemoryError .

有一個特別的情況, 可以在 manifest 文件中的 <application> 標籤中設置 largeHeap 屬性的值爲 "true"時, 當前應用就可以獲取到系統分配的最大堆內存. 如果你設置了該值, 可以通過 ActivityManager 的 getLargeMemoryClass() 函數獲取最大的堆內存.

然後, 只有一小部分應用需要消耗大量堆內存(比如大照片編輯應用). 從來不需要使用大量內存僅僅是因爲你已經消耗了大量的內存並且必須快速修復它, 你必須使用它是因爲你恰好知道所有的內存已經被分配完了而你必須要保留當前應用不會被清除掉. 甚至當你的應用需要消耗大量內存時, 你應該儘可能的避免這種需求. 使用大量內存後, 當你切換不同的應用或者執行其它類似的操作時, 因爲長時間的內存回收會導致系統的性能下降從而漸漸的會損害整個系統的用戶體驗.

另外, 大內存不是所有的設備都相同.  當跑在有運行內存限制的設備上時, 大內存和正常的堆內存是一樣的.  所以如果你需要大內存, 你就要調用 getMemoryClass() 函數查看正常的堆內存的大小並且儘可能使內存使用情況維護在正常堆內存之下.

避免在 bitmaps 中浪費內存

當你加載 bitmap 時, 需要根據分辨率來保持它的內存時最大爲當前設備的分辨率, 如果下載下來的原圖爲高分辨率則要拉伸它. 要小心bitmap的分辨率增加後所佔用的內存也要進行相應的增加, 因爲它是根據x和y的大小來增加內存佔用的.

注意: 在 Android 2.3.x(api level 10)以下, 無論圖片的分辨率多大 bitmap 對象在內存中始終顯示相同大小, 實際的像素數據被存儲在底層 native 的內存中(c++內存). 因爲內存分析工具無法跟蹤 native 的內存狀態所有調試 bitmap 內存分配變得非常困難. 然而, 從 Android 3.0(api level 11)開始, bitmap 對象的內存數據開始在應用程序所在Dalvik虛擬機堆內存中進行分配, 提高了回收機率和調試的可能性. 如果你在老版本中發現 bitmap 對象佔用的內存大小始終一樣時, 切換設備到系統3.0或以上來進行調試.

使用優化後的數據容器

利用 Android 框架優化後的數據容器, 比如 SparseArraySparseBooleanArray 和 LongSparseArray. 傳統的 HashMap 在內存上的實現十分的低效因爲它需要爲 map 中每一項在內存中建立映射關係. 另外, SparseArray類非常高效因爲它避免系統中需要自動封箱(autobox)的key和有些值.

知道內存的開銷

在你設計應用各個階段都要很謹慎的考慮所使用的語言和庫帶來的內存上的成本和開銷. 通常情況下, 表面上看起來無害的會帶來巨大的開銷,  下面在例子說明:

  • 當枚舉(enum)成爲靜態常量時超過正常兩倍以上的內存開銷, 在 android 中你需要嚴格避免使用枚舉
  • java 中的每個類(包含匿名內部類)大約使用500個字節
  • 每個類實例在運行內存(RAM)中佔用12到16個字節
  • 在 hashmap 中放入單項數據時, 需要爲額外的其它項分配內存, 總共佔用32個字節

使用很多的不必要類和對象時, 增加了分析堆內存問題的複雜度.

當心抽象代碼

通常來說, 使用簡單的抽象是一種好的編程習慣, 因爲一定程度上的抽象可以提供代碼的伸縮性和可維護性. 然而抽象會帶來非常顯著的開銷: 需要執行更多的代碼, 需要更長時間和更多的運行內存把代碼映射到內存中, 所以如果抽象沒有帶來顯著的效果就儘量避免.

使用納米 Protocol buffers 作爲序列化數據

Protocol Buffers 是 Google 公司開發的一種數據描述語言,類似於XML能夠將結構化數據序列化. 但是它更小, 更快, 更簡單. 如果你決定使用它作爲你的數據, 你必須在你的客戶端代碼中一直使用納米 protocol buffer, 因爲正常的 protocol buffer 會產生極其冗餘的代碼, 在你的應用生會引起很多問題: 增加了使用的內存, 增加了apk文件的大小, 執行速度較慢以及會快速的把一些限定符號打入 dex 包中.

儘量避免使用依賴注入框架

使用像 Guice 和 RoboGuice 依賴注入框架會有很大的吸引力, 因爲它使我們可以寫一些更簡單的代碼和提供自適應的環境用來進行有用的測試和進行其它配置的更改. 然而這些框架通過註解的方式掃描你的代碼來執行一系列的初始化, 但是這些也會把一些我們不需要的大量的代碼映射到內存中. 被映射後的數據會被分配到乾淨的內存中, 放入到內存中後很長一段時間都不會使用, 這樣造成了內存大量的浪費.

謹慎使用外部依賴庫

許多的外部依賴庫往往不是在移動環境下寫出來的, 這樣當在移動使用中使用這些庫時就會非常低效. 所以當你決定使用一個外部庫時, 你就要承擔爲優化爲移動應用外部庫帶來的移植問題和維護負擔. 在項目計劃前期就要分析該類庫的授權條件, 代碼量, 內存的佔用再來決定是否使用該庫.

甚至據說專門設計用於 android 的庫也有潛在的風險, 因爲每個庫做的事情都不一樣. 例如, 一個庫可能使用的是 nano protobuf 另外一個庫使用的是 micro protobuf, 現在在你的應用中有兩個不同 protobuf 的實現. 這將會有不同的日誌, 分析, 圖片加載框架, 緩存, 等所有你不可預知的事情的發生. Proguard 不會保存你的這些, 因爲所有低級別的 api 依賴需要你依賴的庫裏所包含的特徵. 當你使用從外部庫繼承的 activity 時尤其會成爲一個問題(因爲這往往產生大量的依賴). 庫要使用反射(這是常見的因爲你要花許多時間去調整ProGuard使它工作)等.

也要小心不要陷入使用幾十個依賴庫去實現一兩個特性的陷阱; 不要引入大量不需要使用的代碼. 一天結束時, 當你沒有發現符合你要求的實現時, 最好的方式是創建一個屬於自己的實現.

優化整體性能

除了上述情況外, 還可以優化CPU的性能和用戶界面, 也會帶動內存的優化

使用代碼混淆去掉不需要的代碼

代碼混淆工具 ProGuard 通過去除沒有用的代碼和通過語義模糊來重命名類, 字段和方法來縮小, 優化和混淆你的代碼. 使用它能使你的代碼更簡潔, 更少量的RAM映射頁.

使用簽名工具簽名apk文件

如果構建apk後你沒有做後續的任何處理(包括根據你的證書進行簽名), 你必須運行 zipalign 工具爲你的apk重新簽名, 如果不這樣做會導致你的應用使用更多的內存, 因爲像資源這樣的東西不會再從apk中進行映射(mmap).

注意:goole play store 不接受沒有簽名的apk

分析你的內存使用情況

使用adb shell dumpsys meminfo +包名 等工具來分析你的應用在各個生命週期的內存使用情況, 這個後續博文會有所體現.

使用多進程

一種更高級的技術能管理應用中的內存, 分離組件技術能把單進程內存劃分爲多進程內存. 該技術一定要謹慎的使用並且大多數的應用都不會跑多進程, 因爲如果你操作不當反而會浪費更多的內存而不是減少內存. 它主要用於後臺和前臺能各自負責不同業務的應用程序

當你構建一個音樂播放器應用並且長時間從一個 service 中播放音樂時使用多進程處理對你的應用來說更恰當. 如果整個應用只有一個進程, 當前用戶卻在另外一個應用或服務中控制播放時, 卻爲了播放音樂而運行着許多不相關的用戶界面會造成許多的內存浪費. 像這樣的應用可以分隔爲兩個進程:一個進程負責 UI 工作, 另外一個則在後臺服務中運行其它的工作.

在各個應用的 manifest 文件中爲各個組件申明 android:process 屬性就可以分隔爲不同的進程.例如你可以指定你一運行的服務從主進程中分隔成一個新的進程來並取名爲"background"(當然名字可以任意取).

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

進程名字必須以冒號開頭":"以確保該進程屬於你應用中的私有進程.

在你決定創建一個新的進程之前必須理解對這樣做內存的影響. 爲了說明每個進程的影響, 一個基本空進程會佔用大約1.4兆的內存, 下面的堆內存信息說明這一點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

注意: 上面關鍵的數據是 private dirty 和 private clean 兩項, 第一項主要使用了大約是1.4兆的非分頁內存(分佈在Dalvik heap, native分配, book-keeping, 和庫的加載), 另外執行業務代碼使用150kb的內存.

空進程的內存佔用是相當顯著的, 當你的應用加入了許多業務後會增長得更加迅速. 下面的例子是使用activity顯示一些文字, 當前進程的內存使用狀況的分析.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
** 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

這個比上面多花費了3倍的內存, 只是在 界面 上顯示一些簡單的文字, 用了大約4兆. 從這裏可以得出一個很重要的結論:如果你的想在應用中使用多進程, 只能有一個進程來負責 UI 的工作, 在其它進程中不能出現任何 UI的工作, 否則會迅速提高內存的使用率(尤其當你加載 bitmap 資源和其它資源時). 一旦加入了UI的繪製工作就不可能會減少內存的使用了.

另外, 當你的應用超過一個進程時, 保持代碼的緊湊非常重要, 因爲現在由相同實現造成的不必要的內存開銷會複製到每一個進程中, 會造成內存浪費更嚴重的狀況出現. 例如, 你使用了枚舉, 不同的進程在內存中都會創建和初始化這些常量.並且你所有的抽象適配器和其它臨時的開銷也會和前面一樣被複制過來.

另外要關心的問題是多進程之間的依賴關係. 例如, 當應用中運行默認的進程需要爲UI進程提供內容, 後臺進程的代碼爲進程本身提供內容還要留在內存中爲UI運行提供支持, 如果你的目標是在一個擁有重量級的UI進程的應用裏擁有一個獨立運行的後臺進程, 那麼你在UI進程中則不能直接依賴它, 而要在UI進程使用 service 處理它.


發佈了51 篇原創文章 · 獲贊 20 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章