性能優(yōu)化(2.5)-MAT內(nèi)存泄漏分析(一)

主目錄見:Android高級進(jìn)階知識(這是總目錄索引)
[written by 無心追求]

Activity內(nèi)部類泄漏

  • Activity如果存在內(nèi)部類,無論是匿名內(nèi)部類河泳,或者是聲明的內(nèi)部類沃呢,都有可能造成Activity內(nèi)存泄漏年栓,因為內(nèi)部類默認(rèn)是直接持有這個activity的引用拆挥,如果內(nèi)部類的生命周期比activity的生命周期要長,那么在activity銷毀的時候內(nèi)部類仍然存在并且持有activity的引用某抓,那么activity自然無法被gc纸兔,造成內(nèi)存泄漏

Activity內(nèi)部Handler

class MyHandler extends Handler {
        
        MyHandler() {
            
        }

        @Override
        public void handleMessage(Message msg) {
            // to do your job
        }
    }
MyHandler myHandler = new MyHandler();

如上,在Activity內(nèi)部如果聲明一個這樣的Handler否副,那么myHandler就默認(rèn)持有Activity引用汉矿,假設(shè)Activity退出了,但是可能這時候才有myHandler的任務(wù)post备禀,那么Activity是無法被回收的洲拇,可以采用以下方式解決:

static class MyHandler extends Handler {
        WeakReference<Activity> mActivityReference;

        MyHandler(Activity activity) {
            mActivityReference = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            final Activity activity = mActivityReference.get();
            if (activity != null) {
                if (msg.what == 1 && isJumpToHomePage) {
                    Intent intent = new Intent(activity, HomePageActivity.class);
//                    intent.putExtra("themeType", themeType);
//                    LogUtil.d("themeType == " + themeType);
                    activity.startActivity(intent);
                    activity.finish();
                }
            }
        }
    }

這里面是把MyHandler是一個內(nèi)部靜態(tài)類,靜態(tài)類在java虛擬機加載的時候就是獨立加載到內(nèi)存中的曲尸,不會依賴于任何其他類赋续,而且這里面是把activity以弱引用的方式傳到MyHandler中,即便是靜態(tài)MyHandler類對象一直存在另患,但是由于它持有的是activity弱引用纽乱,在gc回收的時候activity對象是可以被回收的,另外注意一點昆箕,對于Handler的使用如果有sendEmptyMessageDelayed()來延遲任務(wù)執(zhí)行的話最好在Activity的onDestroy里面把Handler的任務(wù)都移除(removeCallbacks(null))鸦列,activity在退出后,就是應(yīng)該在onDestroy方法里面把一些任務(wù)取消掉鹏倘,做一些清理的操作

Activity內(nèi)部線程

  • 在Activity里面有時候為了實現(xiàn)異步操作會單獨開一個線程來執(zhí)行任務(wù)薯嗤,或者是異步的網(wǎng)絡(luò)請求也是單獨開線程來執(zhí)行的,那么就會存在一個問題纤泵,如果內(nèi)部線程的生命周期比Activity的生命周期要長骆姐,那么內(nèi)部線程任然默認(rèn)持有Activity的引用,導(dǎo)致Activity對象無法被回收夕吻,但是當(dāng)這個線程執(zhí)行完了之后诲锹,Activity對象就能被成功的回收了,這會造成一個崩潰風(fēng)險涉馅,可能在線程里面有調(diào)用到一些Activity的內(nèi)部對象归园,但是在Activity退出后這些對象有可能有些已經(jīng)被回收了,就變成null了稚矿,這時候要是不進(jìn)行null的判斷就會報空指針異常庸诱,如果這個線程是一直跑的捻浦,那就會造成Activity對象一直不會被回收了,因此桥爽,在activity退出后一定要做相關(guān)的清理操作朱灿,中斷線程,取消網(wǎng)絡(luò)請求等等

