Android內(nèi)存優(yōu)化是性能優(yōu)化很重要的一部分展父,而如何避免OOM又是內(nèi)存優(yōu)化的核心下愈。
Android內(nèi)存管理機制
android官網(wǎng)有一篇文章
Android是如何管理應(yīng)用的進程與內(nèi)存分配
Android系統(tǒng)的Dalvik虛擬機扮演了內(nèi)存垃圾自動回收的角色皮壁。
OOM介紹(out of memory 內(nèi)存溢出)
Android和java中都會出現(xiàn)由于不良代碼引起的內(nèi)存泄露钉答,為了使Android應(yīng)用程序能夠快速高效的運行青抛,Android每個應(yīng)用程序都會有專門Dalvik虛擬機實例來運行,也就是每個程序都在屬于自己的進程中運行遵馆。
這樣,某個應(yīng)用程序內(nèi)存泄露僅僅只會使自己進程被kill掉不會影響其他進程(如果是system_process等系統(tǒng)進程出現(xiàn)問題丰榴,就會造成系統(tǒng)重啟)货邓,另一方面,系統(tǒng)為每一個應(yīng)用程序分配了不同的內(nèi)存上限四濒,如果超過這個上限被視為內(nèi)存泄露换况,從而被kill掉。
Dalvik Heap size因不同設(shè)備的RAM不同而有所差異盗蟆,應(yīng)用占用內(nèi)存接近這個閥值戈二,在嘗試分配內(nèi)存就會引起outofmemoryError的錯誤。
出現(xiàn)OOM有幾種情況:
- 加載對象過大
- 相應(yīng)資源過多喳资,來不及加載觉吭。
解決這些問題,有:
- 內(nèi)存引用上做一些處理骨饿,常用的有軟引用亏栈。
- 內(nèi)存中加載圖片直接在內(nèi)存中做處理(如邊界壓縮)
這個Glide\Fresco 圖片框架可能封裝好了
3.動態(tài)回收內(nèi)存
4.優(yōu)化Delivk虛擬機的堆內(nèi)存分配
5.自定義堆內(nèi)存大小
共享內(nèi)存
Android應(yīng)用程序的進程都是從Zygote的進程fork出來的。Zygote進程在系統(tǒng)啟動并載入通用的framework代碼和資源后啟動宏赘。一個新的應(yīng)用程序啟動绒北,系統(tǒng)就會從Zygote中fork出來一個新的進程,在新的進程中加載并允許應(yīng)用程序的代碼察署。這使得大多數(shù)RAM pages被分配給framework的代碼闷游,并且RAM資源能夠在應(yīng)用的所有進程之間共享。
大多數(shù)static 數(shù)據(jù)被mmapped到一個進程中贴汪,這樣使得同樣的數(shù)據(jù)在進程之間能夠共享脐往,而且在需要的時候能paged out.常見static 數(shù)據(jù)包括Dalvik code ,app resourecs,so 文件等。
大多數(shù)情況下扳埂,Android通過顯示的方式分配共享內(nèi)存區(qū)域(例如ashmem或gralloc)來實現(xiàn)動態(tài)RAM區(qū)域能夠在不同進程之間進行共享的機制业簿。比如,Window Surface在APP和Screen Composition之間使用共享的內(nèi)存阳懂,
Cursor Buffers在Content Provider與Clients之間共享內(nèi)存梅尤。
分配與回收內(nèi)存
- 每個進程的Dalvik heap都反應(yīng)了使用內(nèi)存的占用范圍,(Dalvik Heap Size),他可以根據(jù)需要進行增長,但是系統(tǒng)有一個上限岩调。
- HeapSize跟實際的物理內(nèi)存大小是不對等的巷燥,PSS(proportional Set Size)記錄了應(yīng)用程序自身占用以及和其他進程共享的內(nèi)容。
- Android不會對heap空閑區(qū)域進行做碎片整理号枕。系統(tǒng)僅僅在新的內(nèi)存分配之前判斷Heap的尾端剩余空間是否足夠缰揪,不夠就會觸發(fā)gc操作,從而騰出更多空閑的內(nèi)存空間葱淳。gc操作(garbage collection)也就是所謂的垃圾回收钝腺,Android在適當(dāng)時候觸發(fā)gc操作抛姑,將一些不再使用的對象回收,在Android高級系統(tǒng)針對Heap空間有一個Generational Heap Memory的模型拍屑,最近分配的對象在放在young generation區(qū)域途戒,當(dāng)停留一段時間,這個對象會被移動到old generation中僵驰,最后在移動到permanent generation區(qū)域中。系統(tǒng)會根據(jù)內(nèi)存中不同的內(nèi)存數(shù)據(jù)類型進行g(shù)c操作唁毒,young generation區(qū)域的對象更容易被銷毀蒜茴,而且gc操作的速度比old generation的速度要快,時間更短浆西。
每個generation的內(nèi)存區(qū)域都有固定的大小粉私,隨著新的對象陸續(xù)被分配到此區(qū)域,當(dāng)這些對象的大小快達到閥門值時近零,就會觸發(fā)gc操作诺核。通常情況下,gc操作發(fā)生時久信,所有線程都是暫停的窖杀。
如何查看本機heap size:
ActivityManager manager=(Activity)getSystemService(Context.ACTIVITY_SERVICE); int heapsize=manager.getMemoryClass();
應(yīng)用切換操作
Android系統(tǒng)不會再用戶切換應(yīng)用的時候進行交換內(nèi)存的操作,而是把不包含F(xiàn)oreground組件的應(yīng)用進程放到LRUCache中裙士,比如用戶啟動一個應(yīng)用入客,系統(tǒng)會為它創(chuàng)建一個進程,但是當(dāng)用戶離開這個應(yīng)用腿椎,此進程不會背立即銷毀而是會放到一個Cache中桌硫,當(dāng)用戶切換回來夠快速的恢復(fù)。
發(fā)生OOM的條件
通過不同的內(nèi)存分配方式對不同的對象(bitmap,etc)進行操作因Android版本差異發(fā)生變化啃炸。
4.0以上铆隘,廢除了external的計數(shù)器,類似bitmap的分配改到dalvik的Java heap(堆)中申請南用,只要allocated+新分配的內(nèi)存>=getMemoryClass()就會發(fā)生OOM膀钠。(在AS memory monitor查看內(nèi)存中Dalvik Heap的實時變化)
如何避免OOM
減少OOM的第一步就是要盡量減少新分配出來的對象占用內(nèi)存的大小,盡量使用更加輕量的對象训枢。
-
使用更加輕量的數(shù)據(jù)結(jié)構(gòu)
考慮使用ArrayMap/SpareseArray而不是傳統(tǒng)的HashMap等數(shù)據(jù)結(jié)構(gòu)托修,Android系統(tǒng)為移動系統(tǒng)設(shè)計的容器ArrayMap更加高效,占用內(nèi)存更少恒界,因為HashMap需要一個額外的實例對象來記錄Mapping的操作睦刃。而SparesArray高效的避免了key和value的自動裝箱,而且避免了裝箱后的解箱十酣。
關(guān)于更多ArrayMap/SparseArray的討論涩拙,請參考http://hukai.me/android-performance-patterns-season-3/的前三個段落 避免在Android中使用Enum
減少Bitmap對象的內(nèi)存占用
Bitmap是一個消耗內(nèi)存的大胖子际长,減少創(chuàng)建出來的Bitmap的內(nèi)存占用很重要。一般有兩種措施
- inSampleSize:縮放比例兴泥,在把圖片載入內(nèi)存之前工育,我們需要計算一個合適的縮放比例,避免不必要的大圖載入搓彻。
- decode format:解碼格式如绸,選擇ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差異旭贬。
- 使用更小的圖片
在設(shè)計圖片資源的時候怔接,我們要考慮圖片是否存在可以壓縮的空間,是否能使用更小的圖片稀轨,使用小圖在xml加載資源時就不會在初始化視圖因為內(nèi)存不足而發(fā)生InflationException,其根本原因就是發(fā)生了OOM扼脐。
內(nèi)存對象的重復(fù)利用
Android最常用的緩存算法LRU(Least Recently Use)
- 復(fù)用系統(tǒng)自帶的資源,比如字符串奋刽、圖片瓦侮、動畫、樣式佣谐、顏色肚吏、簡單布局,在應(yīng)用中直接引用台谍,減少自身負重须喂、apk大小、減少內(nèi)存的開銷趁蕊、復(fù)用性更好坞生。但需要考慮版本差異。
- Listview和GirdView出現(xiàn)大量重復(fù)子組件的視圖里面對ConvertView的復(fù)用掷伙。
- Bitmap對象的復(fù)用
- 在ListView和GridView等顯示大量圖片的控件里面需要使用LRU機制緩存Bitmap.
- 利用inBitmap的高級特性提高Android系統(tǒng)在Bitmap分配和釋放執(zhí)行效率是己,inBitmap屬性可以告知Bitmap解碼器使用已經(jīng)存在的內(nèi)存區(qū)域而不是重新申請一塊內(nèi)存區(qū)域存放Bitmap,也就是新解碼的Bitmap會使用之前那張bitmap在heap占用的內(nèi)存區(qū)域,即使是上千張圖片任柜,也只占用屏幕能放下圖片的內(nèi)存
inBitmap的限制
- SDK19以后:新申請的BItmap大小必須小于或等于前面賦值過的bitmap的大小
-
新的Bitmap和原來的解碼格式要相同卒废,我們可以創(chuàng)建包含多種類型可以重用的bitmap對象池,這樣后序的bitmap創(chuàng)建就可以找到合適的模板去重用宙地。
- 避免在onDraw方法里面執(zhí)行對象的創(chuàng)建
在onDraw這種頻繁調(diào)用的方法要避免對象的創(chuàng)建操作摔认,因為他會迅速增加內(nèi)存的使用,引起頻繁的gc宅粥,甚至內(nèi)存抖動
5.StringBuilder
如果代碼中有大量字符串拼接操作参袱,使用StringBuilder代替"+"
避免對象的內(nèi)存泄露
內(nèi)存對象的泄露會導(dǎo)致不再使用的對象無法及時釋放,不僅浪費了寶貴的內(nèi)存空間,后續(xù)要分配內(nèi)存的時候抹蚀,空間不足造成OOM剿牺。這樣,每級的generation會變小环壤,gc更加容易觸發(fā)晒来,引起內(nèi)存抖動,帶來性能問題郑现。
- LeakCanary開源控件可以幫助我們發(fā)現(xiàn)內(nèi)存泄露的問題湃崩。
介紹:https://github.com/square/leakcanary - 中文文檔 http://www.liaohuqiu.net/cn/posts/leak-canary-read-me/)
- 注意Activity的泄露
Activity泄露是內(nèi)存泄露最為嚴重的問題,涉及內(nèi)存多懂酱,影響面廣
兩種情形:
- 內(nèi)部類引用導(dǎo)致Activity的泄露
典型的是Handler導(dǎo)致的Activity泄露竹习,如果Handler中有延遲的任務(wù)或者等待執(zhí)行的任務(wù)隊列過長,很可能因為Handler繼續(xù)執(zhí)行造成Activity的泄露列牺。
引用鏈是Looper->MessageQueue->Message->handler->Activity,解決辦法是在退出UI之前執(zhí)行 remove Handler消息隊列中的消息與runnable對象∞智裕或者使用Static+WeakReference的方式來判斷Handler和Activity之間存在引用關(guān)系瞎领。 - Activity Context被傳遞到其他實例中,可能導(dǎo)致自身被引用而發(fā)生泄露
- 考慮使用Application Context而不是Activity Context
除必須使用Activity Context的情況(Dialog的context必須是Activity),我們可以使用Application Context來避免Activity泄露 - 注意臨時Bitmap的及時回收
大多數(shù)情況下随夸,我們對Bitmap對象增加緩存機制九默,但是有時候部分bitmap需要及時回收。比如我們臨時創(chuàng)建的摸個相對大的bitmap對象宾毒,變換得到新的bitmap對象后驼修,盡快回收原始的bitmap,及時釋放原來的空間。 - 注意監(jiān)聽器的注銷
android程序里面register后要及時釋放unregister那些監(jiān)聽器诈铛,自己手動add的listener乙各,要記得remove這個listener.
5.注意緩存容器的對象泄露
有時候我們?yōu)榱颂岣邔ο蟮膹?fù)用性,把某些對象放到緩存容器中幢竹,如果這些對象沒有及時從容器中清楚耳峦,也可能導(dǎo)致內(nèi)存泄露, - 注意webview的泄露
Android不同版本對webview產(chǎn)生有很大差異焕毫,較為嚴重的問題是webview的泄露蹲坷,解決辦法:為webview新開一個線程,通過AIDL與主進程通信邑飒,根據(jù)業(yè)務(wù)的需要在合適的時機進行銷毀循签,從而達到內(nèi)存的釋放。 - 注意cursor對象是否關(guān)閉
我們在對數(shù)據(jù)庫進行操作時疙咸,使用完cursor沒有及時關(guān)閉县匠,cursor的泄露,會對內(nèi)存管理帶來負面影響
內(nèi)存使用策略優(yōu)化
1.謹慎使用large heap
android設(shè)備由于軟硬件的差異,heap閥值不同聚唐,特殊情況下可以在manifest中使用largeheap=true聲明一個更大的heap空間丐重,使用getLargeMemoryClass()來獲取到這個更大的空間。但是要謹慎使用杆查,因為額外的空間會影響到系統(tǒng)整體的用戶體驗扮惦,并且會使每次gc的運行時間更長。切換任務(wù)時性能大打折扣亲桦,large heap并不一定能獲取到更大的heap.
- 綜合考慮設(shè)備內(nèi)存閾值與其他因素設(shè)計合適的緩存大小
例如崖蜜,在設(shè)計ListView或者GridView的Bitmap LRU緩存的時候,需要考慮的點有:
應(yīng)用程序剩下了多少可用的內(nèi)存空間?
- 有多少圖片會被一次呈現(xiàn)到屏幕上客峭?有多少圖片需要事先緩存好以便快速滑動時能夠立即顯示到屏幕豫领?
- 設(shè)備的屏幕大小與密度是多少? 一個xhdpi的設(shè)備會比hdpi需要一個更大的Cache來hold住同樣數(shù)量的圖片。
- 不同的頁面針對Bitmap的設(shè)計的尺寸與配置是什么舔琅,大概會花費多少內(nèi)存等恐?
- 頁面圖片被訪問的頻率?是否存在其中的一部分比其他的圖片具有更高的訪問頻繁备蚓?如果是课蔬,也許你想要保存那些最常訪問的到內(nèi)存中,或者為不同組別的位圖(按訪問頻率分組)設(shè)置多個LruCache容器郊尝。
- onLowMemory() 與onTrimMemory()
Android可以在不同的應(yīng)用當(dāng)中隨意切換二跋。為了讓background轉(zhuǎn)到foreground, 每一個background都會占用一定的內(nèi)存。系統(tǒng)會根據(jù)內(nèi)存的使用情況決定回收部分background的應(yīng)用內(nèi)存流昏。background的應(yīng)用從暫停狀態(tài)恢復(fù)到foreground扎即,比較快,如果從kill狀態(tài)恢復(fù)比較慢况凉。 - 資源文件需要選擇合適的文件夾進行存放
我們知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夾下的圖片在不同的設(shè)備上會經(jīng)過scale的處理谚鄙。例如我們只在hdpi的目錄下放置了一張100100的圖片,那么根據(jù)換算關(guān)系茎刚,xxhdpi
的手機去引用那張圖片就會被拉伸到200200襟锐。需要注意到在這種情況下,內(nèi)存占用是會顯著提高的膛锭。對于不希望被拉伸的圖片粮坞,需要放到assets或者nodpi的目錄下。 - Try catch某些大內(nèi)存分配的操作
在某些情況下初狰,我們需要事先評估那些可能發(fā)生OOM的代碼莫杈,對于這些可能發(fā)生OOM的代碼,加入catch機制奢入,可以考慮在catch里面嘗試一次降級的內(nèi)存分配操作筝闹。例如decode bitmap的時候,catch到OOM,可以嘗試把采樣比例再增加一倍之后关顷,再次嘗試decode糊秆。 - 謹慎使用static對象
因為static的生命周期過長,和應(yīng)用的進程保持一致议双,使用不當(dāng)很可能導(dǎo)致對象泄漏痘番,在Android中應(yīng)該謹慎使用static對象。 - 特別留意單例對象中不合理的持有
雖然單例模式簡單實用平痰,提供了很多便利性汞舱,但是因為單例的生命周期和應(yīng)用保持一致,使用不合理很容易出現(xiàn)持有對象的泄漏宗雇。 - 珍惜Services資源
如果你的應(yīng)用需要在后臺使用service昂芜,除非它被觸發(fā)并執(zhí)行一個任務(wù),否則其他時候Service都應(yīng)該是停止?fàn)顟B(tài)赔蒲。另外需要注意當(dāng)這個service完成任務(wù)之后因為停止service失敗而引起的內(nèi)存泄漏泌神。 當(dāng)你啟動一個Service,系統(tǒng)會傾向為了保留這個Service而一直保留Service所在的進程舞虱。這使得進程的運行代價很高腻扇,因為系統(tǒng)沒有辦法把Service所占用的RAM空間騰出來讓給其他組件,另外Service還不能被Paged out砾嫉。這減少了系統(tǒng)能夠存放到LRU緩存當(dāng)中的進程數(shù)量,它會影響應(yīng)用之間的切換效率窒篱,甚至?xí)?dǎo)致系統(tǒng)內(nèi)存使用不穩(wěn)定焕刮,從而無法繼續(xù)保持住所有目前正在運行的service。 建議使用IntentService墙杯,它會在處理完交代給它的任務(wù)之后盡快結(jié)束自己配并。更多信息,請閱讀Running in a Background Service高镐。 - 優(yōu)化布局層次溉旋,減少內(nèi)存消耗
越扁平化的視圖布局,占用的內(nèi)存就越少,效率越高。我們需要盡量保證布局足夠扁平化终佛,當(dāng)使用系統(tǒng)提供的View無法實現(xiàn)足夠扁平的時候考慮使用自定義View來達到目的企量。 - 謹慎使用“抽象”編程
很多時候,開發(fā)者會使用抽象類作為”好的編程實踐”渠驼,因為抽象能夠提升代碼的靈活性與可維護性。然而,抽象會導(dǎo)致一個顯著的額外內(nèi)存開銷:他們需要同等量的代碼用于可執(zhí)行儡陨,那些代碼會被mapping到內(nèi)存中,因此如果你的抽象沒有顯著的提升效率,應(yīng)該盡量避免他們骗村。 - 使用nano protobufs序列化數(shù)據(jù)
Protocol buffers是由Google為序列化結(jié)構(gòu)數(shù)據(jù)而設(shè)計的嫌褪,一種語言無關(guān),平臺無關(guān)胚股,具有良好的擴展性笼痛。類似XML,卻比XML更加輕量信轿,快速晃痴,簡單。如果你需要為你的數(shù)據(jù)實現(xiàn)序列化與協(xié)議化财忽,建議使用nano protobufs倘核。關(guān)于更多細節(jié),請參考protobuf readme的”Nano version”章節(jié)即彪。 - 謹慎使用依賴注入框架
使用類似Guice或者RoboGuice等框架注入代碼紧唱,在某種程度上可以簡化你的代碼。下面是使用RoboGuice前后的對比圖:
13.謹慎使用多進程
使用多進程可以把應(yīng)用中的部分組件運行在單獨的進程當(dāng)中隶校,這樣可以擴大應(yīng)用的內(nèi)存占用范圍漏益,但是這個技術(shù)必須謹慎使用,絕大多數(shù)應(yīng)用都不應(yīng)該貿(mào)然使用多進程深胳,一方面是因為使用多進程會使得代碼邏輯更加復(fù)雜绰疤,另外如果使用不當(dāng),它可能反而會導(dǎo)致顯著增加內(nèi)存舞终。當(dāng)你的應(yīng)用需要運行一個常駐后臺的任務(wù)轻庆,而且這個任務(wù)并不輕量,可以考慮使用這個技術(shù)敛劝。
一個典型的例子是創(chuàng)建一個可以長時間后臺播放的Music Player余爆。如果整個應(yīng)用都運行在一個進程中,當(dāng)后臺播放的時候夸盟,前臺的那些UI資源也沒有辦法得到釋放蛾方。類似這樣的應(yīng)用可以切分成2個進程:一個用來操作UI,另外一個給后臺的Service上陕。
- 使用ProGuard來剔除不需要的代碼
ProGuard能夠通過移除不需要的代碼桩砰,重命名類,域與方法等等對代碼進行壓縮唆垃,優(yōu)化與混淆五芝。使用ProGuard可以使得你的代碼更加緊湊,這樣能夠減少mapping代碼所需要的內(nèi)存空間辕万。 - 謹慎使用第三方libraries
很多開源的library代碼都不是為移動網(wǎng)絡(luò)環(huán)境而編寫的枢步,如果運用在移動設(shè)備上沉删,并不一定適合。即使是針對Android而設(shè)計的library醉途,也需要特別謹慎矾瑰,特別是在你不知道引入的library具體做了什么事情的時候。例如隘擎,其中一個library使用的是nano protobufs, 而另外一個使用的是micro protobufs殴穴。這樣一來,在你的應(yīng)用里面就有2種protobuf的實現(xiàn)方式货葬。這樣類似的沖突還可能發(fā)生在輸出日志采幌,加載圖片,緩存等等模塊里面震桶。另外不要為了1個或者2個功能而導(dǎo)入整個library休傍,如果沒有一個合適的庫與你的需求相吻合,你應(yīng)該考慮自己去實現(xiàn)蹲姐,而不是導(dǎo)入一個大而全的解決方案磨取。 - 考慮不同的實現(xiàn)方式來優(yōu)化內(nèi)存占用
寫在最后:
- 設(shè)計風(fēng)格很大程度上會影響到程序的內(nèi)存與性能,相對來說柴墩,如果大量使用類似Material Design的風(fēng)格忙厌,不僅安裝包可以變小,還可以減少內(nèi)存的占用江咳,渲染性能與加載性能都會有一定的提升逢净。
- 內(nèi)存優(yōu)化并不就是說程序占用的內(nèi)存越少就越好,如果因為想要保持更低的內(nèi)存占用歼指,而頻繁觸發(fā)執(zhí)行g(shù)c操作汹胃,在某種程度上反而會導(dǎo)致應(yīng)用性能整體有所下降,這里需要綜合考慮做一定的權(quán)衡东臀。
- Android的內(nèi)存優(yōu)化涉及的知識面還有很多:內(nèi)存管理的細節(jié),垃圾回收的工作原理犀农,如何查找內(nèi)存泄漏等等都可以展開講很多惰赋。OOM是內(nèi)存優(yōu)化當(dāng)中比較突出的一點,盡量減少OOM的概率對內(nèi)存優(yōu)化有著很大的意義呵哨。
詳細看郭霖的分析內(nèi)存的使用總結(jié)
胡凱大大內(nèi)存優(yōu)化之OOM