内存泄漏与优化分析指南

前言

在android开发中,我们都或多或少的会遇到一些内存泄漏的问题,虽然大都知道哪些情况会导致内存泄露,但是还是不可避免的会遇到类似的问题,因此,知道如何去查找内存泄露就显得非常重要了。本篇和大家分享下如何进行内存泄漏的定位分析,以及对内存占用的优化分析。相信大家看了之后会有所收获。

为了有一个良好的分析体验,我特意新建了一个用于分析内存方面的项目,该项目是一个简易的新闻客户端,结构上大致是这样的,mvp开发模式,网络数据方面采用Retrofit + rxjava,列表使用LRecyclerView,新闻页面由ViewPager将十几个不同类型的新闻列表Fragment页面组合在一起。种情况由于页面的切换,以及数据列表的刷新加载等,在开发中还是比较典型的,在内存控制上也是有较高要求的,因此是比较适合用来做内存分析的。

项目地址

内存快照分析方法

这里我们直接使用Android Studio的内存分析工具进行分析。打开Android Monitor,可看到Logcat,切换到Monitors,可看到内存,CPU相关信息。

  1. 找到当前分析的应用,这里为com.test.memory。
  2. 点击几次Initiate GC,用于通知垃圾收集进行垃圾回收,避免无效的内存分析。
  3. 点击Dump Java Heap,过一会就会打开内存快照。


接下来分析内存快照

  1. 点击选择PackageTreeView,这样就可以按包名层级进行类的查找。
  2. 左上部分就是应用相关的类的内存信息了。通常我们只需按包名com.test.memory找到自己应用下的类进行分析,这里找到NewsListFragment进行分析,它代表一种类型的新闻列表页面。
  3. 可以看到TotalCount这一列是12,也就是说当前有12个NewsListFragment对象,也就是12个NewsListFragment新闻列表页面了,因为之前有将所有类型的页面都打开过了。
  4. Shallow Size这一栏,可以看到是2880,代表的意思就是NewsListFragment的所有对象占用了多少内存,这里是12个的总大小,因此一个NewsListFragment的大小是240。注意,这里仅仅是指NewsListFragment本身占用的内存,而作为它的引用属性对象所占的内存是不算其中的,比如它持有的视图View的大小是不算其中的,而只算一个int类型引用的大小,4字节。所以Shallow Size通常并不大,因为它只是当前对象本身的大小,不算它引用对象的大小在其中。
  5. Retained Size这一栏,是1757826,也就是1.75M大小了,也就是说12个NewsListFragment所持有的总大小是1.75M,这里的持有大小,它不但包括NewsListFragment本身的大小,还包括它持有对象的大小,并且是它持有对象可被回收的大小。因此Retained Size是指,如果NewsListFragment这个对象被回收时,它最终能被回收的内存,也就是它本身的内存,和一部分只有被它引用的对象的内存,而还被其他对象持有的内存是不算在其中的,例如context对象,它不仅被NewsListFragment引用,所以它的内存大小是不算入在Retained Size中的。
  6. 右上部分代表的是所选择类的所有对象和其属性所占内存情况。例如这里是NewsListFragment类的12个对象的具体内存情况,和它其中各个属性引用对象的内存情况。这里可以分析其中的哪些属性或引用对象占用的内存较高。
  7. 下面的部分指的是当前的NewsListFragment对象被哪些对象引用了,可以查看它的引用树,可用于查找最终导致内存泄露无法被释放的最终根源。


内存泄露分析

明白了如何查看内存快照信息,知道它们代表的含义之后,接下来举一个例子来分析下内存泄露问题。场景是这样的,在每个新闻列表页面个NewsListFragment的onCreateView方法时,我会添加一个LeakAnimView在其上,并开始执行缩放动画,当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){
      //如果测试内存泄露问题,则执行
      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){
      //如果测试内存泄露问题,则执行
      if(animView != null && animView.getParent() != null){
        RelativeLayout parent = (RelativeLayout) animView.getParent();
        animView.cancel();
        parent.removeView(animView);
        animView = null;
      }
    }
  }
    
  ...
  
}

下面是LeakAnimView的实现:

/**
 * 存在内存泄露的动画View,由于动画Cancel之后,还是会回调onAnimationEnd,所以需要额外判断是否取消状态,否则动画会一直执行下去,导致内存泄露问题
 */
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");

                //取消动画时,该方法依然会被回调,所以下个动画会执行,存在内存泄露问题,所以要做状态的判断

                if(ControInfos.exitstLeak){
                    //这里存在内存泄露问题
                    animatorSet2.start();
                }else{
                    //这里解决了内存泄露问题
                    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");

                //取消动画时,该方法依然会被回调,所以下个动画会执行,存在内存泄露问题,所以要做状态的判断

                if(ControInfos.exitstLeak){
                    //这里存在内存泄露问题
                    animatorSet.start();
                }else{
                    //这里解决了内存泄露问题
                    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();
            }
        }
    }
}

