內(nèi)存泄漏與優(yōu)化分析指南

前言

在android開發(fā)中喉恋,我們都或多或少的會遇到一些內(nèi)存泄漏的問題,雖然大都知道哪些情況會導(dǎo)致內(nèi)存泄露母廷,但是還是不可避免的會遇到類似的問題轻黑,因此,知道如何去查找內(nèi)存泄露就顯得非常重要了琴昆。本篇和大家分享下如何進(jìn)行內(nèi)存泄漏的定位分析氓鄙,以及對內(nèi)存占用的優(yōu)化分析。相信大家看了之后會有所收獲业舍。

為了有一個良好的分析體驗(yàn)抖拦,我特意新建了一個用于分析內(nèi)存方面的項目升酣,該項目是一個簡易的新聞客戶端,結(jié)構(gòu)上大致是這樣的态罪,mvp開發(fā)模式噩茄,網(wǎng)絡(luò)數(shù)據(jù)方面采用Retrofit + rxjava,列表使用LRecyclerView复颈,新聞頁面由ViewPager將十幾個不同類型的新聞列表Fragment頁面組合在一起绩聘。種情況由于頁面的切換,以及數(shù)據(jù)列表的刷新加載等耗啦,在開發(fā)中還是比較典型的凿菩,在內(nèi)存控制上也是有較高要求的,因此是比較適合用來做內(nèi)存分析的帜讲。

項目地址

內(nèi)存快照分析方法

這里我們直接使用Android Studio的內(nèi)存分析工具進(jìn)行分析衅谷。打開Android Monitor,可看到Logcat舒帮,切換到Monitors会喝,可看到內(nèi)存陡叠,CPU相關(guān)信息玩郊。

  1. 找到當(dāng)前分析的應(yīng)用,這里為com.test.memory枉阵。
  2. 點(diǎn)擊幾次Initiate GC译红,用于通知垃圾收集進(jìn)行垃圾回收,避免無效的內(nèi)存分析兴溜。
  3. 點(diǎn)擊Dump Java Heap侦厚,過一會就會打開內(nèi)存快照。


接下來分析內(nèi)存快照

  1. 點(diǎn)擊選擇PackageTreeView拙徽,這樣就可以按包名層級進(jìn)行類的查找刨沦。
  2. 左上部分就是應(yīng)用相關(guān)的類的內(nèi)存信息了。通常我們只需按包名com.test.memory找到自己應(yīng)用下的類進(jìn)行分析膘怕,這里找到NewsListFragment進(jìn)行分析想诅,它代表一種類型的新聞列表頁面挣柬。
  3. 可以看到TotalCount這一列是12泰佳,也就是說當(dāng)前有12個NewsListFragment對象,也就是12個NewsListFragment新聞列表頁面了货岭,因?yàn)橹坝袑⑺蓄愋偷捻撁娑即蜷_過了忘古。
  4. Shallow Size這一欄徘禁,可以看到是2880,代表的意思就是NewsListFragment的所有對象占用了多少內(nèi)存髓堪,這里是12個的總大小送朱,因此一個NewsListFragment的大小是240娘荡。注意,這里僅僅是指NewsListFragment本身占用的內(nèi)存驶沼,而作為它的引用屬性對象所占的內(nèi)存是不算其中的它改,比如它持有的視圖View的大小是不算其中的,而只算一個int類型引用的大小商乎,4字節(jié)央拖。所以Shallow Size通常并不大,因?yàn)樗皇钱?dāng)前對象本身的大小鹉戚,不算它引用對象的大小在其中鲜戒。
  5. Retained Size這一欄,是1757826抹凳,也就是1.75M大小了遏餐,也就是說12個NewsListFragment所持有的總大小是1.75M,這里的持有大小赢底,它不但包括NewsListFragment本身的大小失都,還包括它持有對象的大小,并且是它持有對象可被回收的大小幸冻。因此Retained Size是指粹庞,如果NewsListFragment這個對象被回收時,它最終能被回收的內(nèi)存洽损,也就是它本身的內(nèi)存庞溜,和一部分只有被它引用的對象的內(nèi)存,而還被其他對象持有的內(nèi)存是不算在其中的碑定,例如context對象流码,它不僅被NewsListFragment引用,所以它的內(nèi)存大小是不算入在Retained Size中的延刘。
  6. 右上部分代表的是所選擇類的所有對象和其屬性所占內(nèi)存情況漫试。例如這里是NewsListFragment類的12個對象的具體內(nèi)存情況,和它其中各個屬性引用對象的內(nèi)存情況碘赖。這里可以分析其中的哪些屬性或引用對象占用的內(nèi)存較高驾荣。
  7. 下面的部分指的是當(dāng)前的NewsListFragment對象被哪些對象引用了,可以查看它的引用樹崖疤,可用于查找最終導(dǎo)致內(nèi)存泄露無法被釋放的最終根源秘车。