Activity內(nèi)部類回調(diào)監(jiān)聽

  • 在編碼中常常會定義各種接口回調(diào)钠四,類似有點擊時間監(jiān)聽OnClickListener盗扒,這些回調(diào)監(jiān)聽有時候就定義在Activity內(nèi)部,或者直接用Activity對象去實現(xiàn)這個接口缀去,到時候設(shè)置監(jiān)聽的時候直接調(diào)用setListener(innerListener)或者setListener(this)侣灶,innerListener是Activity內(nèi)部定義的,this就是Activity對象缕碎,那么問題來了褥影,回調(diào)監(jiān)聽并不一定馬上返回,只有在觸發(fā)條件滿足的時候才會回調(diào)咏雌,這個時間是無法確定的凡怎,因此在Activity退出的時候應(yīng)該顯示的把回調(diào)監(jiān)聽都移除掉setListener(null),既釋放了回調(diào)監(jiān)聽對象占用的內(nèi)存赊抖,也避免回調(diào)監(jiān)聽繼續(xù)持有activity引用统倒;對與內(nèi)部類還有一種解決方式,和內(nèi)部Handler相似熏迹,定義成static內(nèi)部類檐薯,然后把Activity對象的弱引用傳遞進(jìn)去,這樣也就萬無一失注暗,舉個項目中遇到的實際場景:
private static class RecorderTimeListener implements TimeCallback {

        WeakReference<ChatActivity> target;

        RecorderTimeListener(ChatActivity activity) {
            target = new WeakReference<>(activity);
        }

        @Override
        public void onCountDown(final int time) {
            if (target == null || target.get() == null) {
                return;
            }
            final ChatActivity activity = target.get();
            activity.runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    activity.volumeView.setResetTime(time);
                }
            });
        }

        @Override
        public void onMaxTime() {
            if (target == null || target.get() == null) {
                return;
            }
            final ChatActivity activity = target.get();
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    activity.isMaxTime = true;
                    activity.stopRecord();
                }
            });
        }
    }

private class StartRecorderListener implements StartCallback {


        @Override
        public boolean onWait() {
            cancelRecord();
            return true;
        }

        @Override
        public void onStarted() {
            if (playerManager.isPlaying()) {
                playerManager.stop();
            }
            recordWaveView.setVisibility(View.VISIBLE);
            animation = (AnimationDrawable) recordWaveView.getBackground();
            animation.start();

            volumeView.showMoveCancelView();
            volumeDialog.show();

            viewHandler.postDelayed(volumeRunnable, 100);
        }

        @Override
        public void onFailed(int errorCode) {
            if (errorCode == RecorderManager.ERROR_START_FAIL) {
                showHintDialog(R.string.chat_permission_dialog_title, R.string.chat_permission_dialog_message);
            }
        }
    }

private void startRecord() {
        SystemDateUtil.init(this);
        LogUtil.i(ChatKey.TAG_INFO, "--------------------------錄音開始--------------------------");
        final long startSendTime = SystemDateUtil.getCurrentDate().getTime();
        sliceSender = dialogMsgService.createSliceSender(
                AccountUtil.getCurrentFamilyChatDialogId(),
                AccountUtil.getCurrentImAccountId(), new DialogMsgService.OnSendVoiceMsgListener() {
                    @Override
                    public void onSuccess() {
                        LogUtil.d(TAG, "錄音上傳成功");
                        sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_SUCCESS);
                    }

                    @Override
                    public void onFailure() {
                        sendBigData(sliceSender.getGroupId(), ChatMsgBeh.MsgType.EMOJI,
                                SystemDateUtil.getCurrentDate().getTime() - startSendTime, SendMsgEvent.CODE_SEND_FAILURE);
                        LogUtil.d(TAG, "錄音上傳失敗");
                    }
                });
        RecorderManager.getInstance(this).startRecorder(sliceSender, new StartRecorderListener(), new RecorderTimeListener(this));
        LogUtil.i(ChatKey.TAG_INFO, "groupId:" + sliceSender.getGroupId());
    }

如上StartRecorderListener是內(nèi)部類坛缕,RecorderTimeListener是靜態(tài)內(nèi)部類并傳入Activity弱引用,如果把StartRecorderListener的實現(xiàn)改成RecorderTimeListener的實現(xiàn)捆昏,那么Activity內(nèi)存泄漏就不存在了

