微信越滑越卡(修正版)

背景

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

我修复了这个问题,目前这个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的时间间隔竟然在9ms左右,从而导致了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开源代码贡献代码。


本文分享自微信公众号 - 秉心说TM(gh_c6504b1af5ae)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章