內(nèi)存泄露分析

明白了如何查看內(nèi)存快照信息,知道它們代表的含義之后劫哼,接下來舉一個例子來分析下內(nèi)存泄露問題叮趴。場景是這樣的,在每個新聞列表頁面?zhèn)€NewsListFragment的onCreateView方法時权烧,我會添加一個LeakAnimView在其上眯亦,并開始執(zhí)行縮放動畫伤溉,當(dāng)onDestoryView時移除LeakAnimView,并停止它的動畫妻率。代碼如下:

public class NewsListFragment extends BaseListFragment<NewsPresenter>
    implements NewsContract.View {
    
  private LeakAnimView animView;
    
    @Override
  public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    if(ControInfos.isTestLeak){
      //如果測試內(nèi)存泄露問題乱顾,則執(zhí)行
      animView = new LeakAnimView(view.getContext());
      RelativeLayout parent = (RelativeLayout) view;
      RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(100, 100);
      params.addRule(RelativeLayout.CENTER_IN_PARENT);
      parent.addView(animView, params);
      animView.start();
    }

  }

  @Override
  public void onDestroyView() {
    super.onDestroyView();

    if(ControInfos.isTestLeak){
      //如果測試內(nèi)存泄露問題,則執(zhí)行
      if(animView != null && animView.getParent() != null){
        RelativeLayout parent = (RelativeLayout) animView.getParent();
        animView.cancel();
        parent.removeView(animView);
        animView = null;
      }
    }
  }
    
  ...
  
}

下面是LeakAnimView的實(shí)現(xiàn):

/**
 * 存在內(nèi)存泄露的動畫View宫静,由于動畫Cancel之后走净,還是會回調(diào)onAnimationEnd,所以需要額外判斷是否取消狀態(tài)孤里,否則動畫會一直執(zhí)行下去伏伯,導(dǎo)致內(nèi)存泄露問題
 */
public class LeakAnimView extends View{
    private static final String TAG = "AnimView";

    private AnimatorSet animatorSet, animatorSet2;
    private ObjectAnimator scaleX, scaleY;
    private ObjectAnimator scaleX2, scaleY2;

    private boolean isAnimating;

    public LeakAnimView(Context context) {
        super(context);

        init();
    }

