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有幾種情況:
- 加載對象過大
- 相應資源過多,來不及加載。
解決這些問題,有:
- 內存引用上做一些處理,常用的有軟引用。
- 內存中加載圖片直接在內存中做處理(如邊界壓縮)
這個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的第一步就是要盡量減少新分配出來的對象占用內存的大小,盡量使用更加輕量的對象。
-
使用更加輕量的數據結構
考慮使用ArrayMap/SpareseArray而不是傳統的HashMap等數據結構,Android系統為移動系統設計的容器ArrayMap更加高效,占用內存更少,因為HashMap需要一個額外的實例對象來記錄Mapping的操作。而SparesArray高效的避免了key和value的自動裝箱,而且避免了裝箱后的解箱。
關于更多ArrayMap/SparseArray的討論,請參考http://hukai.me/android-performance-patterns-season-3/的前三個段落 避免在Android中使用Enum
減少Bitmap對象的內存占用
Bitmap是一個消耗內存的大胖子,減少創建出來的Bitmap的內存占用很重要。一般有兩種措施
- inSampleSize:縮放比例,在把圖片載入內存之前,我們需要計算一個合適的縮放比例,避免不必要的大圖載入。
- decode format:解碼格式,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異。
- 使用更小的圖片
在設計圖片資源的時候,我們要考慮圖片是否存在可以壓縮的空間,是否能使用更小的圖片,使用小圖在xml加載資源時就不會在初始化視圖因為內存不足而發生InflationException,其根本原因就是發生了OOM。
內存對象的重復利用
Android最常用的緩存算法LRU(Least Recently Use)
- 復用系統自帶的資源,比如字符串、圖片、動畫、樣式、顏色、簡單布局,在應用中直接引用,減少自身負重、apk大小、減少內存的開銷、復用性更好。但需要考慮版本差異。
- Listview和GirdView出現大量重復子組件的視圖里面對ConvertView的復用。
- Bitmap對象的復用
- 在ListView和GridView等顯示大量圖片的控件里面需要使用LRU機制緩存Bitmap.
- 利用inBitmap的高級特性提高Android系統在Bitmap分配和釋放執行效率,inBitmap屬性可以告知Bitmap解碼器使用已經存在的內存區域而不是重新申請一塊內存區域存放Bitmap,也就是新解碼的Bitmap會使用之前那張bitmap在heap占用的內存區域,即使是上千張圖片,也只占用屏幕能放下圖片的內存
inBitmap的限制
- SDK19以后:新申請的BItmap大小必須小于或等于前面賦值過的bitmap的大小
-
新的Bitmap和原來的解碼格式要相同,我們可以創建包含多種類型可以重用的bitmap對象池,這樣后序的bitmap創建就可以找到合適的模板去重用。
- 避免在onDraw方法里面執行對象的創建
在onDraw這種頻繁調用的方法要避免對象的創建操作,因為他會迅速增加內存的使用,引起頻繁的gc,甚至內存抖動
5.StringBuilder
如果代碼中有大量字符串拼接操作,使用StringBuilder代替"+"
避免對象的內存泄露
內存對象的泄露會導致不再使用的對象無法及時釋放,不僅浪費了寶貴的內存空間,后續要分配內存的時候,空間不足造成OOM。這樣,每級的generation會變小,gc更加容易觸發,引起內存抖動,帶來性能問題。
- LeakCanary開源控件可以幫助我們發現內存泄露的問題。
介紹:https://github.com/square/leakcanary - 中文文檔 http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/)
- 注意Activity的泄露
Activity泄露是內存泄露最為嚴重的問題,涉及內存多,影響面廣
兩種情形:
- 內部類引用導致Activity的泄露
典型的是Handler導致的Activity泄露,如果Handler中有延遲的任務或者等待執行的任務隊列過長,很可能因為Handler繼續執行造成Activity的泄露。
引用鏈是Looper->MessageQueue->Message->handler->Activity,解決辦法是在退出UI之前執行 remove Handler消息隊列中的消息與runnable對象。或者使用Static+WeakReference的方式來判斷Handler和Activity之間存在引用關系。 - Activity Context被傳遞到其他實例中,可能導致自身被引用而發生泄露
- 考慮使用Application Context而不是Activity Context
除必須使用Activity Context的情況(Dialog的context必須是Activity),我們可以使用Application Context來避免Activity泄露 - 注意臨時Bitmap的及時回收
大多數情況下,我們對Bitmap對象增加緩存機制,但是有時候部分bitmap需要及時回收。比如我們臨時創建的摸個相對大的bitmap對象,變換得到新的bitmap對象后,盡快回收原始的bitmap,及時釋放原來的空間。 - 注意監聽器的注銷
android程序里面register后要及時釋放unregister那些監聽器,自己手動add的listener,要記得remove這個listener.
5.注意緩存容器的對象泄露
有時候我們為了提高對象的復用性,把某些對象放到緩存容器中,如果這些對象沒有及時從容器中清楚,也可能導致內存泄露, - 注意webview的泄露
Android不同版本對webview產生有很大差異,較為嚴重的問題是webview的泄露,解決辦法:為webview新開一個線程,通過AIDL與主進程通信,根據業務的需要在合適的時機進行銷毀,從而達到內存的釋放。 - 注意cursor對象是否關閉
我們在對數據庫進行操作時,使用完cursor沒有及時關閉,cursor的泄露,會對內存管理帶來負面影響
內存使用策略優化
1.謹慎使用large heap
android設備由于軟硬件的差異,heap閥值不同,特殊情況下可以在manifest中使用largeheap=true聲明一個更大的heap空間,使用getLargeMemoryClass()來獲取到這個更大的空間。但是要謹慎使用,因為額外的空間會影響到系統整體的用戶體驗,并且會使每次gc的運行時間更長。切換任務時性能大打折扣,large heap并不一定能獲取到更大的heap.
- 綜合考慮設備內存閾值與其他因素設計合適的緩存大小
例如,在設計ListView或者GridView的Bitmap LRU緩存的時候,需要考慮的點有:
應用程序剩下了多少可用的內存空間?
- 有多少圖片會被一次呈現到屏幕上?有多少圖片需要事先緩存好以便快速滑動時能夠立即顯示到屏幕?
- 設備的屏幕大小與密度是多少? 一個xhdpi的設備會比hdpi需要一個更大的Cache來hold住同樣數量的圖片。
- 不同的頁面針對Bitmap的設計的尺寸與配置是什么,大概會花費多少內存?
- 頁面圖片被訪問的頻率?是否存在其中的一部分比其他的圖片具有更高的訪問頻繁?如果是,也許你想要保存那些最常訪問的到內存中,或者為不同組別的位圖(按訪問頻率分組)設置多個LruCache容器。
- onLowMemory() 與onTrimMemory()
Android可以在不同的應用當中隨意切換。為了讓background轉到foreground, 每一個background都會占用一定的內存。系統會根據內存的使用情況決定回收部分background的應用內存。background的應用從暫停狀態恢復到foreground,比較快,如果從kill狀態恢復比較慢。 - 資源文件需要選擇合適的文件夾進行存放
我們知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夾下的圖片在不同的設備上會經過scale的處理。例如我們只在hdpi的目錄下放置了一張100100的圖片,那么根據換算關系,xxhdpi
的手機去引用那張圖片就會被拉伸到200200。需要注意到在這種情況下,內存占用是會顯著提高的。對于不希望被拉伸的圖片,需要放到assets或者nodpi的目錄下。 - Try catch某些大內存分配的操作
在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對于這些可能發生OOM的代碼,加入catch機制,可以考慮在catch里面嘗試一次降級的內存分配操作。例如decode bitmap的時候,catch到OOM,可以嘗試把采樣比例再增加一倍之后,再次嘗試decode。 - 謹慎使用static對象
因為static的生命周期過長,和應用的進程保持一致,使用不當很可能導致對象泄漏,在Android中應該謹慎使用static對象。 - 特別留意單例對象中不合理的持有
雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命周期和應用保持一致,使用不合理很容易出現持有對象的泄漏。 - 珍惜Services資源
如果你的應用需要在后臺使用service,除非它被觸發并執行一個任務,否則其他時候Service都應該是停止狀態。另外需要注意當這個service完成任務之后因為停止service失敗而引起的內存泄漏。 當你啟動一個Service,系統會傾向為了保留這個Service而一直保留Service所在的進程。這使得進程的運行代價很高,因為系統沒有辦法把Service所占用的RAM空間騰出來讓給其他組件,另外Service還不能被Paged out。這減少了系統能夠存放到LRU緩存當中的進程數量,它會影響應用之間的切換效率,甚至會導致系統內存使用不穩定,從而無法繼續保持住所有目前正在運行的service。 建議使用IntentService,它會在處理完交代給它的任務之后盡快結束自己。更多信息,請閱讀Running in a Background Service。 - 優化布局層次,減少內存消耗
越扁平化的視圖布局,占用的內存就越少,效率越高。我們需要盡量保證布局足夠扁平化,當使用系統提供的View無法實現足夠扁平的時候考慮使用自定義View來達到目的。 - 謹慎使用“抽象”編程
很多時候,開發者會使用抽象類作為”好的編程實踐”,因為抽象能夠提升代碼的靈活性與可維護性。然而,抽象會導致一個顯著的額外內存開銷:他們需要同等量的代碼用于可執行,那些代碼會被mapping到內存中,因此如果你的抽象沒有顯著的提升效率,應該盡量避免他們。 - 使用nano protobufs序列化數據
Protocol buffers是由Google為序列化結構數據而設計的,一種語言無關,平臺無關,具有良好的擴展性。類似XML,卻比XML更加輕量,快速,簡單。如果你需要為你的數據實現序列化與協議化,建議使用nano protobufs。關于更多細節,請參考protobuf readme的”Nano version”章節。 - 謹慎使用依賴注入框架
使用類似Guice或者RoboGuice等框架注入代碼,在某種程度上可以簡化你的代碼。下面是使用RoboGuice前后的對比圖:
13.謹慎使用多進程
使用多進程可以把應用中的部分組件運行在單獨的進程當中,這樣可以擴大應用的內存占用范圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多進程,一方面是因為使用多進程會使得代碼邏輯更加復雜,另外如果使用不當,它可能反而會導致顯著增加內存。當你的應用需要運行一個常駐后臺的任務,而且這個任務并不輕量,可以考慮使用這個技術。
一個典型的例子是創建一個可以長時間后臺播放的Music Player。如果整個應用都運行在一個進程中,當后臺播放的時候,前臺的那些UI資源也沒有辦法得到釋放。類似這樣的應用可以切分成2個進程:一個用來操作UI,另外一個給后臺的Service。
- 使用ProGuard來剔除不需要的代碼
ProGuard能夠通過移除不需要的代碼,重命名類,域與方法等等對代碼進行壓縮,優化與混淆。使用ProGuard可以使得你的代碼更加緊湊,這樣能夠減少mapping代碼所需要的內存空間。 - 謹慎使用第三方libraries
很多開源的library代碼都不是為移動網絡環境而編寫的,如果運用在移動設備上,并不一定適合。即使是針對Android而設計的library,也需要特別謹慎,特別是在你不知道引入的library具體做了什么事情的時候。例如,其中一個library使用的是nano protobufs, 而另外一個使用的是micro protobufs。這樣一來,在你的應用里面就有2種protobuf的實現方式。這樣類似的沖突還可能發生在輸出日志,加載圖片,緩存等等模塊里面。另外不要為了1個或者2個功能而導入整個library,如果沒有一個合適的庫與你的需求相吻合,你應該考慮自己去實現,而不是導入一個大而全的解決方案。 - 考慮不同的實現方式來優化內存占用
寫在最后:
- 設計風格很大程度上會影響到程序的內存與性能,相對來說,如果大量使用類似Material Design的風格,不僅安裝包可以變小,還可以減少內存的占用,渲染性能與加載性能都會有一定的提升。
- 內存優化并不就是說程序占用的內存越少就越好,如果因為想要保持更低的內存占用,而頻繁觸發執行gc操作,在某種程度上反而會導致應用性能整體有所下降,這里需要綜合考慮做一定的權衡。
- Android的內存優化涉及的知識面還有很多:內存管理的細節,垃圾回收的工作原理,如何查找內存泄漏等等都可以展開講很多。OOM是內存優化當中比較突出的一點,盡量減少OOM的概率對內存優化有著很大的意義。