Android開發(fā)過程當中澜建,軟件卡頓、軟件黑屏退出等等現(xiàn)象都跟內存相關钱烟,安卓軟件與ios軟件體驗同樣是流暢度差距很大租悄,所以我們在搭建架構和開發(fā)軟件過程當中一定要注意內存的管理和代碼的優(yōu)化
Android系統(tǒng)對軟件內存的分配機制
Android 為每個應用分配內存時,采用彈性的分配方式逻锐,即剛開始并不會給應用分配很多的內存夫晌,而是給每一個進程分配一個「夠用」的內存大小,這個大小值是根據(jù)每一個設備的實際的物理內存大小來決定的昧诱。隨著應用的運行和使用晓淀,Android 會為應用分配一些額外的內存大小。但是分配的大小是有限度的盏档,系統(tǒng)不可能為每一個應用分配無限大小的內存要糊。總之妆丘,Android 系統(tǒng)需要最大限度的讓更多的進程存活在內存中锄俄,以保證用戶再次打開應用時減少應用的啟動時間,提高用戶體驗勺拣。關于 Android 系統(tǒng)內存管理機制的相關細節(jié)奶赠,推薦大家閱讀下這兩篇文章:談談 Android 的內存管理機制、Android 操作系統(tǒng)的內存回收機制药有。
避免內存溢出
內存溢出(Out Of Memory 簡稱 OOM)毅戈,簡單地說內存溢出就是指程序運行過程中,申請的內存大于系統(tǒng)能夠提供的內存愤惰,導致無法申請到足夠的內存苇经,于是就發(fā)生了內存溢出。
減小對象的內存占用
避免 OOM 的第一步就是要盡量減少新分配出來的對象占用內存的大小宦言,盡量使用更加輕量的對象扇单。
使用更加輕量的數(shù)據(jù)結構
例如,我們可以考慮使用 ArrayMap/SparseArray 而不是 HashMap 等傳統(tǒng)數(shù)據(jù)結構奠旺。
通常的 HashMap 的實現(xiàn)方式更加消耗內存蜘澜,因為它需要一個額外的實例對象來記錄 Mapping 操作施流。另外,SparseArray 更加高效在于他們避免了對 key 與 value 的 autobox 自動裝箱鄙信,并且避免了裝箱后的解箱瞪醋。
正確的使用枚舉類型
枚舉類型(Enums),是 Java 語言中的一個特性装诡。但 Android 官方強烈建議不要在 Android 應用里面使用到 Enums银受,是因為枚舉類型在編譯之后會生成很多內部類,在移動設備上內存比較珍貴顯然會很占用內存鸦采。想了解枚舉的實現(xiàn)細節(jié)可以查看 Java 枚舉類型的實現(xiàn)原理 這篇文章蚓土。
所以了解了枚舉類型的實現(xiàn)原理可以發(fā)現(xiàn),在 Android 程序里不是不可以使用枚舉類型赖淤,而是推薦使用蜀漆。合理的使用枚舉類型可以做到一些很優(yōu)雅的操作,比如單例模式咱旱。
由反編譯后的代碼可知确丢,INSTANCE被聲明為static的,在類加載過程中吐限,虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確的加鎖鲜侥、同步。所以诸典,枚舉實現(xiàn)是在實例化時是線程安全描函。
在序列化的時候 Java 僅僅是將枚舉對象的 name 屬性輸出到結果中,反序列化的時候則是通過java.lang.Enum的 valueOf 方法來根據(jù)名字查找枚舉對象狐粱。同時舀寓,編譯器是不允許任何對這種序列化機制的定制的,因此禁用了 writeObject肌蜻、readObject互墓、readObjectNoData、writeReplace 和 readResolve 等方法蒋搜。
普通的 Java 類的反序列化過程中篡撵,會通過反射調用類的默認構造函數(shù)來初始化對象。所以豆挽,即使單例中構造函數(shù)是私有的育谬,也會被反射給破壞掉。由于反序列化后的對象是重新 new 出來的帮哈,所以這就破壞了單例膛檀。
但是,枚舉的反序列化并不是通過反射實現(xiàn)的。所以宿刮,也就不會發(fā)生由于反序列化導致的單例破壞問題。
減小 Bitmap 對象的內存占用
Bitmap 是一個極容易消耗內存的大胖子私蕾,關于 Bitmap 內存占用大小詳情請參閱 Android 坑檔案:你的 Bitmap 究竟占多大內存僵缺?,所以減小創(chuàng)建出來的 Bitmap 的內存占用是很重要的踩叭,通常來說有下面 2 個措施:
縮放比例
在把圖片載入內存之前磕潮,我們需要先計算出一個合適的縮放比例,避免不必要的大圖載入容贝。
解碼格式
選擇 ALPHA_8/ARGB_4444/ARGB_8888/RBG_565自脯,存在很大差異。
模式描述占用字節(jié)
ALPHA_8Alpha 由 8 位組成1B
ARGB_44444 個 4 位組成 16 位斤富,每個色彩元素站 4 位2B
ARGB_88884 個 8 為組成 32 位膏潮,每個色彩元素站 8 位(默認)4B
RGB_565R 為 5 位,G 為 6 位满力,B 為 5 位共 16 位焕参,沒有Alpha2B
使用更小的圖片
在設計給到資源圖片的時候,我們需要特別留意這張圖片是否存在可以壓縮的空間油额,是否可以使用一張更小的圖片叠纷。
盡量使用更小的圖片不僅僅可以減少內存的使用,還可以避免出現(xiàn)大量的 InflationException潦嘶。假設有一張很大的圖片被 XML 文件直接引用涩嚣,很有可能在初始化視圖的時候就會因為內存不足而發(fā)生 InflationException,這個問題的根本原因其實是發(fā)生了 OOM掂僵。
JPG vs PNG vs WebP
不了解這三種圖片格式的建議看下 JPG 和 PNG 有什么區(qū)別航厚?、WebP 原理和 Android 支持現(xiàn)狀介紹 這兩篇文章锰蓬。
圖片壓縮
圖片壓縮相關知識推薦看下騰訊音樂技術團隊的Android 中圖片壓縮分析(上)阶淘、Android 中圖片壓縮分析(下)兩篇文章。
了解了圖片壓縮的相關知識互妓,我們可以自己寫算法來實現(xiàn)圖片壓縮溪窒,也可以使用優(yōu)秀的開源庫,比如:魯班冯勉。
內存對象的重復利用
除了減小對象對內存的占用澈蚌,合理的復用內存對象也是很好避免內存溢出的方式。
大多數(shù)對象的復用灼狰,最終實施的方案都是利用對象池技術宛瞄,要么是在編寫代碼的時候顯式的在程序里面去創(chuàng)建對象池,然后處理好復用的實現(xiàn)邏輯交胚,要么就是利用系統(tǒng)框架既有的某些復用特性達到減少對象的重復創(chuàng)建份汗,從而減少內存的分配與回收盈电。
LruCache
在 Android 中最常用的一個緩存算法是 LRU(Least Recently Use),就是當超出緩存容量的時候杯活,就優(yōu)先淘汰鏈表中最近最少使用的那個數(shù)據(jù)匆帚。
使用 LruCache 可以緩存 Bitmap 對象,相同LruCache 只是對內存中對象有效旁钧,如果我們想把圖片吸重、視頻等文件緩存在磁盤上可以使用 JakeWharton 大神開源的 DiskLruCache。
使用 Glide
Glide 是一個快速高效的 Android 圖片加載庫歪今,注重于平滑的滾動嚎幸。Glide 提供了易用的 API,高性能寄猩、可擴展的圖片解碼管道(decode pipeline)嫉晶,以及自動的資源池技術。
Glide 也是 Google 推薦過的開源項目田篇,詳見:Glide车遂。
復用系統(tǒng)自帶的資源
Android 系統(tǒng)本身內置了很多的資源(例如:字符串慌申、顏色颗胡、圖片、動畫箕昭、樣式以彬呻、簡單布局等等)衣陶,這些資源都可以在應用程序中直接引用。
這樣做不僅僅可以減少應用程序的自身負重闸氮,減小 APK 的大小剪况,另外還可以一定程度上減少內存的開銷,復用性更好蒲跨。但是也有必要留意 Android 系統(tǒng)的版本差異性译断,對那些不同系統(tǒng)版本上表現(xiàn)存在很大差異,不符合需求的情況或悲,還是需要應用程序自身內置進去孙咪。
復用 ConvertView
在 ListView、GridView 等出現(xiàn)大量重復子組件的視圖里面對 ConvertView 的復用巡语。
onLowMemory()
OnLowMemory 是 Android 提供的API翎蹈,在系統(tǒng)內存不足,所有后臺程序(優(yōu)先級為 Background 的進程男公,不是指后臺運行的進程)都被殺死時荤堪,系統(tǒng)會調用 OnLowMemory。
系統(tǒng)提供的回調有:Application、Activity澄阳、Fragementice拥知、Service、ContentProvider碎赢。
onTrimMemory()
OnTrimMemory 是 Android 4.0 之后提供的 API低剔,系統(tǒng)會根據(jù)不同的內存狀態(tài)來回調。
系統(tǒng)提供的回調有:Application揩抡、Activity户侥、Fragementice镀琉、Service峦嗤、ContentProvider。
OnTrimMemory的參數(shù)是一個 int 數(shù)值屋摔,代表不同的內存狀態(tài)烁设。
當 App 在前臺運行時,該函數(shù)的 level (從低到高)有:
TRIM_MEMORY_RUNNING_MODERATE
系統(tǒng)開始運行在低內存狀態(tài)下 App 正在運行钓试,不會被殺掉装黑。
TRIM_MEMORY_RUNNING_LOW
系統(tǒng)運行在更加低內存狀態(tài)下,App 在運行弓熏,不會被殺掉 App 可以清理一些資源來保證系統(tǒng)的流暢恋谭。
TRIM_MEMORY_RUNNING_CRITICAL
系統(tǒng)運行在相當?shù)蛢却鏍顟B(tài)下,App 在運行挽鞠,且系統(tǒng)不認為可以殺掉此 App疚颊,系統(tǒng)要開始殺掉后臺進程。此時信认,App 應該去釋放一些不重要的資源材义。
當 App 在后臺運行時,level 狀態(tài)有:
TRIM_MEMORY_UI_HIDDEN:
App 的 UI 不可見嫁赏,App 可以清理 UI 使用的較大的資源其掂。
當 App 進入后臺 LRU List 時:
TRIM_MEMORY_BACKGROUND
系統(tǒng)運行在低內存下,App 進程在 LRU List 開始處附近潦蝇,盡管 App 沒有被殺掉的風險款熬,但是系統(tǒng)也許已經正在殺后臺進程。App 應該清理一些容易恢復的資源攘乒。
TRIM_MEMORY_MODERATE
系統(tǒng)運行在低內存下华烟,App 進程在 LRU List 中間處附件,App 此時有被殺的可能持灰。
TRIM_MEMORY_COMPLETE
系統(tǒng)運行在低內存下盔夜,App 是首先被殺的選擇之一,App 應該及時清理掉恢復 App 到前臺狀態(tài),不重要的所有資源喂链。
另外返十,一個 App 占用內存越多,則系統(tǒng)清理后臺 LRU List 時椭微,越可能優(yōu)先被清理洞坑。所以,內存使用我們要謹慎使用蝇率。
避免在 onDraw 方法里面執(zhí)行對象的創(chuàng)建
類似 onDraw 等頻繁調用的方法迟杂,一定需要注意避免在這里做創(chuàng)建對象的操作,因為他會迅速增加內存的使用本慕,而且很容易引起頻繁的 GC排拷,甚至是內存抖動。
序列化
?Android 中實現(xiàn)序列化一般用 Serializable 和 Parcelable 兩種方式锅尘。
兩者最大的區(qū)別在于 存儲媒介的不同监氢,Serializable 使用 I/O 讀寫存儲在硬盤上,而 Parcelable 是直接 在內存中讀寫藤违。很明顯浪腐,內存的讀寫速度通常大于 IO 讀寫,所以在 Android 中傳遞數(shù)據(jù)優(yōu)先選擇 Parcelable顿乒。Serializable 會使用反射议街,序列化和反序列化過程需要大量 I/O 操作, Parcelable 自已實現(xiàn)封送和解封(marshalled &unmarshalled)操作不需要用反射璧榄,數(shù)據(jù)也存放在 Native 內存中特漩,效率要快很多。
Parcelable 的性能比 Serializable 好犹菱,在內存開銷方面較小拾稳,所以在內存間數(shù)據(jù)傳輸時推薦使用 Parcelable(如 Activity 間傳輸數(shù)據(jù))。
而 Serializable 可將數(shù)據(jù)持久化方便保存腊脱,所以在需要保存或網(wǎng)絡傳輸數(shù)據(jù)時選擇 Serializable访得,因為 Android 不同版本 Parcelable 可能不同,所以不推薦使用 Parcelable進行數(shù)據(jù)持久化.
StringBuilder
在有些時候陕凹,代碼中會需要使用到大量的字符串拼接的操作悍抑,這種時候有必要考慮使用 StringBuilder 來替代頻繁的+。
避免內存泄漏
內存泄漏(Memory Leak)指程序運行過程中分配內存給臨時變量杜耙,用完之后卻沒有被 GC 回收搜骡,始終占用著內存,既不能被使用也不能分配給其他程序佑女,于是就發(fā)生了內存泄漏记靡。
內存泄露有時不嚴重且不易察覺谈竿,這樣開發(fā)者就不知道存在內存泄露,但有時也會很嚴重摸吠,甚至會提示你 OOM空凸。
Context 的泄露
在 Android 開發(fā)中,最容易引發(fā)的內存泄漏問題的是 Context寸痢。比如 Activity 的 Context呀洲,就包含大量的內存引用,一旦泄漏了 Context啼止,也意味泄漏它指向的所有對象道逗。
造成 Activity 泄漏的常見原因:
靜態(tài)引用 Activity
在類中定義了靜態(tài) Activity 變量,把當前運行的 Activity 實例賦值于這個靜態(tài)變量献烦。如果這個靜態(tài)變量在 Activity 生命周期結束后沒有清空滓窍,就導致內存泄漏。
因為 static 變量是貫穿這個應用的生命周期的仿荆,所以被泄漏的 Activity 就會一直存在于應用的進程中贰您,不會被垃圾回收器回收坏平。
單例中保存 Activity
在單例模式中拢操,如果 Activity 經常被用到,那么在內存中保存一個 Activity 實例是很實用的舶替。
但是由于單例的生命周期是應用程序的生命周期令境,這樣會強制延長 Activity 的生命周期,這是相當危險而且不必要的顾瞪,無論如何都不能在單例子中保存類似 Activity 的對象舔庶。
在調用 Singleton 的 getInstance() 方法時傳入了 Activity。那么當 instance 沒有釋放時陈醒,這個 Activity 會一直存在惕橙,因此造成內存泄露。
考慮使用 Application Context 而不是 Activity Context
對于大部分非必須使用 Activity Context 的情況(Dialog 的 Context 就必須是 Activity Context)钉跷,我們都可以考慮使用 Application Context 而不是 Activity 的 Context弥鹦,這樣可以避免不經意的 Activity 泄露。
Inner Classes
內部類的優(yōu)勢可以提高可讀性和封裝性爷辙,而且可以訪問外部類彬坏,不幸的是,導致內存泄漏的原因膝晾,就是內部類持有外部類實例的強引用(例如在內部類中持有 Activity 對象)栓始。
解決方法:
將內部類變成靜態(tài)內部類;
如果有強引用 Activity 中的屬性血当,則將該屬性的引用方式改為弱引用幻赚;
在業(yè)務允許的情況下禀忆,當 Activity 執(zhí)行 onDestory 時,結束這些耗時任務落恼。
避免使用異步回調
異步回調被執(zhí)行的時間不確定油湖,很有可能發(fā)生在 Activity 已經被銷毀之后,這不僅僅很容易引起 crash领跛,還很容易發(fā)生內存泄露乏德。
注意臨時 Bitmap 對象的及時回收
雖然在大多數(shù)情況下,我們會對 Bitmap 增加緩存機制吠昭,但是在某些時候喊括,部分 Bitmap 是需要及時回收的。
例如:臨時創(chuàng)建的某個相對比較大的 Bitmap 對象矢棚,在經過變換得到新的 Bitmap 對象之后郑什,應該盡快回收原始的 Bitmap,這樣能夠更快釋放原始 Bitmap 所占用的空間蒲肋。
需要特別留意的是 Bitmap 類里面提供的 createBitmap() 方法蘑拯,這個函數(shù)返回的 Bitmap 有可能和 source bitmap 是同一個,在回收的時候兜粘,需要特別檢查 source bitmap 與 return bitmap 的引用是否相同申窘,只有在不等的情況下,才能夠執(zhí)行 source bitmap 的 recycle 方法孔轴。
監(jiān)聽器的注銷
在 Android 程序里面存在很多需要 register 與 unregister 的監(jiān)聽器剃法,我們需要確保在合適的時候及時 unregister 那些監(jiān)聽器。自己手動 add 的 listener路鹰,需要記得及時 remove 這個 listener贷洲。
?Cursor 對象是否及時關閉
在程序中我們經常會進行查詢數(shù)據(jù)庫的操作,但時常會存在不小心使用 Cursor 之后沒有及時關閉的情況晋柱。這些 Cursor 的泄露优构,反復多次出現(xiàn)的話會對內存管理產生很大的負面影響,我們需要謹記對 Cursor 對象的及時關閉雁竞。
緩存容器中的對象泄漏
有時候钦椭,我們?yōu)榱颂岣邔ο蟮膹陀眯园涯承ο蠓诺骄彺嫒萜髦校墒侨绻@些對象沒有及時從容器中清除浓领,也是有可能導致內存泄漏的玉凯。
例如:針對 2.3 的系統(tǒng),如果把 drawable 添加到緩存容器联贩,因為 drawable 與 View 的強應用漫仆,很容易導致 activity 發(fā)生泄漏。而從 4.0 開始泪幌,就不存在這個問題盲厌。解決這個問題署照,需要對 2.3 系統(tǒng)上的緩存 drawable 做特殊封裝,處理引用解綁的問題吗浩,避免泄漏的情況建芙。
?WebView 的泄漏
Android 中的 WebView 存在很大的兼容性問題,不僅僅是 Android 系統(tǒng)版本的不同對 WebView 產生很大的差異懂扼,另外不同的廠商出貨的 ROM 里面 WebView 也存在著很大的差異禁荸。更嚴重的是標準的 WebView 存在內存泄露的問題,看這里 WebView causes memory leak - leaks the parent Activity阀湿。
所以通常根治這個問題的辦法是為 WebView 開啟另外一個進程赶熟,通過 AIDL 與主進程進行通信, WebView 所在的進程可以根據(jù)業(yè)務的需要選擇合適的時機進行銷毀陷嘴,從而達到內存的完整釋放映砖。
Lint Tool
Lint 是Android Studio 提供的代碼掃描分析工具,它可以幫助我們發(fā)現(xiàn)代碼結構/質量問題灾挨,同時提供一些解決方案邑退,而且這個過程不需要我們手寫測試用例。
Lint 發(fā)現(xiàn)的每個問題都有描述信息和等級(和測試發(fā)現(xiàn) bug 很相似)劳澄,我們可以很方便地定位問題地技,同時按照嚴重程度進行解決。
點擊Analyze > Inspect Code可打開 Lint 工具浴骂。
詳細使用介紹可查看 Android 開發(fā)文檔 - 使用 Lint 改進您的代碼乓土、Android 性能優(yōu)化:使用 Lint 優(yōu)化代碼宪潮、去除多余資源 這兩篇文章溯警,使用比較簡單,網(wǎng)上介紹資源很多狡相,這里不再詳細介紹梯轻。
adb dumpsys
dumpsys 是 Android 系統(tǒng)提供的一個工具,可以查看系統(tǒng)服務的相關信息尽棕,dumpsys 通過 adb 命令來調用喳挑。
查看指定進程包名的內存使用情況:
$ adb shell dumpsys meminfo <包名>
輸出內容如下:
當然 dumpsys 的功能還有很多,可以查看 Android? 開發(fā)文檔 - dumpsys滔悉、dumpsys 命令用法 這兩篇文章了解更多用法伊诵。
Heap Viewer
Heap Viewer 是 DDMS 中的一個工具,實時查看 App 分配的內存大小和空閑內存大小回官,可幫助查找內存泄露曹宴。Heap Viewer 支持 5.0 及以上的系統(tǒng),現(xiàn)在已經棄用歉提,官方推薦使用 Memory Profiler 來查看 App 內存分配情況笛坦。
Memory Profiler
Memory Profiler 是 Android Profiler 中的一個組件区转,可幫助開發(fā)者識別導致應用卡頓、OOM 和內存泄露版扩。它顯示一個應用內存使用量的實時圖表废离,可以捕獲堆轉儲、強制執(zhí)行垃圾回收以及跟蹤內存分配礁芦。
可以在View > Tool Windows > Android Profiler中打開 Memory Profiler 界面蜻韭。
Memory Profile 的具體使用可查看 Android 開發(fā)文檔 - 使用 Memory Profiler 查看 Java 堆和內存分配、Android Studio 3.0 利用 Android Profiler 測量應用性能 這兩篇文章柿扣。
MAT
MAT(Memory Analyzer Tool)湘捎,一個基于 Eclipse 的內存分析工具,是一個快速窄刘、功能豐富的 Java Heap 分析工具窥妇,它可以幫助我們查找內存泄漏和減少內存消耗。
使用內存分析工具從眾多的對象中進行分析娩践,快速的計算出在內存中對象的占用大小活翩,看看是誰阻止了垃圾收集器的回收工作,并可以通過報表直觀的查看到可能造成這種結果的對象翻伺。
當然 MAT 也有獨立的不依賴 Eclipse 的版本材泄,只不過這個版本在調試 Android 內存的時候,需要將 Android Studio 生成的文件進行轉換吨岭,才可以在獨立版本的 MAT 上打開拉宗。.
一般情況下使用 Memory Profiler 就可以檢測出內存泄露的大致位置,使用 MAT 可以更詳細的分析內存的具體情況辣辫。
MAT 下載 點擊這里旦事,使用教程可查看 Eclipse Wiki - MemoryAnalyzer,也可以參考 Android 內存優(yōu)化之一:MAT 使用入門急灭、Android內存優(yōu)化之二:MAT使用進階 這兩篇文章姐浮。
LeakCanary
LeakCanary 是一個用于檢測 Android 內存泄漏的開源庫,上面介紹的 Memory Profiler葬馋、MAT 使用起來比較復雜卖鲤,LeakCanary 堪稱傻瓜式的內存泄露檢測工具。
使用方式詳見https://square.github.io/leakcanary畴嘶,也可參考 LeakCanary 中文使用說明 這篇文章蛋逾。