動畫導(dǎo)致內(nèi)存泄漏

  • 進(jìn)入Activity界面后如果有一些和控件綁定在一起的屬性動畫在運行赚楚,退出的時候要記得cancel掉這些動畫
自定義控件ImageButton中:
public void start(float startAngle, float endAngle) {
        setStop(false);

        final AnimatorSet as = new AnimatorSet();
        final ObjectAnimator oa = ObjectAnimator.ofFloat(this, "progress",
                startAngle, endAngle);
        oa.setDuration(duration);
        oa.setInterpolator(new DecelerateInterpolator(1.1f));
        oa.setRepeatCount(count);
//      oa.setRepeatMode(ObjectAnimator.INFINITE);
        oa.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                if (stop && as.isRunning()) {
                    as.cancel();
//                    oa.removeAllListeners();
                } else {
                    float p = (float) animator.getAnimatedValue();
                    setProgress(p);
                }
            }
        });
        as.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
            }
        });
        as.play(oa);
        as.start();
    }
    
    public void cancel() {
        setStop(true);
    }

    public void setStop(boolean stop) {
        this.stop = stop;
        if (stop) {
            setProgress(0.0f);
        }
    }

如上如果不cancel掉屬性動畫就會一直運行并且一直去執(zhí)行控件的onDraw方法,那么ImageButton持有了Activity對象骗卜,而屬性動畫ObjectAnimator持有了ImageButton宠页,ObjectAnimator一直在運行,那么Activity對象也就不能被釋放了

  • 屬性動畫的對象盡量不要用static修飾寇仓,static修飾和举户,這個對象一旦被創(chuàng)建那么就一直存在了,屬性動畫一旦start之后遍烦,那么就一直運行俭嘁,這時候就算退出activity的時候cancel掉動畫也仍然會持有activity引用,就像下面這個例子:
private static ValueAnimator valueAnimator;

private void startValueAnimator() {
        int displayTime2Show = displayTime - 1;
        if (displayTime2Show > 1) {
            valueAnimator = ValueAnimator.ofInt(displayTime2Show, 1);
            valueAnimator.setDuration(displayTime2Show * 1000);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    tvStartPageTime.setText(animation.getAnimatedValue().toString());
                }
            });
            valueAnimator.start();
        }

    }
protected void onPause() {
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        super.onPause();
    }

即便是在activity退出后cancel掉動畫服猪,activity依然無法被釋放供填,為什么拐云?因為valueAnimator是靜態(tài)的,而且添加了動畫屬性改變的監(jiān)聽addUpdateListener近她,在監(jiān)聽回調(diào)里面有tvStartPageTime(TextView)控件叉瘩,默認(rèn)持有Activity對象,因此即便Activity退出粘捎,動畫cancel掉也無法釋放持有的引用薇缅,修改方法有兩種,一種是把valueAnimator的static修飾去掉晌端,另一中國是:

protected void onPause() {
valueAnimator.removeAllUpdateListeners();
        if (valueAnimator != null && valueAnimator.isRunning()) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        super.onPause();
    }

加一句監(jiān)聽器的移除代碼removeAllUpdateListeners()

