MarqueeTextView内存泄露
背景
项目中使用了MarqueeTextView
组件,用于以跑马灯的形式展示公告信息。在当前页启动新的Activity时,Leakcanary检测到新的Activity内存泄漏了~~
LeakCanary泄漏链
Activity–>android.app.ActivityThreadActivityClientRecord.activity−−>android.app.ActivityThreadActivityClientRecord.activity -->android.app.ActivityThreadActivityClientRecord.activity−−>android.app.ActivityThreadActivityClientRecord.nextIdle -->android.app.ActivityThread.mNewActivies
从泄漏链条上看,似乎并不是应用内导致的内存泄漏,而是系统导致的~~
一、从源码角度分析原因
1、mNewActivities对象为ActivityThread成员变量
// List of new activities (via ActivityRecord.nextIdle) that should
// be reported when next we idle.
ActivityClientRecord mNewActivities = null;
2、handleResumeActivity时mNewActivities赋值
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
//...省略了部分代码
//关键代码
r.nextIdle = mNewActivities;
mNewActivities = r;
//当MessageQueue空闲时,回调该接口
Looper.myQueue().addIdleHandler(new Idler());
}
IdleHandler 是一个回调接口,当线程中的消息队列将要阻塞等待消息的时候,就会回调该接口,也就是说消息队列中的消息都处理完毕了,没有新的消息了,处于空闲状态时就会回调该接口。
3、Idler中清空mNewActivities
private class Idler implements MessageQueue.IdleHandler {
@Override
public final boolean queueIdle() {
ActivityClientRecord a = mNewActivities;
boolean stopProfiling = false;
if (mBoundApplication != null && mProfiler.profileFd != null
&& mProfiler.autoStopProfiler) {
stopProfiling = true;
}
if (a != null) {
//清空mNewActivities
mNewActivities = null;
final ActivityClient ac = ActivityClient.getInstance();
ActivityClientRecord prev;
do {
if (localLOGV) Slog.v(
TAG, "Reporting idle of " + a +
" finished=" +
(a.activity != null && a.activity.mFinished));
if (a.activity != null && !a.activity.mFinished) {
ac.activityIdle(a.token, a.createdConfig, stopProfiling);
a.createdConfig = null;
}
prev = a;
a = a.nextIdle;
prev.nextIdle = null;
} while (a != null);
}
if (stopProfiling) {
mProfiler.stopProfiling();
}
return false;
}
}
换言之:假如IdleHandler
一直无法执行,则意味着mNewActivities
会一直持有当前Activity实例,即使Activity退出该页面了。ActivityThread
生命周期与应用进程生命周期一致,这就导致了当前Activity实例发生泄漏。那么导致IdleHandler
一直无法执行,其根本原因必然是MessageQueue
一直有待处理的消息,导致无法进入空闲状态。接下来我们对MarqueeTextView
源码进行分析,找到引起泄露的根本原因。
二、从MarqueeTextView源码角度分析原因
1、MarqueueTextView源码
/**
* 开始滚动
*/
public void startScroll() {
mXPaused = 0;
mPaused = true;
mFirst = true;
resumeScroll();
}
/**
* 继续滚动
*/
public void resumeScroll() {
if (!mPaused)
return;
// 设置水平滚动
setHorizontallyScrolling(true);
// 使用 LinearInterpolator 进行滚动
if (mScroller == null) {
mScroller = new Scroller(this.getContext(), new LinearInterpolator());
setScroller(mScroller);
}
int scrollingLen = calculateScrollingLen();
final int distance = scrollingLen - (getWidth() + mXPaused);
final int duration = (Double.valueOf(mRollingInterval * distance * 1.00000
/ scrollingLen)).intValue();
if (mFirst) {
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
@Override
public void run() {
mScroller.startScroll(mXPaused, 0, distance, 0, duration);
invalidate();
mPaused = false;
mFirst = false;
}
}, mFirstScrollDelay);
} else {
mScroller.startScroll(mXPaused, 0, distance, 0, duration);
invalidate();
mPaused = false;
}
}
从startScroll
方法中可知,MarqueueTextView滚动使用的是mScroller来达到跑马灯效果。
2、Scroller.startScroll源码
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
从源码可知,上述代码仅仅只是记录了滚动所需的配置信息,实际上并未做任何关于滚动的事情。而Scroller滚动的原理为:invalidate方法结合MarqueueTextView.computeScroll()来完成动画的执行。
// TextView
@Override
public void computeScroll() {
if (mScroller != null) {
if (mScroller.computeScrollOffset()) {
mScrollX = mScroller.getCurrX();
mScrollY = mScroller.getCurrY();
invalidateParentCaches();
//调用
postInvalidate(); // So we draw again
}
}
}
通过跟踪源码可知:postInvalidate最终调用ViewRootImpl.dispatchInvalidateDelayed方法
ViewRootImpl.java
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
由代码可知,当MarqueueTextView若一直运行,会一直向MessageQueue发送发送了一个MSG_INVALIDATE的消息。因此MessageQueue无法达到空闲状态,故引发泄漏
三、解决方案及经验教训
1、解决方案
MarqueueTextView要在生命周期内管理运行及暂停。销毁当前页时还应该销毁组件
@Override
public void onResume() {
if(tv_market_state_tips.isPaused() && !tv_market_state_tips.isFirst()) {
tv_market_state_tips.resumeScroll();
}
super.onResume();
}
@Override
public void onPause() {
//解决内存泄漏问题
tv_market_state_tips.pauseScroll();
super.onPause();
}
@Override
public void onDestroyView() {
//解决内存泄漏问题
tv_market_state_tips.stopScroll();
super.onDestroyView();
}
2、经验教训
如上问题只是其中一个个例,我们在动画执行、自定义view,也会导致上述问题,今后开发过程中,仍需注意相关的问题
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