Android避免OOM(內存優化)

Android內存優化是性能優化很重要的一部分,而如何避免OOM又是內存優化的核心。

Android內存管理機制

android官網有一篇文章

Android是如何管理應用的進程與內存分配
Android系統的Dalvik虛擬機扮演了內存垃圾自動回收的角色。

OOM介紹(out of memory 內存溢出)

Android和java中都會出現由于不良代碼引起的內存泄露,為了使Android應用程序能夠快速高效的運行,Android每個應用程序都會有專門Dalvik虛擬機實例來運行,也就是每個程序都在屬于自己的進程中運行。
這樣,某個應用程序內存泄露僅僅只會使自己進程被kill掉不會影響其他進程(如果是system_process等系統進程出現問題,就會造成系統重啟),另一方面,系統為每一個應用程序分配了不同的內存上限,如果超過這個上限被視為內存泄露,從而被kill掉。
Dalvik Heap size因不同設備的RAM不同而有所差異,應用占用內存接近這個閥值,在嘗試分配內存就會引起outofmemoryError的錯誤。

出現OOM有幾種情況:

  1. 加載對象過大
  2. 相應資源過多,來不及加載。

解決這些問題,有:

  1. 內存引用上做一些處理,常用的有軟引用。
  2. 內存中加載圖片直接在內存中做處理(如邊界壓縮)
    這個Glide\Fresco 圖片框架可能封裝好了
    3.動態回收內存
    4.優化Delivk虛擬機的堆內存分配
    5.自定義堆內存大小

共享內存

Android應用程序的進程都是從Zygote的進程fork出來的。Zygote進程在系統啟動并載入通用的framework代碼和資源后啟動。一個新的應用程序啟動,系統就會從Zygote中fork出來一個新的進程,在新的進程中加載并允許應用程序的代碼。這使得大多數RAM pages被分配給framework的代碼,并且RAM資源能夠在應用的所有進程之間共享。

大多數static 數據被mmapped到一個進程中,這樣使得同樣的數據在進程之間能夠共享,而且在需要的時候能paged out.常見static 數據包括Dalvik code ,app resourecs,so 文件等。

大多數情況下,Android通過顯示的方式分配共享內存區域(例如ashmem或gralloc)來實現動態RAM區域能夠在不同進程之間進行共享的機制。比如,Window Surface在APP和Screen Composition之間使用共享的內存,
Cursor Buffers在Content Provider與Clients之間共享內存。

分配與回收內存

  • 每個進程的Dalvik heap都反應了使用內存的占用范圍,(Dalvik Heap Size),他可以根據需要進行增長,但是系統有一個上限。
  • HeapSize跟實際的物理內存大小是不對等的,PSS(proportional Set Size)記錄了應用程序自身占用以及和其他進程共享的內容。
  • Android不會對heap空閑區域進行做碎片整理。系統僅僅在新的內存分配之前判斷Heap的尾端剩余空間是否足夠,不夠就會觸發gc操作,從而騰出更多空閑的內存空間。gc操作(garbage collection)也就是所謂的垃圾回收,Android在適當時候觸發gc操作,將一些不再使用的對象回收,在Android高級系統針對Heap空間有一個Generational Heap Memory的模型,最近分配的對象在放在young generation區域,當停留一段時間,這個對象會被移動到old generation中,最后在移動到permanent generation區域中。系統會根據內存中不同的內存數據類型進行gc操作,young generation區域的對象更容易被銷毀,而且gc操作的速度比old generation的速度要快,時間更短。
    每個generation的內存區域都有固定的大小,隨著新的對象陸續被分配到此區域,當這些對象的大小快達到閥門值時,就會觸發gc操作。通常情況下,gc操作發生時,所有線程都是暫停的。
    如何查看本機heap size:
    ActivityManager manager=(Activity)getSystemService(Context.ACTIVITY_SERVICE); int heapsize=manager.getMemoryClass();

應用切換操作

Android系統不會再用戶切換應用的時候進行交換內存的操作,而是把不包含Foreground組件的應用進程放到LRUCache中,比如用戶啟動一個應用,系統會為它創建一個進程,但是當用戶離開這個應用,此進程不會背立即銷毀而是會放到一個Cache中,當用戶切換回來夠快速的恢復。

發生OOM的條件

