Android避免OOM(記憶體優化)
Android記憶體優化是效能優化很重要的一部分,而如何避免OOM又是記憶體優化的核心。
Android記憶體管理機制
android官網有一篇文章
Android是如何管理應用的程序與記憶體分配 Android系統的Dalvik虛擬機器扮演了記憶體垃圾自動回收的角色。
OOM介紹(out of memory 記憶體溢位)
Android和java中都會出現由於不良程式碼引起的記憶體洩露,為了使Android應用程式能夠快速高效的執行,Android每個應用程式都會有專門Dalvik虛擬機器例項來執行,也就是每個程式都在屬於自己的程序中執行。 這樣,某個應用程式記憶體洩露僅僅只會使自己程序被kill掉不會影響其他程序(如果是system_process等系統程序出現問題,就會造成系統重啟),另一方面
出現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更加容易觸發,引起記憶體抖動,帶來效能問題。
- 注意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的概率對記憶體優化有著很大的意義。
轉載自https://www.jianshu.com/p/f5d8d3066b36