Android - 完全消滅OOM的實戰經驗分享(千分之1.5 -> 萬分之0.2)



前言

這是我在掘金的第一篇博客分享,最近在掘金上看了許多大佬的文章,學到了很是多的東西,實在是忍不住想要把咱們平時工做中用到的一些優化方案分享出來,其實也是一個你們一塊兒討論學習的過程,但願你們能夠多多交流 ~ java

自我介紹

第一篇博客,總得介紹下本身~,有校友或者其餘間接捱得着邊的聯繫的能夠私聊交流,前1/4 -> 1/3人生實在沒啥交集的也能夠眼熟一下。祖籍贛,天府磨子橋文理學院七年計算機,18年夏天畢業,目前在北京海淀768工做,「脈脈」平臺客戶端開發一枚。喜歡打遊戲唱歌擼貓次好次的,其餘的沒了程序員

背景

先簡單講講跟oom糾結的歷史吧。web

在18年年末,咱們app進行了一次很是大的版本更迭,由於時間緊急、業務繁忙、人數也沒達到能夠湊人數可讓某些人準點下班的那種數量(各個公司的常規緣由),業務線在對一些模塊進行重構和大量新需求的開發過程當中,許許多多的細節沒有注意到,直接致使了後面一個月的崩潰率、OOM率猛增, 且居高不下。大概快到了千分之2的這個數量級,這是很是很是恐怖的。所以咱們花了一段時間,集中的fix了一把OOM的相關問題,一頓操做,直接讓主版本的崩潰率來到了「萬分之一」,OOM率來到了十萬分之一這個數量級。緩存

幹掉OOM,咱們幹了什麼?

不講廢話了,也不講那些網上均可以查到的一些常規優化方法來填字數了,我會針對如何去fix OOM這個目標,將思考的歷程以及解決問題的辦法分享出來,但願其中會有某一條經驗正好擊中大家,能起到一些幫助~~bash


開幹!!下面的內容,我會用一級標題的字體~ 顯眼一些哈哈,畢竟前面都是囉嗦的廢話app



1、排查內存泄漏

首先fix OOM第一件事確定是來排查內存泄漏。想要排查內存泄漏,那就第一步要對內存泄漏進行監控、上報。框架

咱們採用了LeakCanary,實現了一個自定義的Service繼承自DisplayLeakService,重寫afterDefaultHandling方法,將內存泄漏上報到Sentry。

樣例代碼以下: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

除了LeakCanary,咱們還使用了Android Studio自帶的Profiler工具對內存有進行分析,包括內存泄漏的問題和內存峯值太高的問題。

profiler工具的使用方法我就不贅述了吧,講一下小技巧吧。工具

在排查bitmap對象,咱們能夠用Profiler直接看java 堆中的bitmap對象圖片的預覽~ 這樣能夠直接定位到是哪裏泄漏了以及哪裏bitmap加載過大

方法:找到對應的Bitmap對象,而後~ ,點擊它,而後就能夠preview,以下圖:

複製代碼


2、兜底策略

咱們能夠知道的是,當一個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()方法中進行了一些資源和引用的清除

3、內存峯值過高

在咱們把能fix的內存泄漏都盤了一便以後,上線一週並無發現數據好轉,OOM率仍是高居不下,因而乎,咱們開始懷疑內存峯值過高的問題,在咱們的項目中不只僅只有native的部分模塊,還有混合的H五、RN模塊,當起一個ReactActivity的實例時,內存峯值老是漲的特別特別厲害,同時項目中有消息流的展示,其中會包含着大量的圖片展現,這也是致使內存峯值過高的緣由(Bitmap對象太大以及太多)

咱們又拿出了老夥伴 - Profiler,這但是分析bitmap對象的利器,能夠直接看到大小、圖片的預覽,以及能夠經過 go to instance一層一層的找到究竟是誰在引用它。好比下面這個例子,直接看引用就知道是被Fresco所引用了~ 直接就在CountingMemoryCache中。




其實咱們主要仍是須要去關注Bitmap對象的分配和不合法持有致使的內存峯值問題,若是一個bitmap對象有3M,而後持有一個幾十上百個在內存中,這誰吃得消,低端機器老早直接OOM了。

查Bitmap分配查出來的問題

目前咱們項目中用的圖片加載框架有兩個,UIL、Fresco,UIL我吐槽好久了,這麼多年沒更新,老早就該換了~ 

