[063]微信越滑越卡

背景

在一个已经加载完成很长的微信聊天记录中,持续不断的滑动,慢慢的微信会越滑越卡。

我修复了这个问题,目前这个Patch已经被merge进了Android仓库:
https://android-review.googlesource.com/c/platform/frameworks/base/+/1645426

一、卡顿的原因分析

Choreographer#doFrame的animation中会堆积大量的Callback-AbsListView#FlingRunnable
从而导致了最后这一帧的绘制超时,导致了卡顿。


二、FlingRunnable堆积的原因

一次滑动会触发一个Down事件,多个Move事件,一个Up事件。
从下图可以发现,这次滑动,导致animation的FlingRunnable从3个增加到了4个


看看这4个是怎么来的:

3个是来自于之前的FlingRunnable,新增的一个来自于Up事件触发的。


三、代码分析

3.1 onTouchDown

Touch Down事件会触发mFlingRunnable.flywheelTouch()

    private void onTouchDown(MotionEvent ev) {
        ...
        if (mTouchMode == TOUCH_MODE_OVERFLING) {
        ...
        } else {
            ...
            if (!mDataChanged) { //ListView的数据没有更新
                if (mTouchMode == TOUCH_MODE_FLING) {//ListView处于Fling的状态
                    // Stopped a fling. It is a scroll.
                    createScrollingCache();
                    mTouchMode = TOUCH_MODE_SCROLL;
                    mMotionCorrection = 0;
                    motionPosition = findMotionRow(y);
                    mFlingRunnable.flywheelTouch();//跳转到3.1.1
            ...

3.1.1 mFlingRunnable.flywheelTouch

flywheelTouch会postdelay一个mCheckFlywheel延迟40ms。

如果mVelocityTracker为null,将会直接return。

如果Math.abs(yvel) >= mMinimumVelocity,将会再次postdelay一个mCheckFlywheel,让ListView继续滑动一段时间。

如果Math.abs(yvel) < mMinimumVelocity,将会endFling(),立刻停止滑动

        private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds

        void flywheelTouch() {
            postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT);
        }

        private final Runnable mCheckFlywheel = new Runnable() {
            @Override
            public void run() {
                //计算滑动过程中y方向的速度
                final int activeId = mActivePointerId;
                final VelocityTracker vt = mVelocityTracker;
                final OverScroller scroller = mScroller;
                //onTouchUp的时候会调用recycleVelocityTracker(),
                //然后vt为null,结束这次down事件持续postdelay的mCheckFlywheel
                if (vt == null || activeId == INVALID_POINTER) {
                    return;
                }
                vt.computeCurrentVelocity(1000, mMaximumVelocity);
                final float yvel = -vt.getYVelocity(activeId);
                if (Math.abs(yvel) >= mMinimumVelocity
                        && scroller.isScrollingInDirection(0, yvel)) {
                    //如果速度大于mMinimumVelocity,让列表继续Fling
                    // Keep the fling alive a little longer
                    postDelayed(this, FLYWHEEL_TIMEOUT);
                } else {
                    //如果速度小于mMinimumVelocity,触发endFling,停止Fling
                    endFling();
                    mTouchMode = TOUCH_MODE_SCROLL;
                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                }
            }
        };

        void endFling() {
            ...
            removeCallbacks(this);
            removeCallbacks(mCheckFlywheel);
            ...
        }

3.2 onTouchUp

执行mFlingRunnable.start(-initialVelocity),postOnAnimation(this);
调用recycleVelocityTracker回收mVelocityTracker,mVelocityTracker设置为null,这样会结束Down事件postdelay的mCheckFlywheel,直接return,不做任何事情,详见3.1.1逻辑

    private void onTouchUp(MotionEvent ev) {
        switch (mTouchMode) {
        ...
        case TOUCH_MODE_SCROLL:
        ...
            if (!dispatchNestedPreFling(0, -initialVelocity)) {
                if (mFlingRunnable == null) {
                    mFlingRunnable = new FlingRunnable();
                }
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
                mFlingRunnable.start(-initialVelocity);//跳到下面的start方法
                dispatchNestedFling(0, -initialVelocity, true);
            } else {
                mTouchMode = TOUCH_MODE_REST;
                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
            }
        ...
        recycleVelocityTracker();//这里会回收mVelocityTracker,结束这轮down事件触发的mCheckFlywheel
    }

    void start(int initialVelocity) {
        ...
        postOnAnimation(this);
        ...
    }

    private void recycleVelocityTracker() {
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

3.3 FlingRunnable#run

如果ListView处于TOUCH_MODE_SCROLL或者TOUCH_MODE_FLING的状态,并且还有更多的内容,就会继续postOnAnimation(this)

如果用户不持续的触发滑动事件,慢慢的达到more的值为false,然后endFling,这就是为什么慢慢的ListView会停的逻辑。

@Override
public void run() {
    switch (mTouchMode) {
    default:
        endFling();
        return;
    case TOUCH_MODE_SCROLL:
        if (mScroller.isFinished()) {
            return;
        }
        // Fall through
    case TOUCH_MODE_FLING: {
        boolean more = scroller.computeScrollOffset();
        ...
        if (more && !atEnd) {
            if (atEdge) invalidate();
            mLastFlingY = y;
            postOnAnimation(this);
        } else {
            endFling();//停止滑动          
            ...
        }
        break;
    }
    ...
    }
}

3.4 小结

onTouchDown

postdelay 40ms一个mCheckFlywheel,mCheckFlywheel将会检查ListView是否应该停止。
如果Y方向速度一直大于mMinimumVelocity,将会持续postdelay mCheckFlywheel。
直到onTouchUp将mVelocityTracker置空,然后结束mCheckFlywheel的持续postdelay。

onTouchUp

postOnAnimation(FlingRunnable),让ListView开始Fling起来。
recycleVelocityTracker()将mVelocityTracker设置为null。

FlingRunnable

再次触发一个postOnAnimation(FlingRunnable)。
如果more的值为false,将会触发endfling.

四、对比分析

4.1 为什么Google Pixel不存在这个BUG

原来Google Pixel每次滑动Down和Move事件的间隔绝大多数情况下大于40ms,从而导致mCheckFlywheel中endFling可以在持续的滑动中被有效的执行,这样子就不会导致FlingRunnable的堆积

4.2 为什么我们的手机会存在这个BUG

原来我们的手机TP采样率比较高,接近180hz,而且我们的性能优化比较好,可以保证Down和Move的时间间隔永远小于40ms,从而导致了mCheckFlywheel永远被postdelay,无法有效的执行endFling,这样子就导致了FlingRunnable的堆积

五、解决方案

在FlingRunnable.start中调用postOnAnimation之前removeCallbacks(this),避免FlingRunnable的堆积
这个方案已经被merge进了Android官方主分支中:
https://android-review.googlesource.com/c/platform/frameworks/base/+/1645426

void start(int initialVelocity) {
    int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
    mLastFlingY = initialY;
    mScroller.setInterpolator(null);
    mScroller.fling(0, initialY, 0, initialVelocity,
            0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
    mTouchMode = TOUCH_MODE_FLING;
    mSuppressIdleStateChangeCall = false;
    removeCallbacks(this);//修复的patch
    postOnAnimation(this);
    if (PROFILE_FLINGING) {
        if (!mFlingProfilingStarted) {
            Debug.startMethodTracing("AbsListViewFling");
            mFlingProfilingStarted = true;
        }
    }
    if (mFlingStrictSpan == null) {
        mFlingStrictSpan = StrictMode.enterCriticalSpan("AbsListView-fling");
    }
}

总结

这是我作为android工程师第一次成功提交代码到Android官方主分支,还是值得纪念的,可惜提交的账户不是我自己的,而是公司账户,因为自己的账户很有可能Google工程师不会review你的提交。有了一次就会有第二次,期待我下次继续为Android开源代码贡献代码。

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