Android的記憶體優化是性能優化中很重要的一部分,而避免OOM又是記憶體優化中比較核心的一點。這是一篇關于記憶體優化中如何避免OOM的總結性概要文章,内容大多都是和OOM有關的實踐總結概要。了解錯誤或是偏差的地方,還請多包涵指正,謝謝!
(一)Android的記憶體管理機制
Google在Android的官網上有這樣一篇文章,初步介紹了Android是如何管理應用的程序與記憶體配置設定: http://developer.android.com/training/articles/memory.html。 Android系統的Dalvik虛拟機扮演了正常的記憶體垃圾自動回收的角色,Android系統沒有為記憶體提供交換區,它使用paging與memory-mapping(mmapping)的機制來管理記憶體,下面簡要概述一些Android系統中重要的記憶體管理基礎概念。
1)共享記憶體
Android系統通過下面幾種方式來實作共享記憶體:
- Android應用的程序都是從一個叫做Zygote的程序fork出來的。Zygote程序在系統啟動,并載入通用的framework的代碼與資源之後開始啟動。為了啟動一個新的程式程序,系統會fork Zygote程序生成一個新的程序,然後在新的程序中加載并運作應用程式的代碼。這就使得大多數的RAM pages被用來配置設定給framework的代碼,同時促使RAM資源能夠在應用的所有程序之間進行共享。
- 大多數static的資料被mmapped到一個程序中。這不僅僅讓同樣的資料能夠在程序間進行共享,而且使得它能夠在需要的時候被paged out。常見的static資料包括Dalvik Code、app resources、so檔案等。
- 大多數情況下,Android通過顯式的配置設定共享記憶體區域(例如ashmem或gralloc)來實作動态RAM區域能夠在不同程序之間進行共享的機制。比如,Window Surface在App與Screen Compositor之間使用共享的記憶體,Cursor Buffers在Content Provider與Clients之間共享記憶體。
2)配置設定與回收記憶體
- 每一個程序的Dalvik Heap都反映了使用記憶體的占用範圍。這就是通常邏輯意義上提到的Dalvik Heap Size,它可以随着需要進行增長,但是增長行為會有一個系統為它設定上限。
- 邏輯上講的Heap Size和實際實體意義上使用的記憶體大小是不對等的,Proportional Set Size(PSS)記錄了應用程式自身占用以及與其他程序進行共享的記憶體。
- Android系統并不會對Heap中空閑記憶體區域做碎片整理。系統僅僅會在新的記憶體配置設定之前判斷Heap的尾端剩餘空間是否足夠,如果空間不夠會觸發GC操作,進而騰出更多空閑的記憶體空間。在Android的進階系統版本裡面針對Heap空間有一個Generational Heap Memory的模型,最近配置設定的對象會存放在Young Generation區域。當這個對象在該區域停留的時間達到一定程度,它會被移動到Old Generation,最後累積一定時間再移動到Permanent Generation區域。系統會根據記憶體中不同的記憶體資料類型分别執行不同的GC操作。例如,剛配置設定到Young Generation區域的對象通常更容易被銷毀回收,同時在Young Generation區域的GC操作速度會比Old Generation區域的GC操作速度更快(如圖1所示)。
圖1 根據不同記憶體資料類型執行不同GC操作
每一個Generation的記憶體區域都有固定的大小。随着新的對象陸續被配置設定到此區域,當對象總的大小臨近這一級别記憶體區域的閥值時,會觸發GC操作,以便騰出空間來存放其他新的對象(如圖2所示)。
圖2 對象值臨近閥值觸發GC操作
通常情況下,GC發生的時候,所有的線程都是會被暫停的。執行GC所占用的時間和它發生在哪一個Generation也有關系,Young Generation中的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和目前Generation中的對象數量有關,周遊樹結構查找20000個對象比起周遊50個對象自然是要慢很多的。
3)限制應用的記憶體
- 為了整個系統的記憶體控制需要,Android系統為每一個應用程式都設定一個硬性的Dalvik Heap Size最大限制門檻值,這個門檻值在不同的裝置上會因為RAM大小不同而各有差異。如果你的應用占用記憶體空間已經接近這個門檻值,此時再嘗試配置設定記憶體的話,很容易引發OutOfMemoryError錯誤。
- ActivityManager.getMemoryClass可以用來查詢目前應用的Heap Size門檻值,這個方法會傳回一個整數,表明應用的Heap Size門檻值是多少MB(Megabates)。
4)應用切換操作
- Android系統并不會在使用者切換應用的時候執行交換記憶體操作。Android會把那些不包含Foreground元件的應用程序放到LRU Cache中。例如,當使用者開始啟動一個應用時,系統會為它建立一個程序。但是當使用者離開此應用,程序不會立即被銷毀,而是被放到系統的Cache當中。如果使用者後來再切換回到這個應用,此程序就能夠被馬上完整地恢複,進而實作應用的快速切換。
- 如果你的應用中有一個被緩存的程序,這個程序會占用一定的記憶體空間,它會對系統的整體性能有影響。是以,當系統開始進入Low Memory的狀态時,它會由系統根據LRU的規則與應用的優先級,記憶體占用情況以及其他因素的影響綜合評估之後決定是否被殺掉。
- 對于那些非foreground的程序,Android系統是如何判斷Kill掉哪些程序的問題,請參考Processes and Threads。
(二)OOM(Out Of Memory)
前面我們提到過使用getMemoryClass的方法可以得到Dalvik Heap的門檻值。簡要地擷取某個應用的記憶體占用情況可以參考下面的示例(更多記憶體檢視的知識,可以參考Google官方教程: Investigating Your RAM Usage)
1)檢視記憶體使用情況
通過指令行檢視記憶體詳細占用情況,如圖3所示。
圖3 指令行檢視記憶體詳細占用情況
通過Android Studio的Memory Monitor檢視記憶體中Dalvik Heap的實時變化,如圖4、5、6所示。
圖4 Memory Monitor檢視記憶體中Dalvik Heap的實時變化(一)
圖5 Memory Monitor檢視記憶體中Dalvik Heap的實時變化(二)
圖6 Memory Monitor檢視記憶體中Dalvik Heap的實時變化(三)
2)發生OOM的條件
關于Native Heap、Dalvik Heap、PSS等記憶體管理機制比較複雜,這裡就不展開較長的描述。簡單的說,通過不同的記憶體配置設定方式(malloc/mmap/JNIEnv/etc)對不同的對象(Bitmap/etc)進行操作,會因為Android系統版本的差異而産生不同的行為,對Native Heap與Dalvik Heap以及OOM的判斷條件都會有所影響。在2.x的系統上,我們常常可以看到Heap Size的total值,明顯超過了通過getMemoryClass擷取到的門檻值而不會發生OOM的情況。那麼,針對2.x與4.x的Android系統,到底如何判斷會發生OOM呢?
- Android 2.x系統GC LOG中的dalvik allocated + external allocated + 新配置設定的大小 >= getMemoryClass值的時候就會發生OOM。 例如,假設有這麼一段Dalvik輸出的GC LOG:GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms,那麼32586+8989+(新配置設定23975)=65550>64M時,就會發生OOM。
- Android 4.x的系統廢除了external的計數器,類似Bitmap的配置設定改到Dalvik的Java Heap中申請。隻要allocated + 新配置設定的記憶體 >= getMemoryClass的時候就會發生OOM,如圖7所示(注:雖然圖示示範的是ART運作環境,但是統計規則還是和Dalvik保持一緻)。
圖7
(三)如何避免OOM總結
前面介紹了一些基礎的記憶體管理機制以及OOM的基礎知識,那麼在實踐操作當中,有哪些指導性的規則可以參考呢?歸納下來,可以從四個方面着手,首先是減小對象的記憶體占用,其次是記憶體對象的重複利用,然後是避免對象的記憶體洩露,最後是記憶體使用政策優化。
減小對象的記憶體占用
避免OOM的第一步就是要盡量減少新配置設定出來的對象占用記憶體的大小,盡量使用更加輕量的對象。
1)使用更加輕量的資料結構
例如,我們可以考慮使用ArrayMap/SparseArray而不是HashMap等傳統資料結構。圖8示範了HashMap的簡要工作原理,相比起Android專門為移動作業系統編寫的ArrayMap容器,在大多數情況下,都顯示效率低下,更占記憶體。通常的HashMap的實作方式更加消耗記憶體,因為它需要一個額外的執行個體對象來記錄Mapping操作。另外,SparseArray更加高效,在于他們避免了對key與value的自動裝箱(autoboxing),并且避免了裝箱後的解箱。
圖8 HashMap簡要工作原理
關于更多ArrayMap/SparseArray的讨論,請參考《 Android性能優化典範(三)》的前三個段落。
2)避免在Android裡面使用Enum
Android官方教育訓練課程提到過“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具體原理請參考《Android性能優化典範(三)》,是以請避免在Android裡面使用到枚舉。
3)減小Bitmap對象的記憶體占用
Bitmap是一個極容易消耗記憶體的大胖子,減小建立出來的Bitmap的記憶體占用可謂是重中之重,通常來說有以下2個措施:
- inSampleSize:縮放比例,在把圖檔載入記憶體之前,我們需要先計算出一個合适的縮放比例,避免不必要的大圖載入。
- decode format:解碼格式,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異。
4)使用更小的圖檔
在涉及給到資源圖檔時,我們需要特别留意這張圖檔是否存在可以壓縮的空間,是否可以使用更小的圖檔。盡量使用更小的圖檔不僅可以減少記憶體的使用,還能避免出現大量的InflationException。假設有一張很大的圖檔被XML檔案直接引用,很有可能在初始化視圖時會因為記憶體不足而發生InflationException,這個問題的根本原因其實是發生了OOM。
記憶體對象的重複利用
大多數對象的複用,最終實施的方案都是利用對象池技術,要麼是在編寫代碼時顯式地在程式裡建立對象池,然後處理好複用的實作邏輯。要麼就是利用系統架構既有的某些複用特性,減少對象的重複建立,進而降低記憶體的配置設定與回收(如圖9所示)。
圖9 對象池技術
在Android上面最常用的一個緩存算法是LRU(Least Recently Use),簡要操作原理如圖10所示。
圖10 LRU簡要操作原理
1)複用系統自帶的資源
Android系統本身内置了很多的資源,比如字元串、顔色、圖檔、動畫、樣式以及簡單布局等,這些資源都可以在應用程式中直接引用。這樣做不僅能減少應用程式的自身負重,減小APK的大小,還可以在一定程度上減少記憶體的開銷,複用性更好。但是也有必要留意Android系統的版本差異性,對那些不同系統版本上表現存在很大差異、不符合需求的情況,還是需要應用程式自身内置進去。
2)注意在ListView/GridView等出現大量重複子元件的視圖裡對ConvertView的複用,如圖11所示。
圖11
3)Bitmap對象的複用
在ListView與GridView等顯示大量圖檔的控件裡,需要使用LRU的機制來緩存處理好的Bitmap,如圖12所示。
圖12
- 利用inBitmap的進階特性提高Android系統在Bitmap配置設定與釋放執行效率(注:3.0以及4.4以後存在一些使用限制上的差異)。使用inBitmap屬性可以告知Bitmap解碼器去嘗試使用已經存在的記憶體區域,新解碼的Bitmap會嘗試去使用之前那張Bitmap在Heap中所占據的pixel data記憶體區域,而不是去問記憶體重新申請一塊區域來存放Bitmap。利用這種特性,即使是上千張的圖檔,也隻會僅僅隻需要占用螢幕所能夠顯示的圖檔數量的記憶體大小,如圖13所示。
圖13 利用inBitmap的進階特性提高Android在Bitmap配置設定與釋放執行效率
使用inBitmap需要注意幾個限制條件:
- 在SDK 11 -> 18之間,重用的Bitmap大小必須是一緻的。例如給inBitmap指派的圖檔大小為100-100,那麼新申請的Bitmap必須也為100-100才能夠被重用。從SDK 19開始,新申請的Bitmap大小必須小于或者等于已經指派過的Bitmap大小。
- 新申請的Bitmap與舊的Bitmap必須有相同的解碼格式。例如大家都是8888的,如果前面的Bitmap是8888,那麼就不能支援4444與565格式的Bitmap了。我們可以建立一個包含多種典型可重用Bitmap的對象池,這樣後續的Bitmap建立都能夠找到合适的“模闆”去進行重用,如圖14所示。
圖14
另外,在2.x的系統上,盡管Bitmap是配置設定在Native層,但還是無法避免被計算到OOM的引用計數器裡。這裡提示一下,不少應用會通過反射vBitmapFactory.Options裡面的inNativeAlloc來達到擴大使用記憶體的目的,但是如果大家都這麼做,對系統整體會造成一定的負面影響,建議謹慎采納。
4)避免在onDraw方法裡面執行對象的建立
類似onDraw等頻繁調用的方法,一定需要注意避免在這裡做建立對象的操作,因為他會迅速增加記憶體的使用,而且很容易引起頻繁的gc,甚至是記憶體抖動。
5)StringBuilder
在有些時候,代碼中會需要使用到大量的字元串拼接的操作,這種時候有必要考慮使用StringBuilder來替代頻繁的“+”。
避免對象的記憶體洩露
記憶體對象的洩漏,會導緻一些不再使用的對象無法及時釋放,這樣一方面占用了寶貴的記憶體空間,很容易導緻後續需要配置設定記憶體的時候,空閑空間不足而出現OOM。顯然,這還使得每級Generation的記憶體區域可用空間變小,GC就會更容易被觸發,容易出現記憶體抖動,進而引起性能問題(如圖15所示)。
圖15
最新的LeakCanary開源控件,可以很好的幫助我們發現記憶體洩露的情況,更多關于LeakCanary的介紹,請看 這裡(中文使用說明)。另外也可以使用傳統的MAT工具查找記憶體洩露,請參考這裡(便捷的中文資料)。
1)注意Activity的洩漏
通常來說,Activity的洩漏是記憶體洩漏裡面最嚴重的問題,它占用的記憶體多,影響面廣,我們需要特别注意以下兩種情況導緻的Activity洩漏:
最典型的場景是Handler導緻的Activity洩漏,如果Handler中有延遲的任務或者是等待執行的任務隊列過長,都有可能因為Handler繼續執行而導緻Activity發生洩漏。此時的引用關系鍊是Looper -> MessageQueue -> Message -> Handler -> Activity。為了解決這個問題,可以在UI退出之前,執行remove Handler消息隊列中的消息與runnable對象。或者是使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關系的目的。
- Activity Context被傳遞到其他執行個體中,這可能導緻自身被引用而發生洩漏。
内部類引起的洩漏不僅僅會發生在Activity上,其他任何内部類出現的地方,都需要特别留意!我們可以考慮盡量使用static類型的内部類,同時使用WeakReference的機制來避免因為互相引用而出現的洩露。
2)考慮使用Application Context而不是Activity Context
對于大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),我們都可以考慮使用Application Context而不是Activity的Context,這樣可以避免不經意的Activity洩露。
3)注意臨時Bitmap對象的及時回收
雖然在大多數情況下,我們會對Bitmap增加緩存機制,但是在某些時候,部分Bitmap是需要及時回收的。例如臨時建立的某個相對比較大的bitmap對象,在經過變換得到新的bitmap對象之後,應該盡快回收原始的bitmap,這樣能夠更快釋放原始bitmap所占用的空間。
需要特别留意的是Bitmap類裡面提供的createBitmap方法,如圖16所示:
圖16 createBitmap方法
這個函數傳回的bitmap有可能和source bitmap是同一個,在回收的時候,需要特别檢查source bitmap與return bitmap的引用是否相同,隻有在不等的情況下,才能夠執行source bitmap的recycle方法。
4)注意監聽器的登出
在Android程式裡面存在很多需要register與unregister的監聽器,我們需要確定在合适的時候及時unregister那些監聽器。自己手動add的listener,需要記得及時remove這個listener。
5)注意緩存容器中的對象洩漏
有時候,我們為了提高對象的複用性把某些對象放到緩存容器中,可是如果這些對象沒有及時從容器中清除,也是有可能導緻記憶體洩漏的。例如,針對2.3的系統,如果把drawable添加到緩存容器,因為drawable與View的強應用,很容易導緻activity發生洩漏。而從4.0開始,就不存在這個問題。解決這個問題,需要對2.3系統上的緩存drawable做特殊封裝,處理引用解綁的問題,避免洩漏的情況。
6)注意WebView的洩漏
Android中的WebView存在很大的相容性問題,不僅僅是Android系統版本的不同對WebView産生很大的差異,另外不同的廠商出貨的ROM裡面WebView也存在着很大的差異。更嚴重的是标準的WebView存在記憶體洩露的問題,請看 這裡。是以通常根治這個問題的辦法是為WebView開啟另外一個程序,通過AIDL與主程序進行通信,WebView所在的程序可以根據業務的需要選擇合适的時機進行銷毀,進而達到記憶體的完整釋放。
7)注意Cursor對象是否及時關閉
在程式中我們經常會進行查詢資料庫的操作,但時常會存在不小心使用Cursor之後沒有及時關閉的情況。這些Cursor的洩露,反複多次出現的話會對記憶體管理産生很大的負面影響,我們需要謹記對Cursor對象的及時關閉。
記憶體使用政策優化
1)謹慎使用large heap
正如前面提到的,Android裝置根據硬體與軟體的設定差異而存在不同大小的記憶體空間,他們為應用程式設定了不同大小的Heap限制門檻值。你可以通過調用getMemoryClass來擷取應用的可用Heap大小。在一些特殊的情景下,你可以通過在manifest的application标簽下添加largeHeap=true的屬性來為應用聲明一個更大的heap空間。然後,你可以通過getLargeMemoryClass來擷取到這個更大的heap size門檻值。然而,聲明得到更大Heap門檻值的本意是為了一小部分會消耗大量RAM的應用(例如一個大圖檔的編輯應用)。不要輕易的因為你需要使用更多的記憶體而去請求一個大的Heap Size。隻有當你清楚的知道哪裡會使用大量的記憶體并且知道為什麼這些記憶體必須被保留時才去使用large heap。是以請謹慎使用large heap屬性。使用額外的記憶體空間會影響系統整體的使用者體驗,并且會使得每次gc的運作時間更長。在任務切換時,系統的性能會大打折扣。另外, large heap并不一定能夠擷取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。是以即使你申請了large heap,你還是應該通過執行getMemoryClass來檢查實際擷取到的heap大小。
2)綜合考慮裝置記憶體門檻值與其他因素設計合适的緩存大小
例如,在設計ListView或者GridView的Bitmap LRU緩存的時候,需要考慮的點有:
- 應用程式剩下了多少可用的記憶體空間?
- 有多少圖檔會被一次呈現到螢幕上?有多少圖檔需要事先緩存好以便快速滑動時能夠立即顯示到螢幕?
- 裝置的螢幕大小與密度是多少? 一個xhdpi的裝置會比hdpi需要一個更大的Cache來hold住同樣數量的圖檔。
- 不同的頁面針對Bitmap的設計的尺寸與配置是什麼,大概會花費多少記憶體?
- 頁面圖檔被通路的頻率?是否存在其中的一部分比其他的圖檔具有更高的通路頻繁?如果是,也許你想要儲存那些最常通路的到記憶體中,或者為不同組别的位圖(按通路頻率分組)設定多個LruCache容器。
3)onLowMemory與onTrimMemory
Android使用者可以随意在不同的應用之間進行快速切換。為了讓background的應用能夠迅速的切換到forground,每一個background的應用都會占用一定的記憶體。Android系統會根據目前的系統的記憶體使用情況,決定回收部分background的應用記憶體。如果background的應用從暫停狀态直接被恢複到forground,能夠獲得較快的恢複體驗,如果background應用是從Kill的狀态進行恢複,相比之下就顯得稍微有點慢,如圖17所示。
圖17 從Kill狀态進行恢複體驗更慢
- onLowMemory:Android系統提供了一些回調來通知目前應用的記憶體使用情況,通常來說,當所有的background應用都被kill掉的時候,forground應用會收到onLowMemory的回調。在這種情況下,需要盡快釋放目前應用的非必須的記憶體資源,進而確定系統能夠繼續穩定運作。
- onTrimMemory(int):Android系統從4.0開始還提供了onTrimMemory的回調,當系統記憶體達到某些條件的時候,所有正在運作的應用都會收到這個回調,同時在這個回調裡面會傳遞以下的參數,代表不同的記憶體使用情況,收到onTrimMemory回調的時候,需要根據傳遞的參數類型進行判斷,合理的選擇釋放自身的一些記憶體占用,一方面可以提高系統的整體運作流暢度,另外也可以避免自己被系統判斷為優先需要殺掉的應用。
- TRIM_MEMORY_UI_HIDDEN:你的應用程式的所有UI界面被隐藏了,即使用者點選了Home鍵或者Back鍵退出應用,導緻應用的UI界面完全不可見。這個時候應該釋放一些不可見的時候非必須的資源
當程式正在前台運作的時候,可能會接收到從onTrimMemory中傳回的下面的值之一:
- TRIM_MEMORY_RUNNING_MODERATE:你的應用正在運作并且不會被列為可殺死的。但是裝置此時正運作于低記憶體狀态下,系統開始觸發殺死LRU Cache中的Process的機制。
- TRIM_MEMORY_RUNNING_LOW:你的應用正在運作且沒有被列為可殺死的。但是裝置正運作于更低記憶體的狀态下,你應該釋放不用的資源用來提升系統性能。
- TRIM_MEMORY_RUNNING_CRITICAL:你的應用仍在運作,但是系統已經把LRU Cache中的大多數程序都已經殺死,是以你應該立即釋放所有非必須的資源。如果系統不能回收到足夠的RAM數量,系統将會清除所有的LRU緩存中的程序,并且開始殺死那些之前被認為不應該殺死的程序,例如那個包含了一個運作态Service的程序。
當應用程序退到背景正在被Cached的時候,可能會接收到從onTrimMemory中傳回的下面的值之一:
- TRIM_MEMORY_BACKGROUND: 系統正運作于低記憶體狀态并且你的程序正處于LRU緩存名單中最不容易殺掉的位置。盡管你的應用程序并不是處于被殺掉的高危險狀态,系統可能已經開始殺掉LRU緩存中的其他程序了。你應該釋放那些容易恢複的資源,以便于你的程序可以保留下來,這樣當使用者回退到你的應用的時候才能夠迅速恢複。
- TRIM_MEMORY_MODERATE: 系統正運作于低記憶體狀态并且你的程序已經已經接近LRU名單的中部位置。如果系統開始變得更加記憶體緊張,你的程序是有可能被殺死的。
- TRIM_MEMORY_COMPLETE: 系統正運作于低記憶體的狀态并且你的程序正處于LRU名單中最容易被殺掉的位置。你應該釋放任何不影響你的應用恢複狀态的資源。
因為onTrimMemory的回調是在API 14才被加進來的,對于老的版本,你可以使用onLowMemory)回調來進行相容。onLowMemory相當與TRIM_MEMORY_COMPLETE。
請注意:當系統開始清除LRU緩存中的程序時,雖然它首先按照LRU的順序來執行操作,但是它同樣會考慮程序的記憶體使用量以及其他因素。占用越少的程序越容易被留下來。
4)資源檔案需要選擇合适的檔案夾進行存放
我們知道hdpi/xhdpi/xxhdpi等等不同dpi的檔案夾下的圖檔在不同的裝置上會經過scale的處理。例如我們隻在hdpi的目錄下放置了一張100100的圖檔,那麼根據換算關系,xxhdpi的手機去引用那張圖檔就會被拉伸到200200。需要注意到在這種情況下,記憶體占用是會顯著提高的。對于不希望被拉伸的圖檔,需要放到assets或者nodpi的目錄下。
5)Try catch某些大記憶體配置設定的操作
在某些情況下,我們需要事先評估那些可能發生OOM的代碼,對于這些可能發生OOM的代碼,加入catch機制,可以考慮在catch裡面嘗試一次降級的記憶體配置設定操作。例如decode bitmap的時候,catch到OOM,可以嘗試把采樣比例再增加一倍之後,再次嘗試decode。
6)謹慎使用static對象
因為static的生命周期過長,和應用的程序保持一緻,使用不當很可能導緻對象洩漏,在Android中應該謹慎使用static對象(如圖19所示)。
圖19
7)特别留意單例對象中不合理的持有
雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命周期和應用保持一緻,使用不合理很容易出現持有對象的洩漏。
8)珍惜Services資源
如果你的應用需要在背景使用service,除非它被觸發并執行一個任務,否則其他時候Service都應該是停止狀态。另外需要注意當這個service完成任務之後因為停止service失敗而引起的記憶體洩漏。 當你啟動一個Service,系統會傾向為了保留這個Service而一直保留Service所在的程序。這使得程序的運作代價很高,因為系統沒有辦法把Service所占用的RAM空間騰出來讓給其他元件,另外Service還不能被Paged out。這減少了系統能夠存放到LRU緩存當中的程序數量,它會影響應用之間的切換效率,甚至會導緻系統記憶體使用不穩定,進而無法繼續保持住所有目前正在運作的service。 建議使用 IntentService,它會在處理完交代給它的任務之後盡快結束自己。更多資訊,請閱讀Running in a Background Service。
9)優化布局層次,減少記憶體消耗
越扁平化的視圖布局,占用的記憶體就越少,效率越高。我們需要盡量保證布局足夠扁平化,當使用系統提供的View無法實作足夠扁平的時候考慮使用自定義View來達到目的。
10)謹慎使用“抽象”程式設計
很多時候,開發者會使用抽象類作為”好的程式設計實踐”,因為抽象能夠提升代碼的靈活性與可維護性。然而,抽象會導緻一個顯著的額外記憶體開銷:他們需要同等量的代碼用于可執行,那些代碼會被mapping到記憶體中,是以如果你的抽象沒有顯著的提升效率,應該盡量避免他們。
11)使用nano protobufs序列化資料
Protocol buffers是由Google為序列化結構資料而設計的,一種語言無關,平台無關,具有良好的擴充性。類似XML,卻比XML更加輕量,快速,簡單。如果你需要為你的資料實作序列化與協定化,建議使用nano protobufs。關于更多細節,請參考 protobuf readme的”Nano version”章節。
12)謹慎使用依賴注入架構
使用類似Guice或者RoboGuice等架構注入代碼,在某種程度上可以簡化你的代碼。圖20是使用RoboGuice前後的對比圖:
圖20 使用RoboGuice前後對比圖
使用RoboGuice之後,代碼是簡化了不少。然而,那些注入架構會通過掃描你的代碼執行許多初始化的操作,這會導緻你的代碼需要大量的記憶體空間來mapping代碼,而且mapped pages會長時間的被保留在記憶體中。除非真的很有必要,建議謹慎使用這種技術。
13)謹慎使用多程序
使用多程序可以把應用中的部分元件運作在單獨的程序當中,這樣可以擴大應用的記憶體占用範圍,但是這個技術必須謹慎使用,絕大多數應用都不應該貿然使用多程序,一方面是因為使用多程序會使得代碼邏輯更加複雜,另外如果使用不當,它可能反而會導緻顯著增加記憶體。當你的應用需要運作一個常駐背景的任務,而且這個任務并不輕量,可以考慮使用這個技術。
一個典型的例子是建立一個可以長時間背景播放的Music Player。如果整個應用都運作在一個程序中,當背景播放的時候,前台的那些UI資源也沒有辦法得到釋放。類似這樣的應用可以切分成2個程序:一個用來操作UI,另外一個給背景的Service。
14)使用ProGuard來剔除不需要的代碼
ProGuard能夠通過移除不需要的代碼,重命名類,域與方法等等對代碼進行壓縮,優化與混淆。使用ProGuard可以使得你的代碼更加緊湊,這樣能夠減少mapping代碼所需要的記憶體空間。
15)謹慎使用第三方libraries
很多開源的library代碼都不是為移動網絡環境而編寫的,如果運用在移動裝置上,并不一定适合。即使是針對Android而設計的library,也需要特别謹慎,特别是在你不知道引入的library具體做了什麼事情的時候。例如,其中一個library使用的是nano protobufs, 而另外一個使用的是micro protobufs。這樣一來,在你的應用裡面就有2種protobuf的實作方式。這樣類似的沖突還可能發生在輸出日志,加載圖檔,緩存等等子產品裡面。另外不要為了1個或者2個功能而導入整個library,如果沒有一個合适的庫與你的需求相吻合,你應該考慮自己去實作,而不是導入一個大而全的解決方案。
16)考慮不同的實作方式來優化記憶體占用
在某些情況下,設計的某個方案能夠快速實作需求,但是這個方案卻可能在記憶體占用上表現的效率不夠好。例如:
圖21
對于上面這樣一個時鐘表盤的實作,最簡單的就是使用很多張包含指針的表盤圖檔,使用幀動畫實作指針的旋轉。但是如果把指針扣出來,單獨進行旋轉繪制,顯然比載入N多張圖檔占用的記憶體要少很多。當然這樣做,代碼複雜度上會有所增加,這裡就需要在優化記憶體占用與實作簡易度之間進行權衡了。
總結
- 設計風格很大程度上會影響到程式的記憶體與性能,相對來說,如果大量使用類似Material Design的風格,不僅安裝包可以變小,還可以減少記憶體的占用,渲染性能與加載性能都會有一定的提升。
- 記憶體優化并不就是說程式占用的記憶體越少就越好,如果因為想要保持更低的記憶體占用,而頻繁觸發執行gc操作,在某種程度上反而會導緻應用性能整體有所下降,這裡需要綜合考慮做一定的權衡。
- Android的記憶體優化涉及的知識面還有很多:記憶體管理的細節,垃圾回收的工作原理,如何查找記憶體洩漏等等都可以展開講很多。OOM是記憶體優化當中比較突出的一點,盡量減少OOM的機率對記憶體優化有着很大的意義。