傳Context參數(shù)的時候使用Activity對象造成內(nèi)存泄漏

  • 在android中常常會用到Context環(huán)境變量捅暴,Activity繼承了Context,所以在傳入Context的時候常常直接在Activity中傳入this即Activity本對象咧纠,這是比較不好的習(xí)慣,在沒有規(guī)定一定要傳Activity對象的時候盡量采用全局的Context對象泻骤,即ApplicationContext來作為參數(shù)傳遞進(jìn)去漆羔,因為ApplicationContext只要app在運行那么它就一直存在,因此即便有一個對象長期引用它狱掂,生命周期也不會比ApplicationContext長演痒,所以不會造成ApplicationContext的內(nèi)存泄漏,因為ApplicationContext只要App在運行就不允許被回收
  • 在Android程序中要慎用單例趋惨,如果單例需要傳Context對象鸟顺,那么就需要謹(jǐn)慎了因為在單例中如果把Context保存起來,那么這個單例一旦被創(chuàng)建器虾,就一直存在了讯嫂,如果傳入的是Activity對象,那將一直持有Activity對象引用導(dǎo)致內(nèi)存泄漏兆沙,解決版本是傳入ApplicationContext對象欧芽,或者在Activity退出的時候銷毀這個單例對象,單例在什么時候時候使用葛圃,如果一個對象并不會被頻繁的調(diào)用千扔,那就沒必要用單例,對于可能會被頻繁調(diào)用的對象方法可以采用單例库正,這樣做可以避免反復(fù)創(chuàng)建對象和gc對象造成的內(nèi)存抖動曲楚;對于需要保存的全局變量也可以用單例封裝起來;單例只要創(chuàng)建了就一直有存在引用褥符,所以是不會被gc的
  • 使用靜態(tài)變量來保存Activity對象龙誊,這是一個非常不好的編碼習(xí)慣,static修飾的代碼片段属瓣,變量或者類是在app加載的時候就已經(jīng)加載到內(nèi)存中了载迄,所以和單例有點相似讯柔,static變量也會一直持有Activity對象直到APP被殺死或者顯示的把static變量置空

在Android5.0以上的WebView泄漏

  • 如果Activity引用了WebView控件來加載一個網(wǎng)頁或者加載一個本地的網(wǎng)頁,在退出activity之后即便你調(diào)用了webView.destroy()方法护昧,也無法釋放webview對于activity持有的引用魂迄,原因和解決方案可參考Android5.1的WebView內(nèi)存泄漏,如這篇文章所分析的解決方案確實有效惋耙,親測可用捣炬!

子線程中不當(dāng)?shù)氖褂肔ooper.prepare()和Looper.loop()方法造成內(nèi)存泄漏

  • Looper.loop()是一個無限循環(huán)的方法,它是反復(fù)的去MessageQueue里面去取出Message并分發(fā)給對應(yīng)的Handler去執(zhí)行绽榛,如果在子線程中調(diào)用了Looper.prepare()和Looper.loop()方法湿酸,Looper.loop()會導(dǎo)致這個線程一直不死,一直堵在這里灭美,因此線程就無法結(jié)束運行推溃,在Looper.prepare()和Looper.loop()之間的所有對象都沒辦法被釋放,解決方案就是在不用的時候及時的把Looper給quit掉

EditText使用setTransformationMethod導(dǎo)致的內(nèi)存泄漏

  • 這個問題只有在4.0的android系統(tǒng)上才會存在届腐,在5.0以上的系統(tǒng)已經(jīng)不存在了铁坎,應(yīng)該是屬于Android的一個缺陷
    [圖片上傳失敗...(image-2fd60f-1511154075108)]
    問題的根源應(yīng)該就是這:
loginPasswdEt.setTransformationMethod(PasswordTransformationMethod.getInstance());
loginPasswdEt.setTransformationMethod(HideReturnsTransformationMethod.getInstance());

而PasswordTransformationMethod和HideReturnsTransformationMethod分別都是一個單例:

private static PasswordTransformationMethod sInstance;

private static HideReturnsTransformationMethod sInstance;
PasswordTransformationMethod

public CharSequence getTransformation(CharSequence source, View view) {
        if (source instanceof Spannable) {
            Spannable sp = (Spannable) source;

            /*
             * Remove any references to other views that may still be
             * attached.  This will happen when you flip the screen
             * while a password field is showing; there will still
             * be references to the old EditText in the text.
             */
            ViewReference[] vr = sp.getSpans(0, sp.length(),
                                             ViewReference.class);
            for (int i = 0; i < vr.length; i++) {
                sp.removeSpan(vr[i]);
            }

            removeVisibleSpans(sp);

            sp.setSpan(new ViewReference(view), 0, 0,
                       Spannable.SPAN_POINT_POINT);
        }

        return new PasswordCharSequence(source);
    }
    
