微信越滑越卡(修正版)

背景

在一個已經加載完成很長的微信聊天記錄中,持續不斷的滑動,慢慢的微信會越滑越卡。

我修復了這個問題,目前這個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源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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