1. UIL加載圖片在咱們項目中的問題:

  • 沒有傳入合適的Config,絕大多數地方傳的都是ARGB_8888,其實根本不必,改爲565直接少一半內存佔用
  • 用UIL進行loadImage時,沒有傳入targetSize,這就直接致使了UIL內部是以屏幕的尺寸去Decode的Bitmap對象,想象一下,一個特別小的頭像View,持有着一個屏幕大小尺寸的Bitmap對象,這誰頂得住。
  • 許多地方不須要存內存緩存,好比閃屏廣告圖,app啓動以後就不會再使用了,能夠加載的時候 memoryCache(false)
  • 許多地方不須要磁盤緩存,好比發佈動態,從圖庫中選圖,不須要再存一份磁盤緩存了,自己那些圖片都是本地圖片。直接 diskCache(false)


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);
        }
    };複製代碼

5、特大圖排查優化

我想你們都不會想到,在咱們app的登陸註冊頁,會有一個圖片輪播控件,它輪播着五六張單張6M+的Bitmap。。。固然,特大圖不只限於此,還有其餘地方會有相同狀況,咱們經過Profiler找出那些大的bitmap對象,而後預覽以後肯定是哪裏在用的。

直接優化掉。最不濟 8888 -> 565就少一半內存佔用


怎麼講呢,,OOM這個東西,還沒咋僵持呢,就沒了。


6、總結

深夜一時興起想分享和記錄一些什麼,就隨便寫了這一篇博客,寫的不詳細,沒有排版和良好的語言組織,單純的就是想分享

總結一下吧,咱們爲了fix OOM所作的事情:

  1. 檢查內存泄漏,包括常見的Context泄漏、單例泄漏、EditText的TextWatcher泄漏等等,找到並fix他們,最簡單的例子,能傳application的地方就不要硬傳個activity過去
  2. 兜底方案:
    • 在Activity onDestory的時候,遍歷View樹,清空backGround、Drawable、EditText的TextWatcher等
  3. 內存峯值的優化。內存泄漏會致使內存峯值,內存峯值是OOM的大鍋,舉個例子當可用內存不夠分配一個Bitmap對象時,就會OOM,Android上大多數的內存峯值都是圖片的加載帶來的。如今許多的app中都有信息流的展示,可能會有許多的九宮格展現圖片,且Bitmap對象自己就能夠很是大。
    • 優化UIL的使用
      • memoryCache選用,不是全部的圖片加載都須要UIL去塞一分內存緩存的,好比閃屏圖
      • ImageLoader.getInstance().displayImage()的時候,傳進去的Option不要無腦ARGB_8888,講道理來講,無腦RGB_565都是沒啥問題的。。
      • 調用displayImage的時候,最好傳一個ImageSize做爲targetSize,這個size能夠是你的ImageView的尺寸,當View尺寸自己不肯定的時候,能夠傳一個大概值,好比咱們app中有好些個的頭像標準尺寸,爲了偷懶,直接傳MaxAvatarSize就ok
    • Fresco的優化
      • RN中使用Fresco加載圖片,在RN Activity銷燬的時候,會將Fresco默認的memory cache清空,可是動圖的緩存沒有清。手動清一下。咱們項目中每一個RN頁面都會帶一個Loading動圖,因此吃了大虧。。
  4. 持續的後臺監控內存,起一個HandlerThread,一直在後臺拿內存使用的狀態,達到了危險警惕線就清空一把UIL、Fresco的memory cache,先讓世界安靜一下
  5. 須要對內存泄漏、OOM、Crash、ANR進行監控

一些其餘的細節暫時想不起來了,凌晨四點腦子不清醒了

後續關於這裏面涉及到的Fresco的部分源碼分析、Profiler的最佳使用姿式(通過這一次的折騰,總結出來一句話,Profiler真香)、以及前段時間在作的App的啓動速度優化等等等等等都會單獨拎文章去分享,後續也會帶來更多,涉及的內容包括但不限於:

  • 主流框架的一些設計思想的分享
  • 工做項目中遇到的麻煩和坑
  • 工做中蹚坑的一些經驗
  • 好代碼
  • 壞代碼
  • 壞的設計
  • 程序員從頭髮濃密到成爲下雨天報警員的心路歷程
  • 。。。








個人簡書 鄒啊濤濤濤的簡書

個人CSDN 鄒啊濤濤濤的CSDN

個人掘金 鄒啊濤濤濤的掘金

相關文章
相關標籤/搜索