private static class ViewReference extends WeakReference<View>
            implements NoCopySpan {
        public ViewReference(View v) {
            super(v);
        }
    }

上面是5.0系統(tǒng)的源碼,里面已經(jīng)用ViewReference來包裝view設(shè)置到Spannable中了犁苏,所以是把view的弱引用傳進(jìn)去了硬萍,因此可以被gc回收,而在4.0android系統(tǒng)上围详,很可能就不是這么做的朴乖,所以4.0系統(tǒng)上面就是View對象被PasswordTransformationMethod和HideReturnsTransformationMethod單例長期持有,而View又持有Activity對象助赞,所以針對4.0系統(tǒng)我們只需要釋放這兩個單例對象即可:

private void releaseMemoryLeak() {
        int sdk = Build.VERSION.SDK_INT;
        if (sdk >= Build.VERSION_CODES.LOLLIPOP) {
            return;
        }
        try {
            Field field1 = PasswordTransformationMethod.class.getDeclaredField("sInstance");
            if (field1 != null) {
                field1.setAccessible(true);
                field1.set(PasswordTransformationMethod.class, null);
            }
            Field field2 = HideReturnsTransformationMethod.class.getDeclaredField("sInstance");
            if (field2 != null) {
                field2.setAccessible(true);
                field2.set(HideReturnsTransformationMethod.class, null);
            }
        } catch (NoSuchFieldException e) {
            SyncLogUtil.e(e);
        } catch (IllegalAccessException e) {
            SyncLogUtil.e(e);
        }
    }

加上上述代碼后驗證發(fā)現(xiàn)內(nèi)存不再泄漏买羞,搞定。

控件的BackGround導(dǎo)致的內(nèi)存泄漏(4.0android系統(tǒng)已經(jīng)解決)

  • 有時候為了避免圖片反復(fù)的加載嫉拐,就把第一次加載后的Bitmap或者Drawable用靜態(tài)變量保存起來哩都,但是要是把這種靜態(tài)修飾的圖片對象設(shè)置成控件的背景,那就呵呵了
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
  super.onCreate(state);

  TextView label = new TextView(this);
  label.setText("Leaks are bad");

  if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);

  setContentView(label);
}

因為在View的setBackgroundDrawable方法里面有一句:

public void setBackgroundDrawable(Drawable background) {
......省略很多代碼
background.setCallback(this);
mBackground = background;
}

Drawable對象把View對象作為回調(diào)保存起來了婉徘,不過在4.0系統(tǒng)以后引入回調(diào)來保存View對象了漠嵌,所以已經(jīng)不會造成內(nèi)存泄漏問題了:

public final void setCallback(Callback cb) {
        mCallback = new WeakReference<Callback>(cb);
    }

這里依然要舉例子出來是想說明不恰當(dāng)?shù)氖褂胹tatic來修飾變量很有可能導(dǎo)致對象無法被回收[written by 無心追求]

使用android shell命令查看內(nèi)存使用情況

使用adb shell dumpsys meminfo pkgname或者直接使用AndroidStudio里面的memory usage功能然后就會出現(xiàn)如下信息:

Applications Memory Usage (kB):
Uptime: 14237237 Realtime: 23790474

