由于項(xiàng)目里之前線上版本出現(xiàn)過一定比例的OOM,雖然比例并不大,但是還是暴露了一定的問題,所以打算對我們App分為幾個(gè)步驟進(jìn)行內(nèi)存分析和優(yōu)化,當(dāng)然內(nèi)存的優(yōu)化是個(gè)長期的過程,不是一兩個(gè)版本的事,每個(gè)版本都需要收集線上內(nèi)存數(shù)據(jù)進(jìn)行監(jiān)控以及分析。
本文主要會根據(jù)實(shí)際項(xiàng)目中優(yōu)化步驟分為以下幾部分:
1.Android內(nèi)存分析基礎(chǔ)這部分主要先介紹一些進(jìn)行內(nèi)存分析的基礎(chǔ)方法以及工具,對這部分比較熟悉的同學(xué)可以先跳過哈。 一.App的內(nèi)存使用情況概覽每個(gè)App進(jìn)程可以分配到的最大內(nèi)存是有限的,當(dāng)然不同手機(jī)每個(gè)App進(jìn)程可以分配到的最大內(nèi)存有可能不一樣,可以通過以下命令進(jìn)行查看: adb shell getprop | grep dalvik.vm.heapsize 我們可以輸出我們App的內(nèi)存使用情況概覽: adb shell dumpsys meminfo 包名 我們就可以看到:
二、Android ProfilerAndroidStduio3.0后Android Profiler變得比之前更強(qiáng)大,內(nèi)存分析頁變得更加直觀更加方便,下面是截圖:
三、強(qiáng)大的MATMAT是做比較細(xì)致的內(nèi)存分析的利器了,功能十分強(qiáng)大,其中的:
可以非常方便的排序查看當(dāng)前內(nèi)存中最占內(nèi)存的class或者實(shí)體對象,而且有一條非常清晰的引用鏈來查看該對象的持有者,這對內(nèi)存的分析以及內(nèi)存泄漏的分析都是非常友好的。 同時(shí)MAT支持compare對比功能,將兩個(gè).hprof文件導(dǎo)入,都Add to Compare Basket之后即可進(jìn)行對比,這對于對比某個(gè)頁面相較與前一頁面的內(nèi)存增量來說是非常有意義的。 有一點(diǎn)比較不友好的是,MAT需要標(biāo)準(zhǔn)的.hprof文件,所以在AndroidStduio的Profiler中GC后dump出的內(nèi)存快照還要自己手動利用android sdk platform-tools下的hprof-conv進(jìn)行轉(zhuǎn)換一下才能被MAT打開。 //adb and hprof-conv //GC //dump java heap //conv hprof 2.內(nèi)存泄漏根據(jù)以往經(jīng)驗(yàn),其實(shí)做內(nèi)存優(yōu)化最先要搞定的應(yīng)該是內(nèi)存中的大頭,這類大頭對內(nèi)存的占用很大,也是內(nèi)存問題的主要禍?zhǔn)?,相對來說比較容易定位問題,且優(yōu)化后效果也非常明顯,性價(jià)比非常高。
相對內(nèi)存來說,這個(gè)大頭就是:
內(nèi)存泄漏檢測現(xiàn)在內(nèi)存泄漏的檢測已經(jīng)變得非常簡便了,使用App后在Android Profiler中先觸發(fā)GC然后dump內(nèi)存快照,之后點(diǎn)擊按package分類,就可以迅速查看到你的App目前在內(nèi)存中殘留的class,點(diǎn)擊class即可在右邊查看到對應(yīng)的實(shí)例以及引用對象。
排除內(nèi)存泄漏后,圖片就是另一個(gè)占用內(nèi)存大頭的對象了。 圖片對于圖片來說一個(gè)是顏色模式,檢查一下項(xiàng)目里的圖片的顏色模式,是否可以降低,比如從RGB_8888降到RGB_565,則每張圖片可以節(jié)省1/2的內(nèi)存,如果沒有使用到透明通道等的話基本上肉眼看不出差別。 還有一個(gè)是降低圖片的大小,可能你的ImageView只有你圖片的一半大,則這部分內(nèi)存就大大浪費(fèi)了,我們項(xiàng)目服務(wù)端會根據(jù)前端的參數(shù)做動態(tài)切圖。 前端也可以通過降低采樣率(inSampleSize)來達(dá)到降低圖片占用內(nèi)存大小的目的,但是這個(gè)采樣率InSampleSize只能是整數(shù)(甚至只能是2的次方),如果inSampleSize=2,則最終內(nèi)存占用就會是原來的1/4,適用于圖片過大很多的情況,對于只是想做小幅度壓縮的話,基本沒用。 ok,接下來開始做具體的內(nèi)存分析與稍微細(xì)致一點(diǎn)的內(nèi)存優(yōu)化。 3.靜態(tài)內(nèi)存分析優(yōu)化這邊說的靜態(tài)內(nèi)存指的是在伴隨著App的整個(gè)生命周期一直存在的那部分內(nèi)存,也就是打底的,具體獲取這部分內(nèi)存快照的方式是: 通過對靜態(tài)內(nèi)存數(shù)據(jù)的分析,主要發(fā)現(xiàn)了以下幾個(gè)問題: 問題1: App首頁的主圖有兩張(一張是保底圖,一張是動態(tài)加載的圖),都比較大,而且動態(tài)加載的圖回來后,保底圖并沒有及時(shí)被釋放 優(yōu)化:首先是對首頁的主圖進(jìn)行顏色通道的改變以及壓縮,可以大大降低這兩張圖所占的內(nèi)存,然后在動態(tài)加載圖回來后及時(shí)釋放掉保底圖 -5M 問題2: 首頁底部的輪播背景圖占用內(nèi)存1.6M,且在圖片加載回來后,背景圖一直沒有置空 優(yōu)化:首先一般來說對背景圖的質(zhì)量并沒有很高的要求,所以這張背景圖是可以被成倍壓縮的,并且在圖片加載回來后,背景圖要及時(shí)的釋放掉。同時(shí)首頁的多張輪播圖以及其他圖片都可以進(jìn)行顏色模式的改變以及質(zhì)量壓縮。 -1.6M -4M 問題3: 項(xiàng)目會在App啟動時(shí)拉一個(gè)接口獲取一些實(shí)驗(yàn)配置,放進(jìn)單例,在內(nèi)存分析時(shí)發(fā)現(xiàn),這些實(shí)驗(yàn)配置竟然接近1M 優(yōu)化:排查后發(fā)現(xiàn),接口拉的是整個(gè)公司所有部門的實(shí)驗(yàn)配置,上千個(gè),這也給遍歷拿一個(gè)實(shí)驗(yàn)配置帶來一定的性能損耗,推動接口去改進(jìn),只獲取當(dāng)前部門業(yè)務(wù)需要的實(shí)驗(yàn)配置,可節(jié)省內(nèi)存90%以上 -700K 問題4: 發(fā)現(xiàn)幾個(gè)lottie動畫一直沒有被回收,并且同一個(gè)lottie動畫會有幾個(gè)不同的實(shí)例存在,總共占用內(nèi)存450K 優(yōu)化:首先要確定幾個(gè)lottie動畫為什么在頁面退出后沒有被回收,并且同一個(gè)動畫有幾個(gè)不同的實(shí)例,很容易就聯(lián)想到內(nèi)存泄漏,由于頁面沒有被銷毀,所以導(dǎo)致幾個(gè)lottie動畫也沒有被回收,排查下來是項(xiàng)目里的RN頁面存在內(nèi)存泄漏,解決后大概可以節(jié)省3-5M內(nèi)存 問題5: SharePreference在內(nèi)存里占用了700K的內(nèi)存 優(yōu)化:由于SP中的東西是會一次性加載到內(nèi)存里并且保存為靜態(tài)的,直到App進(jìn)程結(jié)束才會被銷毀,所以SP中千萬別放大的對象,別圖一時(shí)方便把對象序列化成json后保存到SP里,優(yōu)化點(diǎn)就是把已經(jīng)保存在SP中的一些較大的json字符串或者對象遷移到文件或者數(shù)據(jù)庫緩存。 -400K 問題6: 埋點(diǎn)數(shù)據(jù) 優(yōu)化:產(chǎn)品或者運(yùn)營為了統(tǒng)計(jì)數(shù)據(jù)會在每個(gè)版本不斷的增加新埋點(diǎn),但是也需要定期去清理掉一些過時(shí)的不需要的埋點(diǎn),來適當(dāng)優(yōu)化內(nèi)存以及CPU的壓力。 問題7: 還有就是一些App里的單例以及一些靜態(tài)緩存 優(yōu)化:整個(gè)看下來在我們項(xiàng)目中這部分占整體的靜態(tài)內(nèi)存其實(shí)較小,綜合考慮內(nèi)存情況以及使用的高效性可以進(jìn)行一定程度的優(yōu)化,不過這部分內(nèi)存在App內(nèi)存緊張時(shí)可以選擇清理掉他們 我們可以選擇在App退到后臺后內(nèi)存緊張即將被Kill掉時(shí)選擇釋放掉一些內(nèi)存,如圖片的緩存,靜態(tài)緩存等來自保,具體做法是在Activity中重寫onTrimMemory()方法(4.0之前是onLowMemory()),在這里面來做內(nèi)存的釋放。 靜態(tài)內(nèi)存優(yōu)化:約15M 4.運(yùn)行時(shí)內(nèi)存分析優(yōu)化接下來做一下每個(gè)頁面的運(yùn)行時(shí)內(nèi)存分析優(yōu)化,這一部分就是隨著App運(yùn)行過程增長以及回收的內(nèi)存,這部分工作十分繁瑣,需要耐得住寂寞啊。 分析和優(yōu)化運(yùn)行時(shí)內(nèi)存主要是通過以下兩個(gè)核心方式:
首先介紹一下我們App中我們產(chǎn)線的主要核心頁面流程:搜索頁-->列表頁-->詳情頁-->信息頁-->支付,這里重點(diǎn)對列表頁和詳情頁做運(yùn)行時(shí)內(nèi)存分析優(yōu)化。 (1)列表頁內(nèi)存優(yōu)化 可以看到,絕大部分的內(nèi)存增加還是圖片,當(dāng)然還有一些靜態(tài)緩存: 問題1:列表item被回收時(shí)還持有圖片的引用 優(yōu)化:應(yīng)該在item被回收不可見時(shí)釋放掉對圖片的引用,這里注意RecyclerView與ListView的區(qū)別,如果是ListView,因?yàn)槊看蝘tem被回收后再次利用都會重新綁定數(shù)據(jù),只需在ImageView onDetchFromWindow的時(shí)候釋放掉圖片引用即可。而對于RecyclerView來說,因?yàn)楸换厥詹豢梢姇r(shí)第一選擇是放進(jìn)mCacheView中,而這里面的item被復(fù)用時(shí)并不會執(zhí)行bindViewHolder來重新綁定數(shù)據(jù),只有被回收進(jìn)mRecyclePool中后拿出來復(fù)用才會重新綁定數(shù)據(jù),所以如果是RecyclerView,我們釋放圖片引用的時(shí)機(jī)應(yīng)該是item被回收進(jìn)RecyclePool的時(shí)候,只要重寫Adapter中的onViewRecycled方法即可: @Override 問題2:圖片大小有優(yōu)化空間 優(yōu)化:這個(gè)因?yàn)槲宜驹诜?wù)端會對圖片進(jìn)行動態(tài)切圖,所以最簡單的方法就是根據(jù)實(shí)際情況來改變動態(tài)切圖的大小達(dá)到節(jié)省內(nèi)存的作用,當(dāng)然如果從服務(wù)端請求回來的圖片實(shí)在大(一般不要比裝載的ImageView要大),前端就可以采用降低采樣率的方式來進(jìn)行壓縮,當(dāng)然這個(gè)上面說了采樣率(inSampleSize)只支持2的次方,所以對圖片占用內(nèi)存大小的壓縮是非常大的,如果你只是想小幅度的壓縮,基本上這個(gè)是沒用的。 問題3:對ImageLoader圖片緩存策略的思考 ①對于UIL這個(gè)圖片框架,他的緩存策略是內(nèi)存緩存+磁盤緩存,內(nèi)存緩存默認(rèn)的數(shù)據(jù)結(jié)構(gòu)是 LruMemoryCache,對圖片是強(qiáng)引用,默認(rèn)最大 Size 是內(nèi)存的1/8,滿后會按照LRU算法對最近最不常用的圖片進(jìn)行移除,看起來比較合理,但是會有一個(gè)問題,就是當(dāng)圖片緩存達(dá)到1/8后則圖片所占的內(nèi)存一直會保持在接近1/8,它沒有自我清理的能力,可能長時(shí)間過去了這1/8內(nèi)存里的有些圖片都不再需要了,它也依然會保留在內(nèi)存里不會被清除,所以我們可以考慮對緩存的圖片做一個(gè)有效期的管理,圖片過期后則自動清理一波,這樣可以優(yōu)化很大一部分內(nèi)存空間。 ②由于UIL對于內(nèi)存緩存圖片是以“url+targetWidth+targetHeight”作為key,如果我們加載圖片的時(shí)候沒有設(shè)置targetSize,則框架里默認(rèn)會以ImageView的大小作為targetSize,那么就會出現(xiàn)一種情況,同一張圖片,由于放在大小有輕微差異的ImageView上顯示,則由于targetSize不一樣,會在內(nèi)存中被緩存兩份,當(dāng)然要解決這個(gè)問題也很簡單,只要設(shè)置denyCacheImageMultipleSizesInMemory()即可避免這種情況,這樣同一張圖片在內(nèi)存里就只會有一份緩存(之前的會被之后的替換掉)。 那么如何做到讓有輕微大小差異的ImageView加載同一張圖片時(shí)既實(shí)現(xiàn)在內(nèi)存緩存里進(jìn)行復(fù)用又不會在內(nèi)存緩存里保留兩份緩存呢?
(2)詳情頁的內(nèi)存分析優(yōu)化 首先詳情頁依然有大量的圖片,所以對于圖片的大小以及復(fù)用上的優(yōu)化上面已經(jīng)說了,這里就不重復(fù)說了。 問題1:在內(nèi)存里發(fā)現(xiàn)兩個(gè)極少概率出現(xiàn)的empty view,占用了接近2M的內(nèi)存 優(yōu)化:用ViewStub對empty view做了懶加載,對于這些沒有馬上用到的資源要做延遲加載,還有很多大概率不會出現(xiàn)的View更加要做懶加載。 -2M 問題2:發(fā)現(xiàn)詳情頁的輪播大圖的Viewpager用的Adapter是FragmentPagerAdapter,導(dǎo)致了所有的page都會被保存,當(dāng)圖片頁數(shù)多的時(shí)候,往后翻內(nèi)存會不斷上升。 優(yōu)化:這種頁數(shù)多的ViewPager使用FragmentStatePagerAdapter來替代,它只會保留前后pager,在頁數(shù)多的時(shí)候可以 節(jié)省大量內(nèi)存。 問題3:對于一些實(shí)在大的圖并且復(fù)用頻率并不高的大圖只采用文件緩存就行了,不做內(nèi)存緩存。 問題4:我們項(xiàng)目在debug下會打印網(wǎng)絡(luò)請求的reqeust和response,并且會用String.subString()對較長的response json進(jìn)行截取 優(yōu)化:本身subString()就比較耗內(nèi)存,所以在response較大的時(shí)候就會申請大量的內(nèi)存,好在這種情況只會在debug下發(fā)生,但是依然需要改進(jìn)這種打印。 5.監(jiān)控內(nèi)存的分析優(yōu)化并不是一兩個(gè)版本的事,而是一個(gè)必須每個(gè)版本持續(xù)進(jìn)行的工作,這需要一套完善的線上用戶內(nèi)存使用情況監(jiān)測系統(tǒng)來進(jìn)行數(shù)據(jù)上傳、數(shù)據(jù)分析、數(shù)據(jù)整理、數(shù)據(jù)對比,方便我們明確的了解每個(gè)版本線上App內(nèi)存的具體情況。公司的一套性能監(jiān)控平臺,可以在這方面給我們App開發(fā)人員提供很直觀的監(jiān)控?cái)?shù)據(jù)和版本迭代對比。 通過上面我們項(xiàng)目的內(nèi)存分析,可以發(fā)現(xiàn)圖片絕對是內(nèi)存中的一塊大頭,所以對于圖片的使用監(jiān)控就顯得尤為重要,我們自定義了一個(gè)簡單的可以監(jiān)控加載的圖片是否過大的ImageView,可以在debug階段發(fā)出警告,方便開發(fā)人員及早發(fā)現(xiàn)過大的圖片。 當(dāng)然要做的工作還有很多,比如當(dāng)我們發(fā)現(xiàn)占用內(nèi)存過高時(shí),可以嘗試來釋放一些靜態(tài)的緩存,一次來緩存內(nèi)存的壓力。 6.總結(jié)這個(gè)版本利用了點(diǎn)時(shí)間對項(xiàng)目的內(nèi)存占用做了以上分析以及優(yōu)化,還需要做的還有很多,之后的版本會繼續(xù)跟進(jìn),總得來說做內(nèi)存分析和優(yōu)化還是比較辛苦的,特別是各種內(nèi)存快照的分析以及對代碼問題的排查,當(dāng)然時(shí)間有限,可能很多地方說的可能也有疏漏或者錯(cuò)誤,紙上得來終覺淺,絕知此事要躬行,對于性能優(yōu)化特別內(nèi)存優(yōu)化這一塊,實(shí)踐遠(yuǎn)比理論得到的要多。 目前項(xiàng)目里關(guān)于流暢度以及耗電量還沒發(fā)現(xiàn)太大的問題,因?yàn)槊總€(gè)版本或多或少都會做一些優(yōu)化,線上也有數(shù)據(jù)監(jiān)測,之后還是想整理一下關(guān)于 |
|