    public LeakAnimView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    private void init(){
        setBackgroundColor(Color.RED);

        animatorSet = new AnimatorSet();
        scaleX = ObjectAnimator.ofFloat(this, "scaleX", 0.5f, 1f);
        scaleY = ObjectAnimator.ofFloat(this, "scaleY", 0.5f, 1f);
        animatorSet.play(scaleX).with(scaleY);
        animatorSet.setDuration(500);
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.d(TAG, "animatorSet onAnimationStart");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.d(TAG, "animatorSet onAnimationEnd");

                //取消動畫時,該方法依然會被回調(diào)捌袜,所以下個動畫會執(zhí)行说搅,存在內(nèi)存泄露問題,所以要做狀態(tài)的判斷

                if(ControInfos.exitstLeak){
                    //這里存在內(nèi)存泄露問題
                    animatorSet2.start();
                }else{
                    //這里解決了內(nèi)存泄露問題
                    if(isAnimating){
                        animatorSet2.start();
                    }
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.d(TAG, "animatorSet onAnimationCancel");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        animatorSet2 = new AnimatorSet();
        scaleX2 = ObjectAnimator.ofFloat(this, "scaleX", 1f, 0.5f);
        scaleY2 = ObjectAnimator.ofFloat(this, "scaleY", 1f, 0.5f);
        animatorSet2.play(scaleX2).with(scaleY2);
        animatorSet2.setDuration(500);
        animatorSet2.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                Log.d(TAG, "animatorSet2 onAnimationStart");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                Log.d(TAG, "animatorSet2 onAnimationEnd");

                //取消動畫時虏等,該方法依然會被回調(diào)弄唧,所以下個動畫會執(zhí)行,存在內(nèi)存泄露問題霍衫,所以要做狀態(tài)的判斷

                if(ControInfos.exitstLeak){
                    //這里存在內(nèi)存泄露問題
                    animatorSet.start();
                }else{
                    //這里解決了內(nèi)存泄露問題
                    if(isAnimating){
                        animatorSet.start();
                    }
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                Log.d(TAG, "animatorSet2 onAnimationCancel");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

    }

    public void start(){
        if(isAnimating){
            return;
        }
        isAnimating = true;
        animatorSet.start();
    }

    public void cancel(){
        if(isAnimating){
            isAnimating = false;
            if(animatorSet.isRunning() || animatorSet.isStarted()){
                animatorSet.cancel();
            }
            if(animatorSet2.isRunning() || animatorSet2.isStarted()){
                animatorSet2.cancel();
            }
        }
    }
}

上面只給出測試導(dǎo)致內(nèi)存泄露的部分候引,其他代碼實(shí)現(xiàn)可以看項目源碼。其實(shí)導(dǎo)致內(nèi)存泄露的原因也比較簡單慕淡,但是如果對動畫不是很熟悉的話背伴,容易踩這個坑沸毁,做一個循環(huán)動畫峰髓,動畫1執(zhí)行完后執(zhí)行動畫2,動畫2執(zhí)行完后執(zhí)行動畫1息尺,如此循環(huán)携兵。重點(diǎn)是取消的時候,除了會回調(diào)onAnimationCancel之外搂誉,仍然會回調(diào)onAnimationEnd徐紧,而如果不在其中做標(biāo)記判斷的話,那么又會去執(zhí)行下一個動畫炭懊,那么取消方法并不能停止動畫并级,動畫會一直持有LeakAnimView,然后導(dǎo)致NewsListFragment即便是所屬的Activity頁面關(guān)閉了也不能被釋放侮腹,這時就存在內(nèi)存泄露問題了嘲碧。


如圖所示,NewsListFragment對象是12個父阻,而LeakAnimView卻有62個之多愈涩,如果左右滑動更多的話望抽,會一直增加,而從底部引用樹中也可以看出是動畫導(dǎo)致的內(nèi)存泄露履婉。那么關(guān)閉頁面之后煤篙,看看這些NewsListFragment和LeakAnimView能不能被回收


發(fā)現(xiàn)這些對象并沒有隨著所屬Activity頁面的關(guān)閉而被回收。那么在修改了內(nèi)存泄露問題之后毁腿,看看效果是怎么樣的


可以看到辑奈,LeakAnimView變成了12個,無論怎么樣左右滑動頁面已烤,它都只保存在12以內(nèi)身害,這說明內(nèi)存泄露不存在了,同時當(dāng)將頁面關(guān)閉時草戈,可以看到LeakAnimView和NewsListFragment對象數(shù)量都為0塌鸯,都被回收了。


內(nèi)存占用分析

上面我們通過分析將內(nèi)存泄露的問題解決了唐片,但是我們深知當(dāng)前的狀態(tài)并不是完美的丙猬。雖然不存在內(nèi)存泄露,但是內(nèi)存占用的問題還是可以進(jìn)行優(yōu)化的费韭。特別是在每個列表頁面數(shù)據(jù)量大茧球,頁面的布局復(fù)雜,帶有重量級的控件在其中時星持,如果這些不能隨著PageAdapter的滑動進(jìn)行一定的釋放的話抢埋,內(nèi)存占用也是會非常高,導(dǎo)致內(nèi)存溢出的問題督暂。這里我們還是以LeakAnimView來做個例子吧揪垄,我們知道,當(dāng)我們?yōu)g覽過所有的NewsListFragment頁面后逻翁,NewsListFragment的對象數(shù)量維持在12饥努,相應(yīng)的LeakAnimView也是在12個。

但是不覺得有點(diǎn)奇怪嗎八回?我在NewsListFragment的onDestroyView中是做了移除操作的酷愧,并且將animView設(shè)為null了,照理說應(yīng)該沒有被其他對象引用了缠诅,應(yīng)該是可以被回收的溶浴,這樣的話,除了有兩三個LeakAnimView對象還存在之外管引,其他應(yīng)該都是被回收的啦士败,但是為啥沒有呢?我們看下其中一個LeakAnimView對象的引用樹汉匙,發(fā)現(xiàn)了問題拱烁。


當(dāng)前的LeakAnimView對象被android.widget.RelativeLayout.DependencyGraph.Node@318059736 (0x12f534d8)給引用了生蚁,當(dāng)然它還有被其他給引用,不過經(jīng)分析戏自,有效的引用是屬于RelativeLayout.DependencyGraph.Node的邦投,那這個是干嘛用的,跟進(jìn)代碼發(fā)現(xiàn)擅笔,原來RelativeLayout中有個Node來管理它的子View志衣,每個子View作為一個節(jié)點(diǎn)Node,DependencyGraph則是用來管理節(jié)點(diǎn)Node猛们,Node還持有的當(dāng)前的LeakAnimView對象的話念脯,說明Node沒有被釋放,執(zhí)行release方法,也就是DependencyGraph沒有執(zhí)行clear方法弯淘。

public class RelativeLayout{
    ...
    