** MEMINFO in pid 8071 [com.xtc.watch] **
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap        0        0        0        0    21924     8558     6405
  Dalvik Heap   122472   122372        0    15672   143308    65400    77908
 Dalvik Other    10361    10076      164      224
        Stack      440      440        0        8
    Other dev        4        0        4        0
     .so mmap     6441     3452     2636     2048
    .apk mmap      611        0      340        0
    .ttf mmap      538        0      504        0
    .dex mmap     8407     1640     2940       40
   Other mmap       80        4        0        0
      Unknown    10940    10936        0      148
        TOTAL   160294   148920     6588    18140   165232    73958    84313

 Objects
               Views:      288         ViewRootImpl:        2
         AppContexts:       11           Activities:        2
              Assets:        5        AssetManagers:        5
       Local Binders:       30        Proxy Binders:       38
    Death Recipients:        3
     OpenSSL Sockets:        1

 SQL
         MEMORY_USED:      138
  PAGECACHE_OVERFLOW:       24          MALLOC_SIZE:       62

 DATABASES
      pgsz     dbsz   Lookaside(b)          cache  Dbname
         4       20            306       25/47/12  /data/data/com.xtc.watch/databases/upload.db
  • Native Heap是native層的內(nèi)存堆棧,Dalvik Heap是java層的內(nèi)存堆棧盖呼,如果這二者加起來的內(nèi)存占用超過了應(yīng)用最大內(nèi)存限制就會報OOM異常儒鹿,剩下的.so mmap是 C 庫代碼占用的內(nèi)存,.jar mmap是Java 文件代碼占用的內(nèi)存 几晤,.apk mmap是apk代碼占用的內(nèi)存约炎,.dex mmap是Dex 文件代碼占用的內(nèi)存
  • Objects中的Activities表示當(dāng)前內(nèi)存中的activity對象的個數(shù),啟動一個activity就會生成一個activity對象,當(dāng)退出activity的時候圾浅,activity對象就會被釋放掠手,所以反復(fù)的進(jìn)出一個activity界面然后查看Activities的個數(shù)有沒有保持不變,如果增加了狸捕,那么就說明這個activity對象沒有被釋放喷鸽,也就是說可能存在內(nèi)存泄漏,但是具體哪里泄漏了并不知道

DDMS查看內(nèi)存使用情況

eclipse中有一個ddms工具灸拍,可以查看線程信息(Threads)做祝,內(nèi)存使用情況(VM Heap),內(nèi)存分配跟蹤(Allocation Tracker)鸡岗,CUP使用情況(Sysinfo CUP load)混槐,內(nèi)存使用餅狀圖(Sysinfo Memory usage),這里我們暫時用到VM Heap轩性,選擇要查看的app進(jìn)程声登,點擊左上角的show heap updates,選擇VM Heap并點擊Cause GC按鈕炮姨,然后就出現(xiàn)下圖:
[圖片上傳失敗...(image-ad6bc7-1511154091691)]
觀察data object的Total Size選項捌刮,這個是app的創(chuàng)建的java對象做占用的內(nèi)存大小,Count是總內(nèi)存的對象的個數(shù)舒岸,反復(fù)的進(jìn)出一個activity,看data object的Total Size有沒有明顯的增加芦圾,正常情況下進(jìn)入一個activity的時候會明顯增加蛾派,退出一個activity會有明顯的回落,總體是維持在一個比較穩(wěn)定的水平如果反復(fù)進(jìn)出activity个少,Total Size不斷上升洪乍,那么可能就存在內(nèi)存泄漏了,需要具體排查

