Android內(nèi)存管理(一)

聲明:大部分內(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)化。

參考

【騰訊Bugly干貨分享】Android內(nèi)存優(yōu)化總結(jié)&實(shí)踐

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,068評(píng)論 25 709
  • HereAndroid的內(nèi)存優(yōu)化是性能優(yōu)化中很重要的一部分,而避免OOM又是內(nèi)存優(yōu)化中比較核心的一點(diǎn)。這是一篇關(guān)于...
    HarryXR閱讀 3,934評(píng)論 1 24
  • 做事情 有計(jì)劃 昨天晚上收到小R媽媽發(fā)給我的微信,說(shuō)到:“王老師好,我是小R的媽媽?zhuān)娌缓靡馑即驍_你,上一天班很累...
    王方媛閱讀 831評(píng)論 0 7
  • 1.Android的控件架構(gòu): 每個(gè)Activity包含一個(gè)Window對(duì)象(Window是abstract的,一...
    jacky123閱讀 330評(píng)論 0 1
  • 如果有人想開(kāi)發(fā)個(gè)產(chǎn)品,第一步首先要作什么,隨便找個(gè)創(chuàng)業(yè)者,估計(jì)都能答上來(lái),“最簡(jiǎn)可行產(chǎn)品”嘛。它指的是“一個(gè)這樣的...
    Acetx閱讀 632評(píng)論 0 51

友情鏈接更多精彩內(nèi)容