    private static class DependencyGraph {
        ...
        
        void clear() {
            final ArrayList<Node> nodes = mNodes;
            final int count = nodes.size();

            for (int i = 0; i < count; i++) {
                nodes.get(i).release();
            }
            nodes.clear();

            mKeyNodes.clear();
            mRoots.clear();
        }
        
        static class Node {
            ...
        
            void release() {
                view = null;
                dependents.clear();
                dependencies.clear();

                sPool.release(this);
            }
        }
    }
}

再找哪里調(diào)用了DependencyGraph的clear方法绿店,發(fā)現(xiàn)是在RelativeLayout的sortChildren方法中,而sortChildren是在onMeasure方法中被調(diào)用的

public class RelativeLayout{
    ...
    
    private void sortChildren() {
        final int count = getChildCount();
        if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
            mSortedVerticalChildren = new View[count];
        }

        if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
            mSortedHorizontalChildren = new View[count];
        }

        final DependencyGraph graph = mGraph;
        graph.clear();

        for (int i = 0; i < count; i++) {
            graph.add(getChildAt(i));
        }

        graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
        graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mDirtyHierarchy) {
            mDirtyHierarchy = false;
            sortChildren();
        }
        ...
    }
}

也就是說onMeasure沒有在NewsListFragment執(zhí)行onDestroyView時執(zhí)行庐橙。那這個怎么解決假勿,我現(xiàn)在也沒有比較好的解決方案,想了一個做驗(yàn)證性的方法态鳖,通過反射主動調(diào)用RelativeLayout的sortChildren方法

