记MarqueeTextView内存泄漏原因

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官方认证微信卡片免费领取↓↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值