通過不同的內存分配方式對不同的對象(bitmap,etc)進行操作因Android版本差異發生變化。
4.0以上,廢除了external的計數器,類似bitmap的分配改到dalvik的Java heap(堆)中申請,只要allocated+新分配的內存>=getMemoryClass()就會發生OOM。(在AS memory monitor查看內存中Dalvik Heap的實時變化)

如何避免OOM

減少OOM的第一步就是要盡量減少新分配出來的對象占用內存的大小,盡量使用更加輕量的對象。

  1. 使用更加輕量的數據結構
    考慮使用ArrayMap/SpareseArray而不是傳統的HashMap等數據結構,Android系統為移動系統設計的容器ArrayMap更加高效,占用內存更少,因為HashMap需要一個額外的實例對象來記錄Mapping的操作。而SparesArray高效的避免了key和value的自動裝箱,而且避免了裝箱后的解箱。


    關于更多ArrayMap/SparseArray的討論,請參考http://hukai.me/android-performance-patterns-season-3/的前三個段落

  2. 避免在Android中使用Enum

  3. 減少Bitmap對象的內存占用
    Bitmap是一個消耗內存的大胖子,減少創建出來的Bitmap的內存占用很重要。一般有兩種措施

  • inSampleSize:縮放比例,在把圖片載入內存之前,我們需要計算一個合適的縮放比例,避免不必要的大圖載入。
  • decode format:解碼格式,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異。
  1. 使用更小的圖片
    在設計圖片資源的時候,我們要考慮圖片是否存在可以壓縮的空間,是否能使用更小的圖片,使用小圖在xml加載資源時就不會在初始化視圖因為內存不足而發生InflationException,其根本原因就是發生了OOM。

內存對象的重復利用

Android最常用的緩存算法LRU(Least Recently Use)


  1. 復用系統自帶的資源,比如字符串、圖片、動畫、樣式、顏色、簡單布局,在應用中直接引用,減少自身負重、apk大小、減少內存的開銷、復用性更好。但需要考慮版本差異。
  2. Listview和GirdView出現大量重復子組件的視圖里面對ConvertView的復用。
  3. Bitmap對象的復用
  • 在ListView和GridView等顯示大量圖片的控件里面需要使用LRU機制緩存Bitmap.
  • 利用inBitmap的高級特性提高Android系統在Bitmap分配和釋放執行效率,inBitmap屬性可以告知Bitmap解碼器使用已經存在的內存區域而不是重新申請一塊內存區域存放Bitmap,也就是新解碼的Bitmap會使用之前那張bitmap在heap占用的內存區域,即使是上千張圖片,也只占用屏幕能放下圖片的內存
inBitmap的限制
  • SDK19以后:新申請的BItmap大小必須小于或等于前面賦值過的bitmap的大小
  • 新的Bitmap和原來的解碼格式要相同,我們可以創建包含多種類型可以重用的bitmap對象池,這樣后序的bitmap創建就可以找到合適的模板去重用。


  1. 避免在onDraw方法里面執行對象的創建
    在onDraw這種頻繁調用的方法要避免對象的創建操作,因為他會迅速增加內存的使用,引起頻繁的gc,甚至內存抖動
    5.StringBuilder
    如果代碼中有大量字符串拼接操作,使用StringBuilder代替"+"

避免對象的內存泄露

