優化Android應用內存的若干方法
在app開發的各個階段中要考慮RAM的限制問題, 包括在設計階段(正式開發之前). 使用下面的不同的方法可以達到很好的效果. 當您在設計和開發Android應用時用下面的方法可以使內存運用最高效.
使用保守的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 框架優化后的數據容器, 比如 SparseArray, SparseBooleanArray 和 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"(當然名字可以任意取).
<service android:name=".PlaybackService" android:process=":background" />
進程名字必須以冒號開頭":"以確保該進程屬于你應用中的私有進程.
在你決定創建一個新的進程之前必須理解對這樣做內存的影響. 為了說明每個進程的影響, 一個基本空進程會占用大約1.4兆的內存, 下面的堆內存信息說明這一點
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顯示一些文字, 當前進程的內存使用狀況的分析.
** 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 處理它.