這是我在掘金的第一篇博客分享,最近在掘金上看了許多大佬的文章,學到了很是多的東西,實在是忍不住想要把咱們平時工做中用到的一些優化方案分享出來,其實也是一個你們一塊兒討論學習的過程,但願你們能夠多多交流 ~ java
第一篇博客,總得介紹下本身~,有校友或者其餘間接捱得着邊的聯繫的能夠私聊交流,前1/4 -> 1/3人生實在沒啥交集的也能夠眼熟一下。祖籍贛,天府磨子橋文理學院七年計算機,18年夏天畢業,目前在北京海淀768工做,「脈脈」平臺客戶端開發一枚。喜歡打遊戲唱歌擼貓次好次的,其餘的沒了程序員
先簡單講講跟oom糾結的歷史吧。web
在18年年末,咱們app進行了一次很是大的版本更迭,由於時間緊急、業務繁忙、人數也沒達到能夠湊人數可讓某些人準點下班的那種數量(各個公司的常規緣由),業務線在對一些模塊進行重構和大量新需求的開發過程當中,許許多多的細節沒有注意到,直接致使了後面一個月的崩潰率、OOM率猛增, 且居高不下。大概快到了千分之2的這個數量級,這是很是很是恐怖的。所以咱們花了一段時間,集中的fix了一把OOM的相關問題,一頓操做,直接讓主版本的崩潰率來到了「萬分之一」,OOM率來到了十萬分之一這個數量級。緩存
不講廢話了,也不講那些網上均可以查到的一些常規優化方法來填字數了,我會針對如何去fix OOM這個目標,將思考的歷程以及解決問題的辦法分享出來,但願其中會有某一條經驗正好擊中大家,能起到一些幫助~~bash
開幹!!下面的內容,我會用一級標題的字體~ 顯眼一些哈哈,畢竟前面都是囉嗦的廢話app
首先fix OOM第一件事確定是來排查內存泄漏。想要排查內存泄漏,那就第一步要對內存泄漏進行監控、上報。框架
樣例代碼以下:dom
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();
}
}
}複製代碼
當內存泄漏上報到sentry上面以後,咱們直接觀察是哪裏泄漏的就行了。經過sentry進行監控以後,項目裏面的大部份內存泄漏無處可逃~ ,內存泄漏比較簡單,我就不花大量篇幅去贅述了~,我本身看文章的過程當中,最討厭篇幅太長。。。ide
profiler工具的使用方法我就不贅述了吧,講一下小技巧吧。工具
在排查bitmap對象,咱們能夠用Profiler直接看java 堆中的bitmap對象圖片的預覽~ 這樣能夠直接定位到是哪裏泄漏了以及哪裏bitmap加載過大
方法:找到對應的Bitmap對象,而後~ ,點擊它,而後就能夠preview,以下圖:
複製代碼
咱們能夠知道的是,當一個Activity的生命週期要走完了,那就說明咱們絕大機率不會再使用這個Activity對象了,所以徹底能夠對他的可能致使整個Activity泄露的引用進行清空,將其中的一些資源釋放乾淨,好比有EditText的TextWatcher,這是很是容易泄露且在咱們項目中大量出現的一個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();
}
}
}
}複製代碼
咱們在基類BaseActivity的onDestory()方法中進行了一些資源和引用的清除
在咱們把能fix的內存泄漏都盤了一便以後,上線一週並無發現數據好轉,OOM率仍是高居不下,因而乎,咱們開始懷疑內存峯值過高的問題,在咱們的項目中不只僅只有native的部分模塊,還有混合的H五、RN模塊,當起一個ReactActivity的實例時,內存峯值老是漲的特別特別厲害,同時項目中有消息流的展示,其中會包含着大量的圖片展現,這也是致使內存峯值過高的緣由(Bitmap對象太大以及太多)
咱們又拿出了老夥伴 - Profiler,這但是分析bitmap對象的利器,能夠直接看到大小、圖片的預覽,以及能夠經過 go to instance一層一層的找到究竟是誰在引用它。好比下面這個例子,直接看引用就知道是被Fresco所引用了~ 直接就在CountingMemoryCache中。
其實咱們主要仍是須要去關注Bitmap對象的分配和不合法持有致使的內存峯值問題,若是一個bitmap對象有3M,而後持有一個幾十上百個在內存中,這誰吃得消,低端機器老早直接OOM了。
目前咱們項目中用的圖片加載框架有兩個,UIL、Fresco,UIL我吐槽好久了,這麼多年沒更新,老早就該換了~
1. UIL加載圖片在咱們項目中的問題:
2.Fresco在RN頁面中使用的問題,
經過看代碼能夠知道,RN頁面銷燬的時候,連帶着Fresco的內存緩存都會被清空,
直接上代碼圖:
代碼看到這裏,彷佛Fresco不用擔憂了,既然會清空Fresco的內存緩存,何愁會引發內存峯值太高,若是讀者看到這裏,也有這個想法,那就大錯特錯了。話很少說,直接上圖。
Fresco相關源碼的邏輯這篇文章就不分析了,主要講思路,具體的源碼分析後面我會用單獨的篇幅去講~
爲何我會對Fresco的動圖緩存這麼敏感,那仍是Profiler的功勞,我在用Profiler查看內存中bitmap的分配的時候,發現有上百張的Loading圖沒有銷燬(咱們Loading圖是動圖,大概每幀的Bitmap對象在360K左右), 且打開的頁面越多,Loading的bitmap就會越多。(這是由於咱們每個RN頁面都會帶一個Loading動畫)
0.3M * 100 = 30M,很多了。。。,說實話有點恐怖
因而乎,幹掉他們,這裏用了反射,正常狀況下不須要反射。直接拿ImagePipelineFactory中的對象來clear就好
public static void clearAnimationCache() {
if (frescoAnimationCache == null) {
//採用反射的方法,若是native、rn同時初始化Fresco,會形成Fresco內部存儲動圖的CountingMemoryCache不是Fresco.getImagePipelineFactory().getBitmapCountingMemoryCache()了
//暫時用反射的方法,拿到存儲動圖緩存的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();
}複製代碼
爲了防止峯值太高,咱們還起了一個線程,定時的去監控實時的內存使用狀況,若是內存緊急了,直接清空UIL/Fresco的內存緩存救急
private static Handler lowMemoryMonitorHandler;
private static final int MEMORY_MONITOR_INTERVAL = 1000 * 60;
/**
* 開啓低內存監測,若是低內存了,做出相應的反應
*/
public static void startMonitorLowMemory() {
HandlerThread thread = new HandlerThread("thread_monitor_low_memory");
thread.start();
lowMemoryMonitorHandler = new Handler(thread.getLooper());
lowMemoryMonitorHandler.postDelayed(releaseMemoryCacheRunner, MEMORY_MONITOR_INTERVAL);
}
/**
* 低內存時清空Fresco、UIL的內存緩存
* 若是已用內存達到了總的 80%時,就清空緩存
*/
private static Runnable releaseMemoryCacheRunner = new Runnable() {
@Override
public void run() {
long alreadyUsedSize = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
long maxSize = Runtime.getRuntime().maxMemory();
if (Double.compare(alreadyUsedSize, maxSize * 0.8) == 1) {
BitmapUtil.clearMemoryCaches();
}
lowMemoryMonitorHandler.postDelayed(releaseMemoryCacheRunner, MEMORY_MONITOR_INTERVAL);
}
};複製代碼
我想你們都不會想到,在咱們app的登陸註冊頁,會有一個圖片輪播控件,它輪播着五六張單張6M+的Bitmap。。。固然,特大圖不只限於此,還有其餘地方會有相同狀況,咱們經過Profiler找出那些大的bitmap對象,而後預覽以後肯定是哪裏在用的。
直接優化掉。最不濟 8888 -> 565就少一半內存佔用
怎麼講呢,,OOM這個東西,還沒咋僵持呢,就沒了。。
深夜一時興起想分享和記錄一些什麼,就隨便寫了這一篇博客,寫的不詳細,沒有排版和良好的語言組織,單純的就是想分享
總結一下吧,咱們爲了fix OOM所作的事情:
一些其餘的細節暫時想不起來了,凌晨四點腦子不清醒了
後續關於這裏面涉及到的Fresco的部分源碼分析、Profiler的最佳使用姿式(通過這一次的折騰,總結出來一句話,Profiler真香)、以及前段時間在作的App的啓動速度優化等等等等等都會單獨拎文章去分享,後續也會帶來更多,涉及的內容包括但不限於:
個人簡書 鄒啊濤濤濤的簡書
個人CSDN 鄒啊濤濤濤的CSDN
個人掘金 鄒啊濤濤濤的掘金