內存對象的泄露會導致不再使用的對象無法及時釋放,不僅浪費了寶貴的內存空間,后續要分配內存的時候,空間不足造成OOM。這樣,每級的generation會變小,gc更加容易觸發,引起內存抖動,帶來性能問題。

  1. 注意Activity的泄露
    Activity泄露是內存泄露最為嚴重的問題,涉及內存多,影響面廣
    兩種情形:
  • 內部類引用導致Activity的泄露
    典型的是Handler導致的Activity泄露,如果Handler中有延遲的任務或者等待執行的任務隊列過長,很可能因為Handler繼續執行造成Activity的泄露。
    引用鏈是Looper->MessageQueue->Message->handler->Activity,解決辦法是在退出UI之前執行 remove Handler消息隊列中的消息與runnable對象。或者使用Static+WeakReference的方式來判斷Handler和Activity之間存在引用關系。
  • Activity Context被傳遞到其他實例中,可能導致自身被引用而發生泄露
  1. 考慮使用Application Context而不是Activity Context
    除必須使用Activity Context的情況(Dialog的context必須是Activity),我們可以使用Application Context來避免Activity泄露
  2. 注意臨時Bitmap的及時回收
    大多數情況下,我們對Bitmap對象增加緩存機制,但是有時候部分bitmap需要及時回收。比如我們臨時創建的摸個相對大的bitmap對象,變換得到新的bitmap對象后,盡快回收原始的bitmap,及時釋放原來的空間。
  3. 注意監聽器的注銷
    android程序里面register后要及時釋放unregister那些監聽器,自己手動add的listener,要記得remove這個listener.
    5.注意緩存容器的對象泄露
    有時候我們為了提高對象的復用性,把某些對象放到緩存容器中,如果這些對象沒有及時從容器中清楚,也可能導致內存泄露,
  4. 注意webview的泄露
    Android不同版本對webview產生有很大差異,較為嚴重的問題是webview的泄露,解決辦法:為webview新開一個線程,通過AIDL與主進程通信,根據業務的需要在合適的時機進行銷毀,從而達到內存的釋放。
  5. 注意cursor對象是否關閉
    我們在對數據庫進行操作時,使用完cursor沒有及時關閉,cursor的泄露,會對內存管理帶來負面影響

內存使用策略優化

1.謹慎使用large heap
android設備由于軟硬件的差異,heap閥值不同,特殊情況下可以在manifest中使用largeheap=true聲明一個更大的heap空間,使用getLargeMemoryClass()來獲取到這個更大的空間。但是要謹慎使用,因為額外的空間會影響到系統整體的用戶體驗,并且會使每次gc的運行時間更長。切換任務時性能大打折扣,large heap并不一定能獲取到更大的heap.

  1. 綜合考慮設備內存閾值與其他因素設計合適的緩存大小
    例如,在設計ListView或者GridView的Bitmap LRU緩存的時候,需要考慮的點有:

應用程序剩下了多少可用的內存空間?

  • 有多少圖片會被一次呈現到屏幕上?有多少圖片需要事先緩存好以便快速滑動時能夠立即顯示到屏幕?
  • 設備的屏幕大小與密度是多少? 一個xhdpi的設備會比hdpi需要一個更大的Cache來hold住同樣數量的圖片。
  • 不同的頁面針對Bitmap的設計的尺寸與配置是什么,大概會花費多少內存?
  • 頁面圖片被訪問的頻率?是否存在其中的一部分比其他的圖片具有更高的訪問頻繁?如果是,也許你想要保存那些最常訪問的到內存中,或者為不同組別的位圖(按訪問頻率分組)設置多個LruCache容器。
  1. onLowMemory() 與onTrimMemory()
    Android可以在不同的應用當中隨意切換。為了讓background轉到foreground, 每一個background都會占用一定的內存。系統會根據內存的使用情況決定回收部分background的應用內存。background的應用從暫停狀態恢復到foreground,比較快,如果從kill狀態恢復比較慢。
  2. 資源文件需要選擇合適的文件夾進行存放
    我們知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夾下的圖片在不同的設備上會經過scale的處理。例如我們只在hdpi的目錄下放置了一張100100的圖片,那么根據換算關系,xxhdpi
    的手機去引用那張圖片就會被拉伸到200
    200。需要注意到在這種情況下,內存占用是會顯著提高的。對于不希望被拉伸的圖片,需要放到assets或者nodpi的目錄下。
  3. Try catch某些大內存分配的操作
    在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對于這些可能發生OOM的代碼,加入catch機制,可以考慮在catch里面嘗試一次降級的內存分配操作。例如decode bitmap的時候,catch到OOM,可以嘗試把采樣比例再增加一倍之后,再次嘗試decode。
  4. 謹慎使用static對象
    因為static的生命周期過長,和應用的進程保持一致,使用不當很可能導致對象泄漏,在Android中應該謹慎使用static對象。
  5. 特別留意單例對象中不合理的持有
    雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命周期和應用保持一致,使用不合理很容易出現持有對象的泄漏。
  6. 珍惜Services資源
    如果你的應用需要在后臺使用service,除非它被觸發并執行一個任務,否則其他時候Service都應該是停止狀態。另外需要注意當這個service完成任務之后因為停止service失敗而引起的內存泄漏。 當你啟動一個Service,系統會傾向為了保留這個Service而一直保留Service所在的進程。這使得進程的運行代價很高,因為系統沒有辦法把Service所占用的RAM空間騰出來讓給其他組件,另外Service還不能被Paged out。這減少了系統能夠存放到LRU緩存當中的進程數量,它會影響應用之間的切換效率,甚至會導致系統內存使用不穩定,從而無法繼續保持住所有目前正在運行的service。 建議使用IntentService,它會在處理完交代給它的任務之后盡快結束自己。更多信息,請閱讀Running in a Background Service
  7. 優化布局層次,減少內存消耗
    越扁平化的視圖布局,占用的內存就越少,效率越高。我們需要盡量保證布局足夠扁平化,當使用系統提供的View無法實現足夠扁平的時候考慮使用自定義View來達到目的。
  8. 謹慎使用“抽象”編程
    很多時候,開發者會使用抽象類作為”好的編程實踐”,因為抽象能夠提升代碼的靈活性與可維護性。然而,抽象會導致一個顯著的額外內存開銷:他們需要同等量的代碼用于可執行,那些代碼會被mapping到內存中,因此如果你的抽象沒有顯著的提升效率,應該盡量避免他們。
  9. 使用nano protobufs序列化數據
    Protocol buffers是由Google為序列化結構數據而設計的,一種語言無關,平臺無關,具有良好的擴展性。類似XML,卻比XML更加輕量,快速,簡單。如果你需要為你的數據實現序列化與協議化,建議使用nano protobufs。關于更多細節,請參考protobuf readme的”Nano version”章節。
  10. 謹慎使用依賴注入框架
    使用類似Guice或者RoboGuice等框架注入代碼,在某種程度上可以簡化你的代碼。下面是使用RoboGuice前后的對比圖:

