ScrollTextView及其内存泄漏分析

  之前需要实现一个文字轮播滚动的动画,大致效果描述如下:有多条文本,逐条显示在一个文本框中,每条显示3s,然后当前文本向上滚出文本框,下一条文本在当前文本滚出的同时滚入文本框,整个动画0.5s。整个效果的实现采用的是ValueAnimatoronDraw重绘,实现之后发现会有内存泄漏,正好最近看了下内存分析工具MAT,就结合这个案例,一并学习内存泄漏分析。

  首先还是说下这个效果的实现。文字轮播滚动,是通过继承TextView自定义实现了一个ScrollTextView来实现的。文字的滚动效果是通过onDraw重绘来达到的,通过ValueAnimator获取当前文本Y坐标的移动距离,然后重绘当前文本和下条文本。

@Override
    public void onDraw(Canvas canvas) {
        if (mList.isEmpty()) {
            return;
        }

        if (mOffsetY < mMovedDistance) {
            drawText(canvas, mIndex, mStartY - mOffsetY);
        }

        if (mOffsetY > 0) {
            drawText(canvas, mNextIndex, mStartY + mMovedDistance - mOffsetY);
        }

        if (isRunning()) {
            mOffsetY = (Float) mValueAnimator.getAnimatedValue();
            invalidate();
        } else {
            if (mOffsetY > 0) {
                mOffsetY = 0f;
                setIndex();
                invalidate();
            }
        }
    }

 当然这中间会有一些细节,比如:最后一条文本的下一条文本是第一条;自己绘制文本,所以文本长度过长截断显示…这个是需要自己计算的等。

  文本展示3s和滚动0.5s效果,是利用ValueAnimator来实现的。考虑时间不需要那么精确,所以采用的是postDelay来实现定时。

private Runnable mCycleRunnable = new Runnable() {

        @Override
        public void run() {
            if (isRunning()) {
                return;
            }
            mValueAnimator = ValueAnimator.ofFloat(0, mMovedDistance);
            mValueAnimator.setDuration(mScrollTime != 0 ? mScrollTime : Constants.MILLI_500);
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    postAnim();
                }
            });
            mValueAnimator.start();
            mOffsetY = 0f;
            invalidate();
        }
    };
    
    public  void postAnim() {
        if (mList.size() > 1) {
            removeCallbacks(mCycleRunnable);
            postDelayed(mCycleRunnable, mStopTime != 0 ? mStopTime : Constants.SECOND_3);
        }
    }

  这里主要是设置ValueAnimator的动画位置和设置动画时间,以及动画结束后,丢一个延迟任务,3s之后再次执行一遍动画,通过不断丢延迟任务,实现不断重绘,达到轮播滚动的效果。

     ScrollTextView的完整代码会附录,其他细节可以自行理解。ScrollTextView的源码

  接下来说下怎么使用MAT进行内存分析。

  打开DDMS,选择Dump HPROF file,就会自动把内存dump到本地,保存为*.hprof文件。

 ScrollTextView及其内存泄漏分析_第1张图片

  打开*.hprof文件,选择Open Dominator Tree for entire heap,会把内存的使用情况按照树型结构展示出来。结合当前案例,内存泄漏一定是因为activity没有释放导致的,所以过滤activity即可,有些问题可能是bitmap等,需要具体对待。

 ScrollTextView及其内存泄漏分析_第2张图片

  可以看出来,有很多重复的activity在内存中没有释放,导致内存泄漏。接下来需要定位是什么导致activity没有释放。

  选择一条,右键,Path To GC Roots -> with all references。这里需要科普java里面的四大引用,简单的说:最常用的就是直接new出来的对象,属于强引用,只要引用链存在就不会被GC。软引用(SoftReference)和弱引用(WeakReference)是比较常用的两个解决对象不能被GC而导致OOM问题的申明对象时的引用方法。它们区别在于软引用在内存不吃紧的时候是不会GC的,而弱引用只要垃圾回收器发现了弱引用对象就会把它GC掉不管内存是否吃紧。当然还有一个幽灵引用(PhantomReference),这个没见用过,都说这个是当一个标记来用。

 ScrollTextView及其内存泄漏分析_第3张图片

     选择把所有引用链都打印出来,然后显示出来,如下图所示:

 ScrollTextView及其内存泄漏分析_第4张图片

  可以很直观的看到,activitycontextScrollTextView持有了,而ScrollTextView有个message回调被MessageQueue持有,而这个Queue里面的任务是异步的,也就是我的延迟3s然后再执行动画,所以导致整个引用链都不能释放到,从而导致不能GC

     从内存分析定位之后,我们需要从代码端来看下为什么会内存泄漏。

private Runnable mCycleRunnable = new Runnable() {

        @Override
        public void run() {
            if (isRunning()) {
                return;
            }
            mValueAnimator = ValueAnimator.ofFloat(0, mMovedDistance);
            mValueAnimator.setDuration(mScrollTime != 0 ? mScrollTime : Constants.MILLI_500);
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    postAnim();
                }
            });
            mValueAnimator.start();
            mOffsetY = 0f;
            invalidate();
        }
    };

  当知道问题是异步延迟message导致的,就很好定位代码段就是上文中的runnable部分。再仔细分析一下,不难看出,其实是一个比较典型的内存泄漏的错误写法,匿名内部类里面有异步处理逻辑。如果这是一个activity我们可能的解决方案就是把这个runnable定义为static,使得内部类不持有外部类,但是这是一个自定义view,所以可以采用弱引用的解决方案。

private Runnable mCycleRunnable = new Runnable() {

        @Override
        public void run() {
            if (isRunning()) {
                return;
            }
            mValueAnimator = ValueAnimator.ofFloat(0, mMovedDistance);
            mValueAnimator.setDuration(mScrollTime != 0 ? mScrollTime : Constants.MILLI_500);
            mValueAnimator.addListener(new ListenerAdapter(ScrollTextView.this));
            mValueAnimator.start();
            mOffsetY = 0f;
            invalidate();
        }
    };

    private static class ListenerAdapter extends AnimatorListenerAdapter{
        private WeakReference mScrollWeakReference;
        public ListenerAdapter(ScrollTextView scrollTextView) {
            mScrollWeakReference = new WeakReference(scrollTextView);
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            if(mScrollWeakReference == null){
                return;
            }
            ScrollTextView scrollTextView = mScrollWeakReference.get();
            if (scrollTextView != null) {
                scrollTextView.postAnim();
            }
        }
    }

   至此,ScrollTextView的内存泄漏问题搞定,内存dump文件也会附录,有兴趣的同学可以打开看看。ScrollTextView的内存dump文件

你可能感兴趣的:(Android,内存泄露,ScrollTextView,MAT)