MAT分析內(nèi)存泄漏夜焦,用AndroidStudio的Monitors的Memory

  • 多點擊幾下Initiate GC來回收一下可被釋放的java對象壳澳,因為java的GC是定期有條件執(zhí)行的,當(dāng)內(nèi)存中只存在很少的無用對象茫经,這時候可能并不會觸發(fā)GC巷波,所以手動觸發(fā)GC來保證開始檢測內(nèi)存的時候內(nèi)存都是最干凈的
  • 點擊Dump Java Heap,然后過一會兒就會出現(xiàn)一份數(shù)據(jù)分析文件卸伞,這時候的這份數(shù)據(jù)文件是剛開始的程序?qū)ο髢?nèi)存占用情況抹镊,接下去就針對一個activity反復(fù)操作進(jìn)出等等各種反復(fù)操作,覺得差不多了荤傲,這時候就再次瘋狂點擊Initiate GC回收一下可是放的對象垮耳,點擊Dump Java Heap,這時候生成的數(shù)據(jù)分析文件就是經(jīng)過你瘋狂操作后的內(nèi)存占用情況了
    [圖片上傳失敗...(image-3a960f-1511154091691)]
    生成的上述兩個文件右鍵,點擊Export to Standar .hprof導(dǎo)出到一個自己指定的目錄文件夾
  • 去官網(wǎng)上面下載MAT來打開這兩個文件開始內(nèi)存分析
    [圖片上傳失敗...(image-495cd9-1511154091691)]
    上圖中Problem Suspect部分是代表可能存在內(nèi)存泄漏的地方终佛,Remainder表示正常的部分俊嗽,再繼續(xù)往下看
    [圖片上傳失敗...(image-32efc5-1511154091691)]
    [圖片上傳失敗...(image-13261-1511154091691)]
    上面就是對可能存在內(nèi)存泄漏部分的代碼的一個詳細(xì)的信息,可以看到有些byte數(shù)組占用了大量的內(nèi)存铃彰,keywords也是byte[]绍豁,第二張圖的DexCache可能占用的較多的內(nèi)存,再點擊[圖片上傳失敗...(image-36bdaf-1511154091691)]紅色部分就會出現(xiàn)一些對象的信息列表:
    [圖片上傳失敗...(image-765e0-1511154091691)]
    可以輸入正則表達(dá)式來篩選你想要的類豌研,包名下所有的類妹田,Objects是對象的個數(shù),Shallow Heap是當(dāng)前對象所占用的內(nèi)存大小鹃共,不包括對象內(nèi)包含的對象的大小鬼佣,Retained Heap表示當(dāng)前對象包括對象內(nèi)的子對象一共占用的內(nèi)存大小,所以Retained Heap會比Shallow Heap大得多霜浴,對于一些我們已知的對象在內(nèi)存不泄露的情況下晶衷,該對象的個數(shù)是確定的,所以可以通過分析Objects的個數(shù)來確定對象是否存在內(nèi)存泄漏阴孟,例如同一個Activity對象在反復(fù)進(jìn)出該Activity5次之后Objects的值為5晌纫,那就有問題了,說明同一個Activity創(chuàng)建了5個對象永丝,正常情況下應(yīng)該是退出Activity后锹漱,對象會被回收的,所以O(shè)bjects的值應(yīng)該是0才對慕嚷,而有些對象的Objects不為0并不代表一定存在內(nèi)存泄漏哥牍,例如ConnectionService是一個常駐的Service,那么它是不會被GC的喝检,而ConnectionService里面的對象可不會被回收嗅辣,所以這些對象的Objects值不為0其實就是正常的了,至于Shallow Heap和Retained Heap挠说,我覺得可以用來分析一些對象的內(nèi)存占用澡谭,Shallow Heap一般情況下不會很大,當(dāng)你發(fā)現(xiàn)Retained Heap非常大的時候损俭,那就說明該對象里面的對象可能占用了大量的內(nèi)存蛙奖,可能存在問題;在用Objects找到可能存在內(nèi)存泄漏的對象后撩炊,右鍵List Objects外永,然后有兩個選項:with outgoing references(表示的是當(dāng)前對象,引用了外部引用)和with incoming references(表示的是當(dāng)前查看的對象拧咳,被外部引用)伯顶,一般當(dāng)前對象泄漏了就是對象還被外部對象持有引用,無法被釋放缤剧,所以我們選擇查看with incoming references[圖片上傳失敗...(image-ef298b-1511154091691)]慨仿,點擊with incoming references后
    [圖片上傳失敗...(image-7f488-1511154091691)]
    可以看到TaskExecutor對象被外部的兩個對象所引用到了,并且可以看到引用的路徑矿瘦,右鍵TaskExecutor掐暮,選擇Path to GC Roots或者或者M(jìn)erge Shortest Paths to GC Roots選項蝎抽,再選擇exclude all phantom/weak/soft .ect references排除所有的弱引用,軟引用對象尋找看有沒有存在GC Roots路克,如果沒有那就說明這個對象不存在內(nèi)存泄漏樟结,最終是會被GC回收,如果存在GC Roots
    [圖片上傳失敗...(image-70d856-1511154091691)]
    在繼續(xù)查看GC Roots的對象是什么精算,其實就是被哪一個對象所引用了瓢宦,上圖可以查看到被外部的sendTaskExecutor對象所引用了而sendTaskExecutor是在ConnectionService這個常駐Service中的,所以理論上是不應(yīng)該被回收的灰羽,所以這里不算是內(nèi)存泄漏驮履,假如sendTaskExecutor是一個Activity里面的字段,而此時Activity已經(jīng)退出了廉嚼,那么這時候就屬于內(nèi)存泄漏了玫镐,因為Activity退出后,Activity資源包括里面的對象應(yīng)該是被回收掉的怠噪,那就找到對應(yīng)的代碼去具體分析可能造成內(nèi)存泄漏的問題所在了恐似,這里明確一點,存在GC Roots的不一定就一定存在內(nèi)存泄漏傍念,GC是不會回收GC Root或者被GC Root所引用的對象的蹂喻,java對象內(nèi)存泄漏其實就是對象在不用的時候仍然被其他對象持有引用導(dǎo)致GC無法回收
  • 比較反復(fù)操作前的內(nèi)存信息和反復(fù)操作后的內(nèi)存信息分析前后有哪些不同[圖片上傳失敗...(image-2670ab-1511154091691)],點擊紅色部分后會出現(xiàn)下圖
    [圖片上傳失敗...(image-54a276-1511154091691)]
    可以看到對比后的Objects和Shallow Heap信息捂寿,看Objects表示前后兩種內(nèi)存信息對于同一個對象是否有個數(shù)上的增加,因為如果對象能被正撤踉耍回收秦陋,那么開始操作前是0,操作后經(jīng)過GC應(yīng)該也是0治笨,如果個數(shù)增加了驳概,那就表示這個對象可能存在內(nèi)存泄漏了,拖動到底部可看到總的比較情況
    [圖片上傳失敗...(image-2af6e7-1511154091691)]
    上圖可知旷赖,操作前的app的Objects總數(shù)比操作后的Objects少了1個顺又,同時可以看到是HttpDns這個對象的問題,于是再切換到操作后的histogram去按照上述步驟查找GC Roots等孵,再具體分析內(nèi)存泄漏的問題
  • MAT當(dāng)中還有一個dominator tree視圖稚照,具體就不復(fù)述了,可以參考這篇大神文章MAT內(nèi)存分析

