聲明:大部分內(nèi)容為從其他文章中摘錄感興趣的部分,只為記錄給自己看。
Stack由操作系統(tǒng)控制,其中主要存儲(chǔ)函數(shù)地址、函數(shù)參數(shù)、局部變量等等,所以Stack空間不需要很大,一般為幾MB大小。
Heap空間由程序控制,程序員可以使用malloc、new、free、delete等函數(shù)調(diào)用來(lái)操作這片地址空間。Heap為程序完成各種復(fù)雜任務(wù)提供內(nèi)存空間,所以空間比較大,一般為幾百M(fèi)B到幾GB。
進(jìn)程的內(nèi)存空間只是虛擬內(nèi)存(或者叫做邏輯內(nèi)存),而程序的運(yùn)行需要的是實(shí)實(shí)在在的內(nèi)存,即物理內(nèi)存(RAM)。在必要時(shí),操作系統(tǒng)會(huì)將程序運(yùn)行中申請(qǐng)的內(nèi)存(虛擬內(nèi)存)映射到RAM,讓進(jìn)程能夠使用物理內(nèi)存。
Android中的進(jìn)程
(1)native進(jìn)程:采用C/C++實(shí)現(xiàn),不包含dalvik實(shí)例的進(jìn)程,/system/bin/目錄下的程序文件運(yùn)行后都是以native進(jìn)程形式存在的。
(2)java進(jìn)程:Android中運(yùn)行于dalvik虛擬機(jī)之上的進(jìn)程。dalvik虛擬機(jī)的宿主進(jìn)程由fork()系統(tǒng)調(diào)用創(chuàng)建,所以每一個(gè)Java進(jìn)程都是存在于一個(gè)native進(jìn)程中。因此,java進(jìn)程的內(nèi)存分配比native進(jìn)程復(fù)雜,因?yàn)檫M(jìn)程中存在一個(gè)虛擬機(jī)實(shí)例。
Android的java程序?yàn)槭裁慈菀壮霈F(xiàn)OOM?這個(gè)是因?yàn)锳ndroid系統(tǒng)對(duì)dalvik的vm heapsize做了硬性限制,當(dāng)java進(jìn)程申請(qǐng)的java空間超過(guò)閾值時(shí),就會(huì)拋出OOM異常(這個(gè)閾值可以是48M、24M、16M等,視機(jī)型而定),可以通過(guò)adb shell getprop | grep dalvik.vm.heapsize查看此值。
這樣的設(shè)計(jì)似乎有些不合理,但Google的目的是為了讓Android系統(tǒng)能同時(shí)讓比較多的進(jìn)程常駐內(nèi)存,這樣程序啟動(dòng)時(shí)就不用每次都重新加載到內(nèi)存,能夠給用戶(hù)更快的響應(yīng)。
如果RAM真的不足,這時(shí)Android的memory killer會(huì)起作用,當(dāng)RAM所剩不多時(shí),memory killer會(huì)殺死一些優(yōu)先級(jí)比較低的進(jìn)程來(lái)釋放物理內(nèi)存,讓高優(yōu)先級(jí)程序得到更多的內(nèi)存。
可以使用adb shell cat /proc/meminfo查看RAM使用情況。
使用adb shell dumpsys meminfo + packagename/pid查看進(jìn)程的內(nèi)存信息。
對(duì)于一些大型的應(yīng)用程序(如游戲),內(nèi)存使用會(huì)比較多,很容易超出vm heapsize的限制,這時(shí)怎么保證程序不會(huì)因?yàn)镺OM而崩潰呢?
- 創(chuàng)建子進(jìn)程
創(chuàng)建一個(gè)新的進(jìn)程,那么我們就可以把一些對(duì)象分配到新進(jìn)程的heap上了。當(dāng)然,創(chuàng)建子進(jìn)程會(huì)增加系統(tǒng)開(kāi)銷(xiāo),而且并不是所有應(yīng)用都適合這樣做。創(chuàng)建子進(jìn)程的方法:使用android:process標(biāo)簽。 - 使用jni在native heap上申請(qǐng)空間(推薦)
nativeheap的增長(zhǎng)并不受dalvik vm heapsize的限制。 - 使用顯存
使用OpenGL textures等API,texture memory不受dalvik vm heapsize限制,比如Android中的GraphicBufferAllocator申請(qǐng)的內(nèi)存就是顯存。
Android中常見(jiàn)的內(nèi)存泄漏
1、單例(主要原因是因?yàn)橐话闱闆r下單例都是全局的,有時(shí)候會(huì)引用一些實(shí)際生命周期比較短的變量,導(dǎo)致無(wú)法釋放)
2、靜態(tài)變量(同樣也是因?yàn)樯芷诒容^長(zhǎng))
3、Handler內(nèi)存泄漏
4、匿名內(nèi)部類(lèi)(匿名內(nèi)部類(lèi)會(huì)引用外部類(lèi),導(dǎo)致無(wú)法釋放,比如各種回調(diào))
5、資源使用完未關(guān)閉(BroadcastReceiver、ContentObserver、File、Cursor、Stream、Bitmap)
對(duì)于Android內(nèi)存泄漏,LeakCanary是最知名的優(yōu)秀組件。其原理是監(jiān)控每個(gè)activity,在activity onDestroy后,在后臺(tái)線程檢測(cè)引用,然后過(guò)一段時(shí)間進(jìn)行g(shù)c,gc后如果引用還在,那么dump出內(nèi)存堆棧,并解析進(jìn)行可視化顯示。
但有時(shí)候APP本身就是有一些比較耗內(nèi)存的功能,比如直播、視頻播放、音樂(lè)播放,我們還能做什么可以降低內(nèi)存使用,減少OOM呢?
分辨率適配問(wèn)題
很多情況下圖片所占的內(nèi)存在整個(gè)App內(nèi)存占用中會(huì)占大部分。我們知道可以通過(guò)將圖片放到hdpi/xhdpi/xxhdpi等不同文件夾進(jìn)行適配,通過(guò)xml android:background設(shè)置背景圖片,或者通過(guò)BitmapFactory.decodeResource()方法,圖片實(shí)際上默認(rèn)情況下是會(huì)進(jìn)行縮放的。在java層實(shí)際調(diào)用的函數(shù)都是通過(guò)BitmapFactory里的decodeResourceStream函數(shù)。
decodeResource在解析時(shí)會(huì)對(duì)Bitmap根據(jù)當(dāng)前設(shè)備屏幕像素密度densityDPI的值進(jìn)行縮放適配操作,使得解析出來(lái)的Bitmap與當(dāng)前設(shè)備的分辨率匹配,達(dá)到一個(gè)最佳的顯示效果,并且Bitmap的大小將比原始的大。
盡管現(xiàn)在已經(jīng)有比較先進(jìn)的圖片加載組件類(lèi)似Glide,F(xiàn)resco等,但是有時(shí)就是需要手動(dòng)拿到一個(gè)bitmap或者drawable,特別是在一些可能會(huì)頻繁調(diào)用的場(chǎng)景(如ListView的getView),怎樣盡可能對(duì)bitmap進(jìn)行復(fù)用呢?這里可以簡(jiǎn)單自己用WeakReference做一個(gè)bitmap緩存池,也可以用類(lèi)似圖片加載庫(kù)寫(xiě)一個(gè)統(tǒng)一的bitmap緩存池,可以參考GlideBitmapPool的實(shí)現(xiàn)。
圖片壓縮
BitmapFactory在解碼圖片時(shí),可以帶一個(gè)Options,有一些比較有用的功能,比如:
- inTargetDensity 表示要被畫(huà)出來(lái)時(shí)的目標(biāo)像素密度
- inSampleSize 當(dāng)它小于1時(shí),會(huì)被當(dāng)做1處理,大于1會(huì)按比例(1 / inSampleSize)縮小bitmap的寬和高、降低分辨率,大于1時(shí)這個(gè)值將會(huì)被處置為2的倍數(shù)。
- inJustDecodeBounds 字面意思就可以理解為只解析圖片的邊界,有時(shí)如果只是為了獲取圖片的大小就可以用這個(gè),而不必直接加載整張圖片。
- inPurgeable和inInputShareable 這兩個(gè)需要一起使用,BitmapFactory的源碼里有注釋?zhuān)笾乱馑际潜硎驹谙到y(tǒng)內(nèi)存不足時(shí)是否可以回收這個(gè)bitmap,有點(diǎn)類(lèi)似軟引用,但實(shí)際在5.0以后這兩個(gè)屬性已經(jīng)被忽略,因?yàn)橄到y(tǒng)認(rèn)為回收后再解碼實(shí)際會(huì)反而可能導(dǎo)致性能問(wèn)題。
- inBitmap 官方推薦使用的參數(shù),表示重復(fù)利用圖片內(nèi)存,減少內(nèi)存分配,在4.4以前只有相同大小的圖片內(nèi)存區(qū)域可以復(fù)用,4.4以后只要原有的圖片比將要解碼的圖片大即可復(fù)用了。
緩存池大小
現(xiàn)在很多圖片加載組件都不僅僅使用軟引用或者弱引用了,實(shí)際上類(lèi)似Glide默認(rèn)使用的是LruCache,因?yàn)檐浺?、弱引用都比較難以控制,使用LruCache可以實(shí)現(xiàn)比較精細(xì)的控制,而默認(rèn)緩存池設(shè)置太大了會(huì)導(dǎo)致浪費(fèi)內(nèi)存,設(shè)置小了又會(huì)導(dǎo)致圖片經(jīng)常被回收,所以需要根據(jù)每個(gè)App的情況,以及設(shè)備的分辨率,內(nèi)存計(jì)算出一個(gè)比較合理的初始值,可以參考Glide的做法。
內(nèi)存抖動(dòng)
Android里內(nèi)存抖動(dòng)是指內(nèi)存頻繁地分配和回收,而頻繁的gc會(huì)導(dǎo)致卡頓,嚴(yán)重時(shí)還會(huì)導(dǎo)致OOM。一個(gè)經(jīng)典的案例是String拼接創(chuàng)建大量小的對(duì)象。
而內(nèi)存抖動(dòng)為什么會(huì)引起OOM呢?主要原因還是因?yàn)榇罅啃〉膶?duì)象頻繁創(chuàng)建,導(dǎo)致內(nèi)存碎片,從而當(dāng)需要分配內(nèi)存時(shí),雖然總體上還是有剩余內(nèi)存可分配,而由于這些內(nèi)存不連續(xù),導(dǎo)致無(wú)法分配,系統(tǒng)就直接OOM了。
其他
常用數(shù)據(jù)結(jié)構(gòu)優(yōu)化,ArrayMap及SparseArray是Android的系統(tǒng)API,是專(zhuān)門(mén)為移動(dòng)設(shè)備而定制的。用于在一定情況下取代HashMap而達(dá)到節(jié)省內(nèi)存的目的,具體性能見(jiàn)HashMap、ArrayMap、SparseArray源碼分析及性能對(duì)比。對(duì)于key為int的HashMap盡量使用SparseArray替代,大概可以省30%的內(nèi)存,而對(duì)于其他類(lèi)型,ArrayMap對(duì)內(nèi)存的節(jié)省實(shí)際并不明顯,10%左右,但是數(shù)據(jù)量在1000以上時(shí),查找速度可能會(huì)變慢。
枚舉,Android平臺(tái)枚舉是比較爭(zhēng)議的,在較早的Android版本,使用枚舉會(huì)導(dǎo)致包過(guò)大。隨著虛擬機(jī)的優(yōu)化,目前枚舉變量在Android平臺(tái)性能問(wèn)題已經(jīng)不大,而目前Android官方建議,使用枚舉變量還是需要謹(jǐn)慎,因?yàn)槊杜e變量可能比直接用int多使用2倍的內(nèi)存。
ListView復(fù)用,getView里盡量復(fù)用convertView,同時(shí)因?yàn)間etView會(huì)頻繁調(diào)用,要避免頻繁地生成對(duì)象。
謹(jǐn)慎使用多進(jìn)程,現(xiàn)在很多APP都不是單進(jìn)程,為了?;睿蛘咛岣叻€(wěn)定性都會(huì)進(jìn)行一些進(jìn)程拆分,而實(shí)際上即使是空進(jìn)程也會(huì)占用內(nèi)存(1M左右),對(duì)于使用完的進(jìn)程,服務(wù)都要及時(shí)進(jìn)行回收。
盡量使用系統(tǒng)資源,系統(tǒng)組件,圖片甚至控件的id。
減少view的層級(jí),對(duì)于可以延遲初始化的頁(yè)面,使用viewstub。
數(shù)據(jù)相關(guān):序列化數(shù)據(jù)使用protobuf可以比xml省30%內(nèi)存,慎用sharedpreference,因?yàn)閷?duì)于同一個(gè)sp,會(huì)將整個(gè)xml文件載入內(nèi)存,有時(shí)候?yàn)榱俗x一個(gè)配置,就會(huì)將幾百k的數(shù)據(jù)讀進(jìn)內(nèi)存。數(shù)據(jù)庫(kù)字段盡量精簡(jiǎn),只提取所需字段。
dex優(yōu)化,代碼優(yōu)化,謹(jǐn)慎使用外部庫(kù)。有人覺(jué)得代碼多少于內(nèi)存沒(méi)有關(guān)系,實(shí)際會(huì)有那么點(diǎn)關(guān)系,現(xiàn)在稍微大一點(diǎn)的項(xiàng)目動(dòng)輒就是百萬(wàn)行代碼以上,多dex也是常態(tài),不僅占用rom空間,實(shí)際上運(yùn)行的時(shí)候需要家長(zhǎng)dex也是會(huì)占用內(nèi)存的(幾M),有時(shí)候?yàn)榱耸褂靡恍?kù)里的某個(gè)功能函數(shù)就引入了整個(gè)龐大的庫(kù),此時(shí)可以考慮抽取必要的部分,開(kāi)啟proguard優(yōu)化代碼,使用Facebook redex優(yōu)化dex(好像有不少坑)。
當(dāng)我們接到一個(gè)內(nèi)存優(yōu)化任務(wù)時(shí),應(yīng)該從何開(kāi)始?
1、首先是解決大部分內(nèi)存泄漏,接入LeakCanary
2、通過(guò)MAT查看內(nèi)存占用,優(yōu)化占用內(nèi)存較大的地方
3、對(duì)RDM上的OOM進(jìn)行分析
4、同時(shí)對(duì)一些邏輯代碼進(jìn)行調(diào)整,如APP主頁(yè)的tab進(jìn)行數(shù)據(jù)延遲加載和定時(shí)回收
總結(jié),我們可以通過(guò)各種內(nèi)存泄漏檢測(cè)組件,MAT查看內(nèi)存占用,Memory Monitor跟蹤整個(gè)APP的內(nèi)存變化情況,Heap Viewer查看當(dāng)前內(nèi)存快照,Allocation Tracker追蹤內(nèi)存對(duì)象的來(lái)源,以及利用崩潰上報(bào)平臺(tái)從多方面對(duì)App內(nèi)存進(jìn)行監(jiān)控和優(yōu)化。