上面只给出测试导致内存泄露的部分,其他代码实现可以看项目源码。其实导致内存泄露的原因也比较简单,但是如果对动画不是很熟悉的话,容易踩这个坑,做一个循环动画,动画1执行完后执行动画2,动画2执行完后执行动画1,如此循环。重点是取消的时候,除了会回调onAnimationCancel之外,仍然会回调onAnimationEnd,而如果不在其中做标记判断的话,那么又会去执行下一个动画,那么取消方法并不能停止动画,动画会一直持有LeakAnimView,然后导致NewsListFragment即便是所属的Activity页面关闭了也不能被释放,这时就存在内存泄露问题了。


如图所示,NewsListFragment对象是12个,而LeakAnimView却有62个之多,如果左右滑动更多的话,会一直增加,而从底部引用树中也可以看出是动画导致的内存泄露。那么关闭页面之后,看看这些NewsListFragment和LeakAnimView能不能被回收


发现这些对象并没有随着所属Activity页面的关闭而被回收。那么在修改了内存泄露问题之后,看看效果是怎么样的


可以看到,LeakAnimView变成了12个,无论怎么样左右滑动页面,它都只保存在12以内,这说明内存泄露不存在了,同时当将页面关闭时,可以看到LeakAnimView和NewsListFragment对象数量都为0,都被回收了。


内存占用分析

上面我们通过分析将内存泄露的问题解决了,但是我们深知当前的状态并不是完美的。虽然不存在内存泄露,但是内存占用的问题还是可以进行优化的。特别是在每个列表页面数据量大,页面的布局复杂,带有重量级的控件在其中时,如果这些不能随着PageAdapter的滑动进行一定的释放的话,内存占用也是会非常高,导致内存溢出的问题。这里我们还是以LeakAnimView来做个例子吧,我们知道,当我们浏览过所有的NewsListFragment页面后,NewsListFragment的对象数量维持在12,相应的LeakAnimView也是在12个。

但是不觉得有点奇怪吗?我在NewsListFragment的onDestroyView中是做了移除操作的,并且将animView设为null了,照理说应该没有被其他对象引用了,应该是可以被回收的,这样的话,除了有两三个LeakAnimView对象还存在之外,其他应该都是被回收的啦,但是为啥没有呢?我们看下其中一个LeakAnimView对象的引用树,发现了问题。


当前的LeakAnimView对象被android.widget.RelativeLayout.DependencyGraph.Node@318059736 (0x12f534d8)给引用了,当然它还有被其他给引用,不过经分析,有效的引用是属于RelativeLayout.DependencyGraph.Node的,那这个是干嘛用的,跟进代码发现,原来RelativeLayout中有个Node来管理它的子View,每个子View作为一个节点Node,DependencyGraph则是用来管理节点Node,Node还持有的当前的LeakAnimView对象的话,说明Node没有被释放,执行release方法,也就是DependencyGraph没有执行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);
            }
        }
    }
}

再找哪里调用了DependencyGraph的clear方法,发现是在RelativeLayout的sortChildren方法中,而sortChildren是在onMeasure方法中被调用的

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执行onDestroyView时执行。那这个怎么解决,我现在也没有比较好的解决方案,想了一个做验证性的方法,通过反射主动调用RelativeLayout的sortChildren方法

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

        if(ControInfos.isTestLeak){
          //如果测试内存泄露问题,则执行
          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);
    
            //这里通过反射主动调用RelativeLayout的sortChildren方法,达到清除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;
          }
        }
    }
}

现在测试一下看看效果。


很欣喜的看到,这个只有2个LeakAnimView对象了(当前的NewsListFragment和旁边的NewsListFragment所持有的LeakAnimView对象)。说明确实是由于被RelativeLayout.DependencyGraph.Node持有的引用导致LeakAnimView对象不能被回收了。当然通过反射去实现不一定是合适的办法,大家可以想想其他更合适的方法去实现。

显然,这样省去了10个LeakAnimView对象所占用的内存,那么再延伸到NewsListFragment持有的View的话,是不是可以想办法去实现回收其他10个NewsListFragment中的View的内存呢,那么想想,内存占用是不是会减少很多?具体怎么去做需要大家自己去做尝试和验证。

总结

好啦,到总结的时候了。无论是内存泄露的检测分析,还是内存占用的优化分析,都可以通过查看Android Studio导出的内存快照进行分析。内存泄露问题着重看类对象的数量Total Size,看是否符合预期,而内存占用则更注重去找内存占用较大的对象Shallow Size,分析它的数量,以及哪里占用了较大内存,分析是否合理,然后进行针对性的优化,更深的体会就得自己亲自尝试了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容