public class NewsListFragment extends BaseListFragment<NewsPresenter>
    implements NewsContract.View {
    ...
     @Override
    public void onDestroyView() {
        super.onDestroyView();

        if(ControInfos.isTestLeak){
          //如果測試內(nèi)存泄露問題转培,則執(zhí)行
          if(animView != null && animView.getParent() != null){
            Log.e("NewsListFragment", "remove pre, animView parent : " + animView.getParent());
            RelativeLayout parent = (RelativeLayout) animView.getParent();
            animView.cancel();
            parent.removeView(animView);
    
            //這里通過反射主動調(diào)用RelativeLayout的sortChildren方法,達(dá)到清除animView被RelativeLayout.DependencyGraph.Node持有引用的問題
            ReflectUtil.invokeMethod(parent.getClass().getName(), "sortChildren", parent, null, new Object[]{});
    
            Log.e("NewsListFragment", "remove post, animView parent : " + animView.getParent());
            Log.e("NewsListFragment", "remove post, parent size : " + parent.getChildCount());
    
            animView = null;
          }
        }
    }
}

現(xiàn)在測試一下看看效果浆竭。


很欣喜的看到浸须,這個只有2個LeakAnimView對象了(當(dāng)前的NewsListFragment和旁邊的NewsListFragment所持有的LeakAnimView對象)。說明確實(shí)是由于被RelativeLayout.DependencyGraph.Node持有的引用導(dǎo)致LeakAnimView對象不能被回收了邦泄。當(dāng)然通過反射去實(shí)現(xiàn)不一定是合適的辦法删窒,大家可以想想其他更合適的方法去實(shí)現(xiàn)。

顯然虎韵,這樣省去了10個LeakAnimView對象所占用的內(nèi)存易稠,那么再延伸到NewsListFragment持有的View的話,是不是可以想辦法去實(shí)現(xiàn)回收其他10個NewsListFragment中的View的內(nèi)存呢包蓝,那么想想,內(nèi)存占用是不是會減少很多企量?具體怎么去做需要大家自己去做嘗試和驗(yàn)證测萎。

總結(jié)

好啦,到總結(jié)的時候了届巩。無論是內(nèi)存泄露的檢測分析硅瞧,還是內(nèi)存占用的優(yōu)化分析,都可以通過查看Android Studio導(dǎo)出的內(nèi)存快照進(jìn)行分析恕汇。內(nèi)存泄露問題著重看類對象的數(shù)量Total Size腕唧,看是否符合預(yù)期或辖,而內(nèi)存占用則更注重去找內(nèi)存占用較大的對象Shallow Size,分析它的數(shù)量枣接,以及哪里占用了較大內(nèi)存颂暇,分析是否合理,然后進(jìn)行針對性的優(yōu)化但惶,更深的體會就得自己親自嘗試了耳鸯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市膀曾,隨后出現(xiàn)的幾起案子县爬,更是在濱河造成了極大的恐慌,老刑警劉巖添谊,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件财喳,死亡現(xiàn)場離奇詭異,居然都是意外死亡斩狱,警方通過查閱死者的電腦和手機(jī)纲缓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喊废,“玉大人祝高,你說我怎么就攤上這事∥劭辏” “怎么了工闺?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瓣蛀。 經(jīng)常有香客問我陆蟆,道長,這世上最難降的妖魔是什么惋增? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任叠殷,我火速辦了婚禮,結(jié)果婚禮上诈皿,老公的妹妹穿的比我還像新娘林束。我一直安慰自己,他們只是感情好稽亏,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布壶冒。 她就那樣靜靜地躺著,像睡著了一般截歉。 火紅的嫁衣襯著肌膚如雪胖腾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天,我揣著相機(jī)與錄音咸作,去河邊找鬼锨阿。 笑死,一個胖子當(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
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年缺脉,在試婚紗的時候發(fā)現(xiàn)自己被綠了痪欲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡攻礼,死狀恐怖业踢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情礁扮,我是刑警寧澤知举,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站太伊,受9級特大地震影響雇锡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜僚焦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一锰提、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芳悲,春花似錦立肘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至罢洲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惹苗。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工殿较, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桩蓉。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓淋纲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親院究。 傳聞我的和親對象是個殘疾皇子洽瞬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評論 2 345

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