前言
這是我在掘金的第一篇博客分享,最近在掘金上看了許多大佬的文章,學(xué)到了非常多的東西,實(shí)在是忍不住想要把我們平時(shí)工作中用到的一些優(yōu)化方案分享出來,其實(shí)也是一個(gè)大家一起討論學(xué)習(xí)的過程,希望大家可以多多交流 ~?
自我介紹
第一篇博客,總得介紹下自己~,有校友或者其他間接挨得著邊的聯(lián)系的可以私聊交流,前1/4 -> 1/3人生實(shí)在沒啥交集的也可以眼熟一下。祖籍贛,天府磨子橋文理學(xué)院七年計(jì)算機(jī),18年夏天畢業(yè),目前在北京海淀768工作,「脈脈」平臺(tái)客戶端開發(fā)一枚。喜歡打游戲唱歌擼貓次好次的,其他的沒了
背景
先簡(jiǎn)單講講跟oom糾結(jié)的歷史吧。
在18年年底,我們app進(jìn)行了一次非常大的版本更迭,因?yàn)闀r(shí)間緊急、業(yè)務(wù)繁忙、人數(shù)也沒達(dá)到可以湊人數(shù)可以讓某些人準(zhǔn)點(diǎn)下班的那種數(shù)量(各個(gè)公司的常規(guī)原因),業(yè)務(wù)線在對(duì)一些模塊進(jìn)行重構(gòu)和大量新需求的開發(fā)過程中,許許多多的細(xì)節(jié)沒有注意到,直接導(dǎo)致了后面一個(gè)月的崩潰率、OOM率猛增, 且居高不下。大概快到了千分之2的這個(gè)數(shù)量級(jí),這是非常非??植赖?。因此我們花了一段時(shí)間,集中的fix了一把OOM的相關(guān)問題,一頓操作,直接讓主版本的崩潰率來到了「萬分之一」,OOM率來到了十萬分之一這個(gè)數(shù)量級(jí)。
干掉OOM,我們干了什么?
不講廢話了,也不講那些網(wǎng)上都可以查到的一些常規(guī)優(yōu)化方法來填字?jǐn)?shù)了,我會(huì)針對(duì)如何去fix OOM這個(gè)目標(biāo),將思考的歷程以及解決問題的辦法分享出來,希望其中會(huì)有某一條經(jīng)驗(yàn)正好擊中你們,能起到一些幫助~~
開干??!下面的內(nèi)容,我會(huì)用一級(jí)標(biāo)題的字體~ 顯眼一些哈哈,畢竟前面都是啰嗦的廢話
一、排查內(nèi)存泄漏
首先fix OOM第一件事肯定是來排查內(nèi)存泄漏。想要排查內(nèi)存泄漏,那就第一步要對(duì)內(nèi)存泄漏進(jìn)行監(jiān)控、上報(bào)。
我們采用了LeakCanary,實(shí)現(xiàn)了一個(gè)自定義的Service繼承自DisplayLeakService,重寫afterDefaultHandling方法,將內(nèi)存泄漏上報(bào)到Sentry。
樣例代碼如下:
public static class LeakReportService extends DisplayLeakService {?
? ? @SuppressWarnings("ThrowableNotThrown")? ?
? ? @Override? ?
? ? protected void afterDefaultHandling(@NonNull HeapDump heapDump, @NonNull AnalysisResult result, @NonNull String leakInfo) {? ? ? ?
? ? ? ? if (!result.leakFound || result.excludedLeak) {? ? ? ? ? ?
? ? ? ? ? ? return;? ? ?
? ? ? ? }? ? ? ?
? ? ? ? try {? ? ? ? ? ?
? ? ? ? ? ? Exception exception = new Exception("Memory Leak from LeakCanary");? ? ? ? ? ?
? ? ? ? ? ? exception.setStackTrace(result.leakTraceAsFakeException().getStackTrace());? ? ? ? ? ?
? ? ? ? ? ? Sentry.capture(exception);? ? ? ?
? ? ? ? } catch (Exception e) {? ? ? ? ? ?
? ? ? ? ? ? e.printStackTrace();? ? ? ?
? ? ? ? }? ?
? ? }
}
當(dāng)內(nèi)存泄漏上報(bào)到sentry上面之后,我們直接觀察是哪里泄漏的就好了。通過sentry進(jìn)行監(jiān)控之后,項(xiàng)目里面的大部分內(nèi)存泄漏無處可逃~ ,內(nèi)存泄漏比較簡(jiǎn)單,我就不花大量篇幅去贅述了~,我自己看文章的過程中,最討厭篇幅太長(zhǎng)。。。
除了LeakCanary,我們還使用了Android Studio自帶的Profiler工具對(duì)內(nèi)存有進(jìn)行分析,包括內(nèi)存泄漏的問題和內(nèi)存峰值過高的問題。
profiler工具的使用方法我就不贅述了吧,講一下小技巧吧。
在排查bitmap對(duì)象,我們可以用Profiler直接看java 堆中的bitmap對(duì)象圖片的預(yù)覽~ 這樣可以直接定位到是哪里泄漏了以及哪里bitmap加載過大
方法:找到對(duì)應(yīng)的Bitmap對(duì)象,然后~ ,點(diǎn)擊它,然后就可以preview,如下圖:
二、兜底策略
我們可以知道的是,當(dāng)一個(gè)Activity的生命周期要走完了,那就說明我們絕大概率不會(huì)再使用這個(gè)Activity對(duì)象了,因此完全可以對(duì)他的可能導(dǎo)致整個(gè)Activity泄露的引用進(jìn)行清空,將其中的一些資源釋放干凈,比如有EditText的TextWatcher,這是非常容易泄露且在我們項(xiàng)目中大量出現(xiàn)的一個(gè)case,然后,于是乎我們加上了更加喪心病狂的兜底策略,
話不多說,直接上代碼
private void traverse(ViewGroup root) {? ?
? ? final int childCount = root.getChildCount();? ?
? ? for (int i = 0; i < childCount; ++i) {? ? ? ?
? ? ? ? final View child = root.getChildAt(i);? ? ? ?
? ? ? ? if (child instanceof ViewGroup) {? ? ? ? ? ?
? ? ? ? ? ? child.setBackground(null);? ? ? ? ? ?
? ? ? ? ? ? traverse((ViewGroup) child);? ? ? ?
? ? ? ? } else {? ? ? ? ? ?
? ? ? ? ? ? if (child != null) {? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? child.setBackground(null);? ? ? ? ? ?
? ? ? ? ? ? }? ? ? ? ? ?
? ? ? ? ? ? if (child instanceof ImageView) {? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ((ImageView) child).setImageDrawable(null);? ? ? ? ? ?
? ? ? ? ? ? } else if (child instanceof EditText) {? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ((EditText) child).cleanWatchers();? ? ? ? ? ?
? ? ? ? ? ? }? ? ? ?
? ? ? ? }? ?
? ? }
}
我們?cè)诨怋aseActivity的onDestory()方法中進(jìn)行了一些資源和引用的清除
三、內(nèi)存峰值太高
在我們把能fix的內(nèi)存泄漏都盤了一便之后,上線一周并沒有發(fā)現(xiàn)數(shù)據(jù)好轉(zhuǎn),OOM率還是高居不下,于是乎,我們開始懷疑內(nèi)存峰值太高的問題,在我們的項(xiàng)目中不僅僅只有native的部分模塊,還有混合的H5、RN模塊,當(dāng)起一個(gè)ReactActivity的實(shí)例時(shí),內(nèi)存峰值總是漲的特別特別厲害,同時(shí)項(xiàng)目中有消息流的展現(xiàn),其中會(huì)包含著大量的圖片展示,這也是導(dǎo)致內(nèi)存峰值太高的原因(Bitmap對(duì)象太大以及太多)
我們又拿出了老伙伴 - Profiler,這可是分析bitmap對(duì)象的利器,可以直接看到大小、圖片的預(yù)覽,以及可以通過 go to instance一層一層的找到到底是誰在引用它。比如下面這個(gè)例子,直接看引用就知道是被Fresco所引用了~ 直接就在CountingMemoryCache中。
其實(shí)我們主要還是需要去關(guān)注Bitmap對(duì)象的分配和不合法持有導(dǎo)致的內(nèi)存峰值問題,如果一個(gè)bitmap對(duì)象有3M,然后持有一個(gè)幾十上百個(gè)在內(nèi)存中,這誰吃得消,低端機(jī)器老早直接OOM了。
查Bitmap分配查出來的問題
目前我們項(xiàng)目中用的圖片加載框架有兩個(gè),UIL、Fresco,UIL我吐槽很久了,這么多年沒更新,老早就該換了~?
1. UIL加載圖片在我們項(xiàng)目中的問題:
沒有傳入合適的Config,絕大多數(shù)地方傳的都是ARGB_8888,其實(shí)根本沒必要,改成565直接少一半內(nèi)存占用
用UIL進(jìn)行l(wèi)oadImage時(shí),沒有傳入targetSize,這就直接導(dǎo)致了UIL內(nèi)部是以屏幕的尺寸去Decode的Bitmap對(duì)象,想象一下,一個(gè)特別小的頭像View,持有著一個(gè)屏幕大小尺寸的Bitmap對(duì)象,這誰頂?shù)米 ?/p>
許多地方不需要存內(nèi)存緩存,比如閃屏廣告圖,app啟動(dòng)之后就不會(huì)再使用了,可以加載的時(shí)候 memoryCache(false)
許多地方不需要磁盤緩存,比如發(fā)布動(dòng)態(tài),從圖庫中選圖,不需要再存一份磁盤緩存了,本身那些圖片都是本地圖片。直接 diskCache(false)
2.Fresco在RN頁面中使用的問題,
通過看代碼可以知道,RN頁面銷毀的時(shí)候,連帶著Fresco的內(nèi)存緩存都會(huì)被清空,
直接上代碼圖:
代碼看到這里,似乎Fresco不用擔(dān)心了,既然會(huì)清空Fresco的內(nèi)存緩存,何愁會(huì)引起內(nèi)存峰值過高,如果讀者看到這里,也有這個(gè)想法,那就大錯(cuò)特錯(cuò)了。話不多說,直接上圖。
Fresco相關(guān)源碼的邏輯這篇文章就不分析了,主要講思路,具體的源碼分析后面我會(huì)用單獨(dú)的篇幅去講~?
為什么我會(huì)對(duì)Fresco的動(dòng)圖緩存這么敏感,那還是Profiler的功勞,我在用Profiler查看內(nèi)存中bitmap的分配的時(shí)候,發(fā)現(xiàn)有上百?gòu)埖腖oading圖沒有銷毀(我們Loading圖是動(dòng)圖,大概每幀的Bitmap對(duì)象在360K左右), 且打開的頁面越多,Loading的bitmap就會(huì)越多。(這是因?yàn)槲覀兠恳粋€(gè)RN頁面都會(huì)帶一個(gè)Loading動(dòng)畫)
0.3M * 100 = 30M,不少了。。。,說實(shí)話有點(diǎn)恐怖
于是乎,干掉他們,這里用了反射,正常情況下不需要反射。直接拿ImagePipelineFactory中的對(duì)象來clear就好
public static void clearAnimationCache() {? ?
if (frescoAnimationCache == null) {? ? ? ?
? ? //采用反射的方法,如果native、rn同時(shí)初始化Fresco,會(huì)造成Fresco內(nèi)部存儲(chǔ)動(dòng)圖的CountingMemoryCache不是Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache()了? ? ? ?
? ? //暫時(shí)用反射的方法,拿到存儲(chǔ)動(dòng)圖緩存的cache,并清空? ? ? ?
? ? try {? ? ? ? ? ?
? ? ? ? Class imagePipelineFactoryClz = Class.forName("com.facebook.imagepipeline.core.ImagePipelineFactory");? ? ? ? ? ?
? ? ? ? Field mAnimatedFactoryField = imagePipelineFactoryClz.getDeclaredField("mAnimatedFactory");? ? ? ? ? ?
? ? ? ? mAnimatedFactoryField.setAccessible(true);? ? ? ? ? ?
? ? ? ? AnimatedFactoryV2Impl animatedFactoryV2 = (AnimatedFactoryV2Impl) mAnimatedFactoryField.get(Fresco.getImagePipelineFactory());? ? ? ? ? ?
? ? ? ? Class animatedFactoryV2ImplClz = Class.forName("com.facebook.fresco.animation.factory.AnimatedFactoryV2Impl");? ? ? ? ? ?
? ? ? ? Field mBackingCacheField = animatedFactoryV2ImplClz.getDeclaredField("mBackingCache");? ? ? ? ? ?
? ? ? ? mBackingCacheField.setAccessible(true);? ? ? ? ? ?
? ? ? ? frescoAnimationCache = (CountingMemoryCache) mBackingCacheField.get(animatedFactoryV2);? ? ? ?
? ? } catch (Exception e) {? ? ? ? ? ?
? ? ? ? Log.e("FrescoUtil", e.getMessage(), e);? ? ? ?
? ? }? ?
}? ?
if (frescoAnimationCache != null) {?
? ? frescoAnimationCache.clear();?
}? ?
Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache().clear();
Fresco.getImagePipelineFactory().getEncodedCountingMemoryCache().clear();
}
又一個(gè)兜底方案
為了防止峰值過高,我們還起了一個(gè)線程,定時(shí)的去監(jiān)控實(shí)時(shí)的內(nèi)存使用情況,如果內(nèi)存緊急了,直接清空UIL/Fresco的內(nèi)存緩存救急
五、特大圖排查優(yōu)化
我想大家都不會(huì)想到,在我們app的登錄注冊(cè)頁,會(huì)有一個(gè)圖片輪播控件,它輪播著五六張單張6M+的Bitmap。。。當(dāng)然,特大圖不僅限于此,還有其他地方會(huì)有相同情況,我們通過Profiler找出那些大的bitmap對(duì)象,然后預(yù)覽之后確定是哪里在用的。
直接優(yōu)化掉。最不濟(jì) 8888 -> 565就少一半內(nèi)存占用
怎么講呢,,OOM這個(gè)東西,還沒咋僵持呢,就沒了。。
六、總結(jié)
深夜一時(shí)興起想分享和記錄一些什么,就隨便寫了這一篇博客,寫的不詳細(xì),沒有排版和良好的語言組織,單純的就是想分享
總結(jié)一下吧,我們?yōu)榱薴ix OOM所做的事情:
檢查內(nèi)存泄漏
包括常見的Context泄漏、單例泄漏、EditText的TextWatcher泄漏等等,找到并fix他們,最簡(jiǎn)單的例子,能傳application的地方就不要硬傳個(gè)activity過去,能用匿名內(nèi)部類的地方,就不要為了一個(gè)callback讓Activity去實(shí)現(xiàn),然后傳Activity.this進(jìn)去。。
兜底方案:
在Activity onDestory的時(shí)候,遍歷View樹,清空backGround、Drawable、EditText的TextWatcher等
內(nèi)存峰值的優(yōu)化。內(nèi)存泄漏會(huì)導(dǎo)致內(nèi)存峰值,內(nèi)存峰值是OOM的大鍋,舉個(gè)例子當(dāng)可用內(nèi)存不夠分配一個(gè)Bitmap對(duì)象時(shí),就會(huì)OOM,Android上大多數(shù)的內(nèi)存峰值都是圖片的加載帶來的?,F(xiàn)在許多的app中都有信息流的展現(xiàn),可能會(huì)有許多的九宮格展示圖片,且Bitmap對(duì)象本身就可以非常大。
優(yōu)化UIL的使用
memoryCache選用,不是所有的圖片加載都需要UIL去塞一份內(nèi)存緩存的,比如閃屏圖
ImageLoader.getInstance().displayImage()的時(shí)候,傳進(jìn)去的Option不要無腦ARGB_8888,講道理來說,無腦RGB_565都是沒啥問題的。。
調(diào)用displayImage的時(shí)候,最好傳一個(gè)ImageSize作為targetSize,這個(gè)size可以是你的ImageView的尺寸,當(dāng)View尺寸本身不確定的時(shí)候,可以傳一個(gè)大概值,比如我們app中有好些個(gè)的頭像標(biāo)準(zhǔn)尺寸,為了偷懶,直接傳MaxAvatarSize就ok
Fresco的優(yōu)化
RN中使用Fresco加載圖片,在RN Activity銷毀的時(shí)候,會(huì)將Fresco默認(rèn)的memory cache清空,但是動(dòng)圖的緩存沒有清。手動(dòng)清一下。我們項(xiàng)目中每個(gè)RN頁面都會(huì)帶一個(gè)Loading動(dòng)圖,所以吃了大虧。。
持續(xù)的后臺(tái)監(jiān)控內(nèi)存,起一個(gè)HandlerThread,一直在后臺(tái)拿內(nèi)存使用的狀態(tài),達(dá)到了危險(xiǎn)警戒線就清空一把UIL、Fresco的memory cache,先讓世界安靜一下
需要對(duì)內(nèi)存泄漏、OOM、Crash、ANR進(jìn)行監(jiān)控
一些其他的細(xì)節(jié)暫時(shí)想不起來了,凌晨四點(diǎn)腦子不清醒了
后續(xù)關(guān)于這里面涉及到的Fresco的部分源碼分析、Profiler的最佳使用姿勢(shì)(經(jīng)過這一次的折騰,總結(jié)出來一句話,Profiler真香)、以及前段時(shí)間在做的App的啟動(dòng)速度優(yōu)化等等等等等都會(huì)單獨(dú)拎文章去分享,后續(xù)也會(huì)帶來更多,涉及的內(nèi)容包括但不限于:
主流框架的一些設(shè)計(jì)思想的分享
工作項(xiàng)目中遇到的麻煩和坑
工作中蹚坑的一些經(jīng)驗(yàn)
好代碼
壞代碼
壞的設(shè)計(jì)
程序員從頭發(fā)濃密到成為下雨天報(bào)警員的心路歷程
。。。
我的簡(jiǎn)書?鄒啊濤濤濤的簡(jiǎn)書
我的CSDN?鄒啊濤濤濤的CSDN
我的掘金?鄒啊濤濤濤的掘金