之前需要实现一个文字轮播滚动的动画,大致效果描述如下:有多条文本,逐条显示在一个文本框中,每条显示3s,然后当前文本向上滚出文本框,下一条文本在当前文本滚出的同时滚入文本框,整个动画0.5s。整个效果的实现采用的是ValueAnimator和onDraw重绘,实现之后发现会有内存泄漏,正好最近看了下内存分析工具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文件。
打开*.hprof文件,选择Open Dominator Tree for entire heap,会把内存的使用情况按照树型结构展示出来。结合当前案例,内存泄漏一定是因为activity没有释放导致的,所以过滤activity即可,有些问题可能是bitmap等,需要具体对待。
可以看出来,有很多重复的activity在内存中没有释放,导致内存泄漏。接下来需要定位是什么导致activity没有释放。
选择一条,右键,Path To GC Roots -> with all references。这里需要科普java里面的四大引用,简单的说:最常用的就是直接new出来的对象,属于强引用,只要引用链存在就不会被GC。软引用(SoftReference)和弱引用(WeakReference)是比较常用的两个解决对象不能被GC而导致OOM问题的申明对象时的引用方法。它们区别在于软引用在内存不吃紧的时候是不会GC的,而弱引用只要垃圾回收器发现了弱引用对象就会把它GC掉不管内存是否吃紧。当然还有一个幽灵引用(PhantomReference),这个没见用过,都说这个是当一个标记来用。
选择把所有引用链都打印出来,然后显示出来,如下图所示:
可以很直观的看到,activity的context被ScrollTextView持有了,而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<ScrollTextView> mScrollWeakReference;
public ListenerAdapter(ScrollTextView scrollTextView) {
mScrollWeakReference = new WeakReference<ScrollTextView>(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文件