網(wǎng)上的開源項目LeakCanary也是分析內(nèi)存泄漏的一種有效方法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市果录,隨后出現(xiàn)的幾起案子上枕,更是在濱河造成了極大的恐慌,老刑警劉巖弱恒,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辨萍,死亡現(xiàn)場離奇詭異,居然都是意外死亡返弹,警方通過查閱死者的電腦和手機锈玉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來义起,“玉大人拉背,你說我怎么就攤上這事〔⑸龋” “怎么了去团?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長穷蛹。 經(jīng)常有香客問我土陪,道長,這世上最難降的妖魔是什么肴熏? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任鬼雀,我火速辦了婚禮,結(jié)果婚禮上蛙吏,老公的妹妹穿的比我還像新娘源哩。我一直安慰自己,他們只是感情好鸦做,可當(dāng)我...
    茶點故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布励烦。 她就那樣靜靜地躺著,像睡著了一般泼诱。 火紅的嫁衣襯著肌膚如雪坛掠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天治筒,我揣著相機與錄音屉栓,去河邊找鬼。 笑死耸袜,一個胖子當(dāng)著我的面吹牛友多,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播堤框,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼域滥,長吁一口氣:“原來是場噩夢啊……” “哼纵柿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起骗绕,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤藐窄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后酬土,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荆忍,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年撤缴,在試婚紗的時候發(fā)現(xiàn)自己被綠了刹枉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡屈呕,死狀恐怖微宝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情虎眨,我是刑警寧澤蟋软,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站嗽桩,受9級特大地震影響岳守,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碌冶,卻給世界環(huán)境...
    茶點故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一湿痢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扑庞,春花似錦譬重、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至栅隐,卻和暖如春以现,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背约啊。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留佣赖,地道東北人恰矩。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像憎蛤,于是被迫代替她去往敵國和親外傅。 傳聞我的和親對象是個殘疾皇子纪吮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容