前一段時間自己學習了下Android內存管理相關的東西,在部門內部做了一次技術分享。作為一個在公開場合會靦腆,上不了臺面,表達能力也不行的人來說是一個不小的進步。一直很佩服墻內墻外的牛人們堅持寫博客和大家分享的精神。嗯,今天就將上次的技術分享寫在博客里面,希望對大家有幫助。
下面分為4個部分來闡述:
1.Linux vs. Windows
兩者都能將物理內存中長時間不使用的內容轉移到磁盤空間上,再次訪問時才加載回內存。
Windows只在應用程序需要的時候才會分配內存,不能充分充分利用內存。
Linux充分利用物理內存空間(RAM)及其高速讀寫特性,將程序調用過程中的硬盤數據讀入內存,提高數據訪問性能。
Android是基于Linux系統的,繼承了Linux的很多優秀特性。但是Android沒有可交換磁盤空間(swap space),所有的內存都存在于RAM中,這使得Android系統釋放內存的唯一方式就是釋放引用的對象。
2.Android進程和內存
Android進程包含2種:
(1)Native進程:采用C/C++實現,不包含dalvik實例的進程。/system/bin/目錄下面的文件運行后都是以native進程存在的。
(2)Java進程:Android中運行于dalvik虛擬機上的進程。dalvik虛擬機的宿主進程由fork()系統調用創建,所以每一個Java進程都存在于一個Native進程中。由于每個Java存在一個虛擬機實例,因此Java進程內存分配要比Native進程復雜。
3.Android中的java程序為啥容易OOM?
Android對dalvik的vm heapsize作了限制,當Java進程申請的內存超過閾值時,會拋出OOM異常。
程序發生OOM并不能說明RAM不足,只能說明是Java heap超出了dalvik vm heapsize的閾值。
當RAM內存不足時,memory killer會殺掉低優先級進程,保證高優先級進程有更多內存。
Google之所以這樣設計,處于以下考慮:
為了讓比較多的進程同時常駐內存,這樣程序啟動時不需要每次都加載到內存,能夠給用戶更快的響應。通過限制每個應用程序的內存,使得Android系統內存中同時常駐多個進程。
4.如何繞過dalvik vm heapsize的限制
(1)創建子進程
(2)使用JNI在native heap上申請內存(推薦)
(3)使用RAM中的顯存空間
1.Use services sparingly
Service
,當后臺任務運行完成時及時關閉Service
IntentService
取代Service
,當后臺任務完成時自動結束服務自身原因:Service啟動時,Service所在進程會保持運行狀態,Service所占用的內存不會釋放,使得進程占用資源過多,因此系統LRU cache中能同時保持的進程數減少,應用程序的切換變得更低效。由于沒有足夠的進程來處理系統中的Service,也會導致系統穩定性變差。
2.當UI不可見或內存緊張時,釋放內存:
在Activity
的回調方法onTrimMemory(int level)
中根據level的不同釋放內存。
進程不在緩存中:
TRIM_MEMORY_RUNNING_MODERATE
應用程序正在運行,并且處于非killable狀態,此時設備內存低(low),系統主動殺LRU緩存中的進程TRIM_MEMORY_RUNNING_LOW
應用程序正在運行,并且處于非killable狀態,此時設備內存很低(much lower),需要釋放沒用的資源TRIM_MEMORY_RUNNING_CRITICAL
應用程序正在運行,但是系統已殺死LRU緩存中的大部分進程,此時需要釋放所有不至關重要的資源。如果系統不能回收足夠的內存,就會清掉LRU中所有的進程以及服務進程。 進程在LRU緩存中:
TRIM_MEMORY_BACKGROUND
系統低內存下運行,程序進程位于LRU緩存列表的開頭位置。雖然程序進程被kill的概率不大,但是系統可能正在殺LRU中的進程。你需要釋放容易恢復的資源以便程序進程還在LRU list中,當從其他App返回時,能快速恢復現場。TRIM_MEMORY_MODERATE
系統低內存下運行,程序進程位于LRU緩存列表的中間位置。你的進程被殺掉的可能性變大。TRIM_MEMORY_COMPLETE
系統低內存下運行,程序進程最先容易被系統殺死。你需要釋放所有對于恢復程序狀態不至關重要的資源。 API14開始有onTrimMemory()
回調;API 14以下使用的是onLowMemory()
,等價于TRIM_MEMORY_COMPLETE
3.恰當使用Bitmap
加載bitmap時,盡量保證bitmap分辨率和屏幕分辨率匹配,對于大分辨率的bitmap需要進行壓縮處理。
4.使用SparseArray
,SparseBooleanArray
和LongSparseArray
等優化的數據容器代替HashMap
5.使用static const代替enum
6.非必要情況下,少用抽象
7.對于序列化數據,使用nano protobuf
8.盡量少使用依賴注入框架
9.謹慎使用第三方庫
10.使用ProGuard去除不必要的代碼
11.apk打包簽名時,使用zipalign工具對齊
12.使用多進程
一般情況下,大多數應用程序都不需要使用多進程。只有對于需要在后臺和前臺同時運行,并且前后臺單獨管理的程序才涉及到多進程(如音樂播放器)。
使用多進程方法:
<service android:name=".PlaybackService" android:process=":background" />
一個空進程占用大概1.4MB內存,如果在該進程中操作UI,內存占用將是原來的好幾倍。因此如果使用多進程,一般一個進程用于UI,其他進程不要有任何UI相關操作。
1.分析Logcat信息
D/dalvikvm:<GC_Reason><Amount_freed>,<Heap_stats>,<External_memory_stats>,<Pause_time> 參數解釋:
2.利用Eclipse的DDMS視圖自帶的內存分析工具
3.使用adb命令行
adb shell dumpsys meminfo <package_name>
4.使用MAT內存分析工具
下載eclipse插件memory analyze tool
操作DDMS -> Dump HPROF file -> save,分析Histigram view和Dominator tree內容。
重要概念:shallow heap和retained heap
1.GC原理
GC會選擇一些還存活的對象作為內存遍歷的根節點GC Roots(如thread stack中的變量,JNI中的全局變量,zygote中的class loader對象等),對heap進行遍歷,沒有被直接或間接遍歷到的引用會被GC回收,能被遍歷到的不能被回收。
內存泄露:某些不再使用的對象被GC Roots引用,導致不能回收,使實際可使用內存變小。
2.引起內存泄露的因素
(1)長時間保持對Activity
,Context
,View
,Drawable
和其他對象的引用
(2)非靜態內部類
(3)持有對象的時間超出需要的時間
3.常見的內存泄露
(1)非靜態內部類的靜態實例
(2)Activity
使用靜態成員
(3)Handler
、HandlerThread
使用時的問題
(4)register某個對象后缺少對應的unregister操作
(5)集合對象未清理,資源對象未關閉
(6)Dialog或PopupWindow未關閉引起的window leak
(7)不良代碼造成的壓力。如Bitmap使用不當;構造adapter時,沒有使用緩存的convertView;在循環方法中創建對象。
4.改進建議
WeakReference