13.謹慎使用多進程
使用多進程可以把應用中的部分組件運行在單獨的進程當中,這樣可以擴大應用的內存占用范圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多進程,一方面是因為使用多進程會使得代碼邏輯更加復雜,另外如果使用不當,它可能反而會導致顯著增加內存。當你的應用需要運行一個常駐后臺的任務,而且這個任務并不輕量,可以考慮使用這個技術。

一個典型的例子是創建一個可以長時間后臺播放的Music Player。如果整個應用都運行在一個進程中,當后臺播放的時候,前臺的那些UI資源也沒有辦法得到釋放。類似這樣的應用可以切分成2個進程:一個用來操作UI,另外一個給后臺的Service。

  1. 使用ProGuard來剔除不需要的代碼
    ProGuard能夠通過移除不需要的代碼,重命名類,域與方法等等對代碼進行壓縮,優化與混淆。使用ProGuard可以使得你的代碼更加緊湊,這樣能夠減少mapping代碼所需要的內存空間。
  2. 謹慎使用第三方libraries
    很多開源的library代碼都不是為移動網絡環境而編寫的,如果運用在移動設備上,并不一定適合。即使是針對Android而設計的library,也需要特別謹慎,特別是在你不知道引入的library具體做了什么事情的時候。例如,其中一個library使用的是nano protobufs, 而另外一個使用的是micro protobufs。這樣一來,在你的應用里面就有2種protobuf的實現方式。這樣類似的沖突還可能發生在輸出日志,加載圖片,緩存等等模塊里面。另外不要為了1個或者2個功能而導入整個library,如果沒有一個合適的庫與你的需求相吻合,你應該考慮自己去實現,而不是導入一個大而全的解決方案。
  3. 考慮不同的實現方式來優化內存占用

寫在最后:

  • 設計風格很大程度上會影響到程序的內存與性能,相對來說,如果大量使用類似Material Design的風格,不僅安裝包可以變小,還可以減少內存的占用,渲染性能與加載性能都會有一定的提升。
  • 內存優化并不就是說程序占用的內存越少就越好,如果因為想要保持更低的內存占用,而頻繁觸發執行gc操作,在某種程度上反而會導致應用性能整體有所下降,這里需要綜合考慮做一定的權衡。
  • Android的內存優化涉及的知識面還有很多:內存管理的細節,垃圾回收的工作原理,如何查找內存泄漏等等都可以展開講很多。OOM是內存優化當中比較突出的一點,盡量減少OOM的概率對內存優化有著很大的意義。

詳細看郭霖的分析內存的使用總結
胡凱大大內存優化之OOM

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容