作者:舒大飛
鏈接:
https://juejin.im/post/5b1b5e29f265da6e01174b84
由于項(xiàng)目里之前線上版本出現(xiàn)過一定比例的OOM,雖然比例并不大,但是還是暴露了一定的問題,所以打算對我們App分為幾個步驟進(jìn)行內(nèi)存分析和優(yōu)化膛虫,當(dāng)然內(nèi)存的優(yōu)化是個長期的過程舍哄,不是一兩個版本的事,每個版本都需要收集線上內(nèi)存數(shù)據(jù)進(jìn)行監(jiān)控以及分析普舆。
版本迭代過程中恬口,內(nèi)存增長過快,不僅會導(dǎo)致一定概率的OOM沼侣,運(yùn)行時若出現(xiàn)內(nèi)存抖動祖能,導(dǎo)致頻繁GC,則會對App的流暢度以及用戶體驗(yàn)造成很大影響华临。
本文主要會根據(jù)實(shí)際項(xiàng)目中優(yōu)化步驟分為以下幾部分:
Android內(nèi)存分析基礎(chǔ)
內(nèi)存泄漏
靜態(tài)內(nèi)存分析優(yōu)化
運(yùn)行時內(nèi)存分析優(yōu)化
監(jiān)控
1.Android內(nèi)存分析基礎(chǔ)
這部分主要先介紹一些進(jìn)行內(nèi)存分析的基礎(chǔ)方法以及工具芯杀,對這部分比較熟悉的同學(xué)可以先跳過哈。
一.App的內(nèi)存使用情況概覽
每個App進(jìn)程可以分配到的最大內(nèi)存是有限的,當(dāng)然不同手機(jī)每個App進(jìn)程可以分配到的最大內(nèi)存有可能不一樣揭厚,可以通過以下命令進(jìn)行查看:
adb shell getprop | grep dalvik.vm.heapsize
我們可以輸出我們App的內(nèi)存使用情況概覽:
adb shell dumpsys meminfo 包名
我們就可以看到:
Pss: 該進(jìn)程獨(dú)占的內(nèi)存+與其他進(jìn)程共享的內(nèi)存(按比例分配却特,比如與其他3個進(jìn)程共享9K內(nèi)存,則這部分為3K)
Privete Dirty:該進(jìn)程獨(dú)享內(nèi)存
Heap Size:分配的內(nèi)存
Heap Alloc:已使用的內(nèi)存
Heap Free:空閑內(nèi)存
二筛圆、Android Profiler
AndroidStduio3.0后Android Profiler變得比之前更強(qiáng)大裂明,內(nèi)存分析頁變得更加直觀更加方便,下面是截圖:
進(jìn)程占用總內(nèi)存
javaHeap:這部分內(nèi)存大小是有限制的太援,溢出則會OOM闽晦,這部分內(nèi)存也是我們分析優(yōu)化的重點(diǎn)
NativeHeap:native層的 so 中調(diào)用malloc或new創(chuàng)建的內(nèi)存,對于單個進(jìn)程來說大小沒有限制,所以可以利用在native層分配內(nèi)存來緩解javaHeap的壓力(比如2.3.3之前Android Bitmap的內(nèi)存分配就是在native層提岔,之后移到j(luò)avaHeap, 8.0又回到native)
Graphics:這部分一般游戲app中用的較多仙蛉,OpenGL和SurfaceFlinger相關(guān)的內(nèi)存,若沒有直接調(diào)用到OpenGL,則一般不會涉及到這塊內(nèi)存
Stack:棧碱蒙,了解jvm內(nèi)存模型的應(yīng)該都知道
Code: 代碼荠瘪,主要是dex以及so等占用的內(nèi)存
Others:就是others啦
所以我們可以看到事實(shí)上我們可以優(yōu)化的點(diǎn)有:JavaHeap、NativeHeap赛惩、Stack哀墓、Code所占用的內(nèi)存
三、強(qiáng)大的MAT
MAT是做比較細(xì)致的內(nèi)存分析的利器了喷兼,功能十分強(qiáng)大,其中的:
Hisogram:Lists number of instances per class
Dominator Tree:List the biggest objects and what they keep alive.
可以非常方便的排序查看當(dāng)前內(nèi)存中最占內(nèi)存的class或者實(shí)體對象篮绰,而且有一條非常清晰的引用鏈來查看該對象的持有者,這對內(nèi)存的分析以及內(nèi)存泄漏的分析都是非常友好的季惯。
同時MAT支持compare對比功能吠各,將兩個.hprof文件導(dǎo)入,都Add to Compare Basket之后即可進(jìn)行對比星瘾,這對于對比某個頁面相較與前一頁面的內(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打開琳状。
當(dāng)然如果覺得麻煩的話也可以自己寫個腳本執(zhí)行幾條命令來直接完成GC->dump java heap->轉(zhuǎn)換.hprof文件? 這個流程:
//adb and hprof-conv
ADB=${ANDROID_HOME}/platform-tools/adb
HPROF_CONV=${ANDROID_HOME}/platform-tools/hprof-conv
//GC${ADB} shell pkill -l10$(PACKAGE_NAME)
//dump java heap${ADB} shell"am dumpheap $(PACKAGE_NAME) $(OUT_PATH)"
//conv hprof
${HPROF_CONV} -z ${FILE_NAME} droid-${FILE_NAME}
2.內(nèi)存泄漏
根據(jù)以往經(jīng)驗(yàn)磕瓷,其實(shí)做內(nèi)存優(yōu)化最先要搞定的應(yīng)該是內(nèi)存中的大頭,這類大頭對內(nèi)存的占用很大念逞,也是內(nèi)存問題的主要禍?zhǔn)桌常鄬碚f比較容易定位問題,且優(yōu)化后效果也非常明顯翎承,性價比非常高硕盹。
事實(shí)上很多優(yōu)化都是這樣,比如減包大小的優(yōu)化叨咖,也是要先分析出主要大頭禍?zhǔn)状窭热缈赡苣愕陌锇艘粡?M大小的無用圖片啊胶,如果你沒找到這種禍?zhǔn)祝赡苣阕隽舜罅康墓ぷ魅ハ朕k法減少無用代碼等垛贤,最終可能只有幾百K的收益焰坪。
相對內(nèi)存來說,這個大頭就是:
內(nèi)存泄漏
圖片
所以首先你要確保你的應(yīng)用里沒有存在內(nèi)存泄漏聘惦,然后再去做其他的內(nèi)存優(yōu)化某饰。
內(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í)例以及引用對象。
當(dāng)然你也可以在debug下集成LeakCanary做內(nèi)存泄漏監(jiān)控警告
排除內(nèi)存泄漏后禀酱,圖片就是另一個占用內(nèi)存大頭的對象了炬守。
圖片
對于圖片來說一個是顏色模式,檢查一下項(xiàng)目里的圖片的顏色模式比勉,是否可以降低劳较,比如從RGB_8888降到RGB_565驹止,則每張圖片可以節(jié)省1/2的內(nèi)存浩聋,如果沒有使用到透明通道等的話基本上肉眼看不出差別。
還有一個是降低圖片的大小臊恋,可能你的ImageView只有你圖片的一半大衣洁,則這部分內(nèi)存就大大浪費(fèi)了,我們項(xiàng)目服務(wù)端會根據(jù)前端的參數(shù)做動態(tài)切圖抖仅。
前端也可以通過降低采樣率(inSampleSize)來達(dá)到降低圖片占用內(nèi)存大小的目的坊夫,但是這個采樣率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的整個生命周期一直存在的那部分內(nèi)存渡紫,也就是打底的到推,具體獲取這部分內(nèi)存快照的方式是:
打開App開始重度使用App,基本打開每一個主要頁面主要功能惕澎,然后回到首頁莉测,進(jìn)開發(fā)者選項(xiàng)打開"不保留后臺活動",然后將我們的app退到后臺唧喉。最后GC捣卤,dump出內(nèi)存快照忍抽。
下面是我們app dump出的內(nèi)存快照,進(jìn)行分析后制圖如下:
通過對靜態(tài)內(nèi)存數(shù)據(jù)的分析董朝,主要發(fā)現(xiàn)了以下幾個問題:
問題1:App首頁的主圖有兩張(一張是保底圖梯找,一張是動態(tài)加載的圖),都比較大益涧,而且動態(tài)加載的圖回來后锈锤,保底圖并沒有及時被釋放
優(yōu)化:首先是對首頁的主圖進(jìn)行顏色通道的改變以及壓縮,可以大大降低這兩張圖所占的內(nèi)存闲询,然后在動態(tài)加載圖回來后及時釋放掉保底圖-5M
問題2:首頁底部的輪播背景圖占用內(nèi)存1.6M久免,且在圖片加載回來后,背景圖一直沒有置空
優(yōu)化:首先一般來說對背景圖的質(zhì)量并沒有很高的要求扭弧,所以這張背景圖是可以被成倍壓縮的阎姥,并且在圖片加載回來后,背景圖要及時的釋放掉鸽捻。同時首頁的多張輪播圖以及其他圖片都可以進(jìn)行顏色模式的改變以及質(zhì)量壓縮呼巴。-1.6M -4M
問題3:項(xiàng)目會在App啟動時拉一個接口獲取一些實(shí)驗(yàn)配置,放進(jìn)單例御蒲,在內(nèi)存分析時發(fā)現(xiàn)衣赶,這些實(shí)驗(yàn)配置竟然接近1M
優(yōu)化:排查后發(fā)現(xiàn),接口拉的是整個公司所有部門的實(shí)驗(yàn)配置厚满,上千個府瞄,這也給遍歷拿一個實(shí)驗(yàn)配置帶來一定的性能損耗,推動接口去改進(jìn)碘箍,只獲取當(dāng)前部門業(yè)務(wù)需要的實(shí)驗(yàn)配置遵馆,可節(jié)省內(nèi)存90%以上-700K
問題4:發(fā)現(xiàn)幾個lottie動畫一直沒有被回收,并且同一個lottie動畫會有幾個不同的實(shí)例存在丰榴,總共占用內(nèi)存450K
優(yōu)化:首先要確定幾個lottie動畫為什么在頁面退出后沒有被回收货邓,并且同一個動畫有幾個不同的實(shí)例,很容易就聯(lián)想到內(nèi)存泄漏四濒,由于頁面沒有被銷毀换况,所以導(dǎo)致幾個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中千萬別放大的對象姆涩,別圖一時方便把對象序列化成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)計數(shù)據(jù)會在每個版本不斷的增加新埋點(diǎn)骨饿,但是也需要定期去清理掉一些過時的不需要的埋點(diǎn)亏栈,來適當(dāng)優(yōu)化內(nèi)存以及CPU的壓力台腥。
問題7:還有就是一些App里的單例以及一些靜態(tài)緩存
優(yōu)化:整個看下來在我們項(xiàng)目中這部分占整體的靜態(tài)內(nèi)存其實(shí)較小,綜合考慮內(nèi)存情況以及使用的高效性可以進(jìn)行一定程度的優(yōu)化绒北,不過這部分內(nèi)存在App內(nèi)存緊張時可以選擇清理掉他們
我們可以選擇在App退到后臺后內(nèi)存緊張即將被Kill掉時選擇釋放掉一些內(nèi)存黎侈,如圖片的緩存,靜態(tài)緩存等來自保闷游,具體做法是在Activity中重寫onTrimMemory()方法(4.0之前是onLowMemory())峻汉,在這里面來做內(nèi)存的釋放。
靜態(tài)內(nèi)存優(yōu)化:約15M
4.運(yùn)行時內(nèi)存分析優(yōu)化
接下來做一下每個頁面的運(yùn)行時內(nèi)存分析優(yōu)化脐往,這一部分就是隨著App運(yùn)行過程增長以及回收的內(nèi)存休吠,這部分工作十分繁瑣,需要耐得住寂寞啊业簿。
分析和優(yōu)化運(yùn)行時內(nèi)存主要是通過以下兩個核心方式:
從首頁開始用腳本dump出每個頁面的內(nèi)存快照文件瘤礁,然后利用MAT的對比功能,找出每個頁面相對于上個頁面內(nèi)存里主要增加了哪些東西梅尤,做針對性優(yōu)化
利用Android Profiler實(shí)時觀察進(jìn)入每個頁面后的內(nèi)存變化情況柜思,對產(chǎn)生的內(nèi)存較大波峰做分析
首先介紹一下我們App中我們產(chǎn)線的主要核心頁面流程:搜索頁-->列表頁-->詳情頁-->信息頁-->支付,這里重點(diǎn)對列表頁和詳情頁做運(yùn)行時內(nèi)存分析優(yōu)化巷燥。
(1)列表頁內(nèi)存優(yōu)化
下面是列表頁的內(nèi)存快照與搜索頁的對比:
可以看到赡盘,絕大部分的內(nèi)存增加還是圖片,當(dāng)然還有一些靜態(tài)緩存:
問題1:列表item被回收時還持有圖片的引用
優(yōu)化:應(yīng)該在item被回收不可見時釋放掉對圖片的引用矾湃,這里注意RecyclerView與ListView的區(qū)別亡脑,如果是ListView,因?yàn)槊看蝘tem被回收后再次利用都會重新綁定數(shù)據(jù)邀跃,只需在ImageView onDetchFromWindow的時候釋放掉圖片引用即可。而對于RecyclerView來說蛙紫,因?yàn)楸换厥詹豢梢姇r第一選擇是放進(jìn)mCacheView中拍屑,而這里面的item被復(fù)用時并不會執(zhí)行bindViewHolder來重新綁定數(shù)據(jù),只有被回收進(jìn)mRecyclePool中后拿出來復(fù)用才會重新綁定數(shù)據(jù)坑傅,所以如果是RecyclerView,我們釋放圖片引用的時機(jī)應(yīng)該是item被回收進(jìn)RecyclePool的時候僵驰,只要重寫Adapter中的onViewRecycled方法即可:
@OverridepublicvoidonViewRecycled(@Nullable VH holder){super.onViewRecycled(holder);if(holder !=null) {//做釋放圖片引用的操作}}
問題2:圖片大小有優(yōu)化空間
優(yōu)化:這個因?yàn)槲宜驹诜?wù)端會對圖片進(jìn)行動態(tài)切圖,所以最簡單的方法就是根據(jù)實(shí)際情況來改變動態(tài)切圖的大小達(dá)到節(jié)省內(nèi)存的作用唁毒,當(dāng)然如果從服務(wù)端請求回來的圖片實(shí)在大(一般不要比裝載的ImageView要大)蒜茴,前端就可以采用降低采樣率的方式來進(jìn)行壓縮,當(dāng)然這個上面說了采樣率(inSampleSize)只支持2的次方浆西,所以對圖片占用內(nèi)存大小的壓縮是非常大的粉私,如果你只是想小幅度的壓縮,基本上這個是沒用的近零。
問題3:對ImageLoader圖片緩存策略的思考
①對于UIL這個圖片框架诺核,他的緩存策略是內(nèi)存緩存+磁盤緩存抄肖,內(nèi)存緩存默認(rèn)的數(shù)據(jù)結(jié)構(gòu)是LruMemoryCache,對圖片是強(qiáng)引用,默認(rèn)最大Size是內(nèi)存的1/8,滿后會按照LRU算法對最近最不常用的圖片進(jìn)行移除窖杀,看起來比較合理漓摩,但是會有一個問題,就是當(dāng)圖片緩存達(dá)到1/8后則圖片所占的內(nèi)存一直會保持在接近1/8入客,它沒有自我清理的能力管毙,可能長時間過去了這1/8內(nèi)存里的有些圖片都不再需要了,它也依然會保留在內(nèi)存里不會被清除桌硫,所以我們可以考慮對緩存的圖片做一個有效期的管理锅风,圖片過期后則自動清理一波,這樣可以優(yōu)化很大一部分內(nèi)存空間鞍泉。
②由于UIL對于內(nèi)存緩存圖片是以“url+targetWidth+targetHeight”作為key皱埠,如果我們加載圖片的時候沒有設(shè)置targetSize,則框架里默認(rèn)會以ImageView的大小作為targetSize,那么就會出現(xiàn)一種情況咖驮,同一張圖片边器,由于放在大小有輕微差異的ImageView上顯示,則由于targetSize不一樣托修,會在內(nèi)存中被緩存兩份忘巧,當(dāng)然要解決這個問題也很簡單,只要設(shè)置denyCacheImageMultipleSizesInMemory()即可避免這種情況睦刃,這樣同一張圖片在內(nèi)存里就只會有一份緩存(之前的會被之后的替換掉)砚嘴。設(shè)置完denyCacheImageMultipleSizesInMemory()后又會出現(xiàn)一個新問題,雖然內(nèi)存里同一張圖片只有一份了涩拙,但這也意味著有輕微差異的ImageView加載的同一張圖片在內(nèi)存里沒辦法被復(fù)用了际长,每次都要去磁盤緩存里重新加載(磁盤緩存是只以url作為key的)。
那么如何做到讓有輕微大小差異的ImageView加載同一張圖片時既實(shí)現(xiàn)在內(nèi)存緩存里進(jìn)行復(fù)用又不會在內(nèi)存緩存里保留兩份緩存呢兴泥?
開啟denyCacheImageMultipleSizesInMemory()避免同一張圖片因?yàn)閠argetSize不同而存在多個內(nèi)存緩存
將有輕微大小差異的ImageView加載圖片時手動設(shè)置一樣的targetSize,這樣緩存的Key就一致了工育,就可以實(shí)現(xiàn)在內(nèi)存里進(jìn)行復(fù)用了,而指定一樣的targetSize并不會有什么風(fēng)險搓彻,因?yàn)樯厦嬲f了如绸,只有你指定的targetSize比圖片實(shí)際大小小2倍以上,采樣率才會生效旭贬,實(shí)際圖片才會被壓縮怔接。
(2)詳情頁的內(nèi)存分析優(yōu)化
可以看看剛進(jìn)入詳情頁后會有一個明顯的波峰,通過點(diǎn)擊Adnroid Profiler上的紅色圓點(diǎn)來記錄查看這段波峰里的內(nèi)存分配稀轨。
首先詳情頁依然有大量的圖片扼脐,所以對于圖片的大小以及復(fù)用上的優(yōu)化上面已經(jīng)說了,這里就不重復(fù)說了靶端。
問題1:在內(nèi)存里發(fā)現(xiàn)兩個極少概率出現(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ù)多的時候须喂,往后翻內(nèi)存會不斷上升吁断。
優(yōu)化:這種頁數(shù)多的ViewPager使用FragmentStatePagerAdapter來替代,它只會保留前后pager,在頁數(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較大的時候就會申請大量的內(nèi)存,好在這種情況只會在debug下發(fā)生卒废,但是依然需要改進(jìn)這種打印沛厨。
5.監(jiān)控
內(nèi)存的分析優(yōu)化并不是一兩個版本的事,而是一個必須每個版本持續(xù)進(jìn)行的工作摔认,這需要一套完善的線上用戶內(nèi)存使用情況監(jiān)測系統(tǒng)來進(jìn)行數(shù)據(jù)上傳逆皮、數(shù)據(jù)分析、數(shù)據(jù)整理参袱、數(shù)據(jù)對比电谣,方便我們明確的了解每個版本線上App內(nèi)存的具體情況。公司的一套性能監(jiān)控平臺抹蚀,可以在這方面給我們App開發(fā)人員提供很直觀的監(jiān)控數(shù)據(jù)和版本迭代對比剿牺。
通過上面我們項(xiàng)目的內(nèi)存分析,可以發(fā)現(xiàn)圖片絕對是內(nèi)存中的一塊大頭况鸣,所以對于圖片的使用監(jiān)控就顯得尤為重要牢贸,我們自定義了一個簡單的可以監(jiān)控加載的圖片是否過大的ImageView,可以在debug階段發(fā)出警告镐捧,方便開發(fā)人員及早發(fā)現(xiàn)過大的圖片。
當(dāng)然要做的工作還有很多臭增,比如當(dāng)我們發(fā)現(xiàn)占用內(nèi)存過高時懂酱,可以嘗試來釋放一些靜態(tài)的緩存誊抛,一次來緩存內(nèi)存的壓力。
6.總結(jié)
這個版本利用了點(diǎn)時間對項(xiàng)目的內(nèi)存占用做了以上分析以及優(yōu)化拗窃,還需要做的還有很多瞎领,之后的版本會繼續(xù)跟進(jìn),總得來說做內(nèi)存分析和優(yōu)化還是比較辛苦的九默,特別是各種內(nèi)存快照的分析以及對代碼問題的排查,當(dāng)然時間有限驼修,可能很多地方說的可能也有疏漏或者錯誤,紙上得來終覺淺乙各,絕知此事要躬行,對于性能優(yōu)化特別內(nèi)存優(yōu)化這一塊耳峦,實(shí)踐遠(yuǎn)比理論得到的要多。
目前項(xiàng)目里關(guān)于流暢度以及耗電量還沒發(fā)現(xiàn)太大的問題蹲坷,因?yàn)槊總€版本或多或少都會做一些優(yōu)化,線上也有數(shù)據(jù)監(jiān)測冠句,之后還是想整理一下關(guān)于卡頓流程度的分析優(yōu)化以及耗電量的分析優(yōu)化實(shí)踐轻掩。
作者:舒大飛
鏈接:https://juejin.im/post/5b1b5e29f265da6e01174b84
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)懦底,非商業(yè)轉(zhuǎn)載請注明出處唇牧。