SmartRefreshLayout 时间逆流缺陷分析

一、问题表现

SmartRefreshLayout 开启 mEnableAutoLoadMore(自动加载更多)后,用户向下拉出 Header 一点点松手回弹 (或惯性滑动触顶越界)时,会异常触发 onLoadMore (上拉加载更多)

二、 根源剖析:时钟域错位与时序异常

该 Bug 的根本原因是 Android 系统的 Vsync 帧时钟(帧锁死期)主线程 Handler 的实时物理时钟(非锁死期) 在同一主线程的消息队列调度过程中发生了 跨时钟域的时序错位

2.1 AOSP 时钟哲学与 lockAnimationClock 锁死机制

Android 系统的 AnimationUtils 维护了一个静态的线程局部变量(ThreadLocal)来管理动画时钟,其官方真实源码如下:

java 复制代码
private static class AnimationState {
    boolean animationClockLocked;
    long currentVsyncTimeMillis;
    long lastReportedTimeMillis;
};

private static ThreadLocal<AnimationState> sAnimationState
= new ThreadLocal<AnimationState>() {
    @Override
    protected AnimationState initialValue() {
        return new AnimationState();
    }
};  

/**
* Returns the current animation time in milliseconds. This time should be used when invoking
* {  @link  Animation#setStartTime(long)}. Refer to {  @link  android.os.SystemClock} for more
* information about the different available clocks. The clock used by this method is
* <em>not</em> the "wall" clock (it is not {  @link  System#currentTimeMillis}).
*
*  @return  the current animation time in milliseconds
*
*  @see  android.os.SystemClock
*/
public static long currentAnimationTimeMillis() {
    AnimationState state = sAnimationState.get();
    if (state.animationClockLocked) {
        // It's important that time never rewinds
        return Math.max(state.currentVsyncTimeMillis,
                state.lastReportedTimeMillis);
    }
    state.lastReportedTimeMillis = SystemClock.uptimeMillis();
    return state.lastReportedTimeMillis;
}    

2.1.1 lockAnimationClock 的设计约束

根据 Android AOSP 官方源码对于 lockAnimationClock(long vsyncMillis) 方法的详细英文注释,我们可以直窥其底层时钟机制的设计哲学:

java 复制代码
/**
 * Locks AnimationUtils{  @link  #currentAnimationTimeMillis()} to a fixed value for the current
 * thread. This is used by {  @link  android.view.Choreographer} to ensure that all accesses
 * during a vsync update are synchronized to the timestamp of the vsync.
 *
 * It is also exposed to tests to allow for rapid, flake-free headless testing.
 *
 * Must be followed by a call to {  @link  #unlockAnimationClock()} to allow time to
 * progress. Failing to do this will result in stuck animations, scrolls, and flings.
 *
 * Note that time is not allowed to "rewind" and must perpetually flow forward. So the
 * lock may fail if the time is in the past from a previously returned value, however
 * time will be frozen for the duration of the lock. The clock is a thread-local, so
 * ensure that {  @link  #lockAnimationClock(long)}, {  @link  #unlockAnimationClock()}, and
 * {  @link  #currentAnimationTimeMillis()} are all called on the same thread.
 *
 * This is also not reference counted in any way. Any call to {  @link  #unlockAnimationClock()}
 * will unlock the clock for everyone on the same thread. It is therefore recommended
 * for tests to use their own thread to ensure that there is no collision with any existing
 * {  @link  android.view.Choreographer} instance.
 *
 *  @hide
 * */
@TestApi
public static void lockAnimationClock(long vsyncMillis) {
    AnimationState state = sAnimationState.get();
    state.animationClockLocked = true;
    state.currentVsyncTimeMillis = vsyncMillis;
}

"Note that time is not allowed to 'rewind' and must perpetually flow forward. So the lock may fail if the time is in the past from a previously returned value, however time will be frozen for the duration of the lock."

(译:注意,时间是不允许"倒回/回退"的,它必须永远向前流动。因此,如果锁定的时间在之前已返回时间的"过去",锁可能会失效,但在锁定期间时间会被冻结。)

为了保证这一"时间单调递增"的约束,Android 在 currentAnimationTimeMillis() 内部以 Math.max(currentVsyncTimeMillis, lastReportedTimeMillis) 实现了时间戳的单调递增保障。

2.1.2 锁死期"只读不写"的 API 使用边界约定

这套防倒流防线在锁死期内存在一个经过刻意设计"只读不写" 约束(这是 Google 工程师有意为之的设计规范,不是漏洞):

  • state.animationClockLocked == true 的锁死期内,调用时钟虽然会进行 Math.max 比对并返回较大值,但这是一个纯只读操作,源码不会将计算出来的较大时间戳写回 state.lastReportedTimeMillis 字段
  • 这个设计的初衷是:同帧内所有动画时钟读取都应在同一个锁死期上下文内进行 (AOSP 注释明确要求 lockAnimationClockunlockAnimationClockcurrentAnimationTimeMillis 必须在同一线程的同一帧周期内成对调用)。lastReported 只在锁完全释放后的常规非锁死期才会被推进,确保帧内时钟完全幂等、稳定一致------这正是 Vsync 帧对齐机制的精髓所在。
  • SmartRefreshLayout 的根本问题在于BounceRunnable 在帧锁死期(T0)读取了 Vsync 帧时钟值作为 mLastTime,却在下一个 Looper 消息的非锁死期(T1)才读取实时物理时钟作为 now------两次读取分属不同时钟域,差值语义不一致,从而导致时间步长为负。

2.2 帧时钟域与实时物理时钟域的差异

在多高刷与多缓冲渲染管线中,Choreographer 在触发 doFrame 帧渲染时传入的并非当前实时时间,而是预测该帧被投递到屏幕时的期望显示时间(Expected Presentation Time)

这使得同一主线程在运行时存在两种不同的时钟语义:

  1. Vsync 帧时钟(帧锁死期, locked: true :时钟锁定为 Choreographer 预期的未来显示时间,通常比当前物理时间提前 20ms ~ 30ms,用于保障同帧内所有动画时间戳的一致性。
  2. 实时物理时钟(非锁死期, locked: false :时钟直接返回 SystemClock.uptimeMillis() 的当前物理时间。

2.2.1 实测日志数据与时序分析

为在真机上复现并验证时钟域错位问题,通过以下方式对 BounceRunnable 进行插桩:在构造方法(T0)和第一帧 run()(T1)分别读取 SystemClock.uptimeMillis()AnimationUtils.currentAnimationTimeMillis(),并通过反射获取主线程当前的 AnimationState 对象字段快照。

① 引入依赖( build.gradle

arduino 复制代码
dependencies {
    implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3'
}

AnimationState 反射工具类( TestUtil.java

由于 AnimationUtils.sAnimationState 是非公开 API,Android 9+ 默认禁止反射访问。TestUtil 通过 HiddenApiBypass 解除限制后,使用标准 Java 反射读取 AnimationState 的三个关键字段:

ini 复制代码
public class TestUtil {

    private static final String TAG = "SRL_DEBUG";
    private static boolean sBypassed = false;

    /**
     * 通过反射获取 AnimationUtils 的 ThreadLocal sAnimationState 局部变量的所有字段值并高 亮打印
     * 使用 HiddenApiBypass 绕过 Android 9+ 对非 SDK 隐藏接口的反射限制
     *
     *  @param  tag 日志的前缀标签
    */
    public static void printAnimationState(String tag) {
        try {
            // 1. 首次调用时,通过 HiddenApiBypass 豁免所有隐藏 API 限制
            if (!sBypassed) {
                try {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                        org.lsposed.hiddenapibypass.HiddenApiBypass.addHiddenApiExemptions("L");
                        Log.e(TAG, "Successfully added HiddenApi exemptions via HiddenApiBypass!");
                    }
                    sBypassed = true;
                } catch (Throwable t) {
                    Log.e(TAG, "Failed to initialize HiddenApiBypass: " + t.getMessage(), t);
                }
            }

            // 2. 豁免之后,直接使用最标准的 Java 反射获取 sAnimationState
            Field sAnimationStateField = AnimationUtils.class.getDeclaredField("sAnimationState");
            sAnimationStateField.setAccessible(true);
            ThreadLocal<?> sAnimationState = (ThreadLocal<?>) sAnimationStateField.get(null);
            
            if (sAnimationState == null) {
                Log.e(TAG, tag + " -> sAnimationState is null!");
                return;
            }

            // 3. 从 ThreadLocal 中拿到当前线程的 AnimationState 对象
            Object animationStateObj = sAnimationState.get();
            if (animationStateObj == null) {
                Log.e(TAG, tag + " -> animationStateObj is null!");
                return;
            }

            Class<?> animationStateClass = animationStateObj.getClass();

            // 4. 直接使用最标准的 Java 反射获取私有内部类的各个字段
            Field lockedField = animationStateClass.getDeclaredField("animationClockLocked");
            lockedField.setAccessible(true);
            boolean animationClockLocked = (boolean) lockedField.get(animationStateObj);

            Field vsyncField = animationStateClass.getDeclaredField("currentVsyncTimeMillis");
            vsyncField.setAccessible(true);
            long currentVsyncTimeMillis = (long) vsyncField.get(animationStateObj);

            Field lastReportedField = animationStateClass.getDeclaredField("lastReportedTimeMillis");
            lastReportedField.setAccessible(true);
            long lastReportedTimeMillis = (long) lastReportedField.get(animationStateObj);

            // 4. 获取对象的唯一 Identity HashCode,用以验证是否为同一个 Java 堆对象
            int objHashCode = System.identityHashCode(animationStateObj);

            // 5. 打印输出
            Log.e(TAG, String.format("[%s] ThreadLocal AnimationState (Obj: @%x) -> " +
                    "locked: %b, " +
                    "currentVsync: %d, " +
                    "lastReported: %d",
                    tag, objHashCode, animationClockLocked, currentVsyncTimeMillis, lastReportedTimeMillis));

        } catch (Throwable e) {
            Log.e(TAG, tag + " -> Failed to reflect AnimationState: " + e.getMessage(), e);
        }
    }
}

BounceRunnable 插桩点

java 复制代码
protected class BounceRunnable implements Runnable {

    BounceRunnable(float velocity, int smoothDistance) {
        // ...
        // T0:记录当前物理时间与帧时钟快照(构造时处于帧锁死期内)
        Log.d("SRL_DEBUG", "[BounceRunnable Constructor] SystemClock.uptimeMillis() = " + SystemClock.uptimeMillis());
        mLastTime = AnimationUtils.currentAnimationTimeMillis();
        TestUtil.printAnimationState("BounceRunnable Constructor");
        mHandler.postDelayed(this, mFrameDelay);
    }

    @Override
    public void run() {
        if (animationRunnable == this && !mState.isFinishing) {
            // T1:记录 run() 执行时的物理时间与帧时钟快照(此时已在非锁死期)
            Log.d("SRL_DEBUG", "[BounceRunnable run]" + " SystemClock.uptimeMillis() = " + SystemClock.uptimeMillis());
            long now = AnimationUtils.currentAnimationTimeMillis();
            com.scwang.smart.refresh.layout.util.TestUtil.printAnimationState("BounceRunnable Run");
            float t = 1f * (now - mLastTime) / 1000;
            float velocity = mVelocity * t;
            if (now < mLastTime) {
                android.util.Log.e("SRL_DEBUG", "Detected Time Rewinds in BounceRunnable! now(" + now + ") < mLastTime(" + mLastTime + "), time step t = " + t + ", velocity = " + velocity);
            }
            // ...
        }
    }
}

④ 捕获的实测日志

less 复制代码
// T0 时刻:20:20:36.188,Traversal 重绘期间(帧锁死期,locked: true)
D/SRL_DEBUG: [BounceRunnable Constructor] SystemClock.uptimeMillis() = 49392919
E/SRL_DEBUG: [BounceRunnable Constructor] AnimationState (Obj: @87d2f05) -> locked: true, currentVsync: 49392943, lastReported: 49392895

// T1 时刻:20:20:36.200,第 1 帧 run()(帧已结束,locked: false)------ 时钟逆序
D/SRL_DEBUG: [BounceRunnable run] SystemClock.uptimeMillis() = 49392930
E/SRL_DEBUG: [BounceRunnable Run] AnimationState (Obj: @87d2f05) -> locked: false, currentVsync: 49392943, lastReported: 49392931
E/SRL_DEBUG: Detected Time Rewinds in BounceRunnable! now(49392931) < mLastTime(49392943), time step t = -0.012, velocity = -165.51784

// T2 时刻:20:20:36.230,第 2 帧 run()(步长恢复正常)
D/SRL_DEBUG: [BounceRunnable run] SystemClock.uptimeMillis() = 49392961
E/SRL_DEBUG: [BounceRunnable Run] AnimationState (Obj: @87d2f05) -> locked: false, currentVsync: 49392954, lastReported: 49392961

// T3 时刻:20:20:36.240,第 3 帧 run()
D/SRL_DEBUG: [BounceRunnable run] SystemClock.uptimeMillis() = 49392971
E/SRL_DEBUG: [BounceRunnable Run] AnimationState (Obj: @87d2f05) -> locked: false, currentVsync: 49392965, lastReported: 49392971

// T4 时刻:20:20:36.251,第 4 帧 run()
D/SRL_DEBUG: [BounceRunnable run] SystemClock.uptimeMillis() = 49392982
E/SRL_DEBUG: [BounceRunnable Run] AnimationState (Obj: @87d2f05) -> locked: false, currentVsync: 49392976, lastReported: 49392982

// T5 时刻:20:20:36.261,第 5 帧 run()
D/SRL_DEBUG: [BounceRunnable run] SystemClock.uptimeMillis() = 49392992
E/SRL_DEBUG: [BounceRunnable Run] AnimationState (Obj: @87d2f05) -> locked: false, currentVsync: 49392987, lastReported: 49392992

// T6 时刻:20:20:36.271,第 6 帧 run()
D/SRL_DEBUG: [BounceRunnable run] SystemClock.uptimeMillis() = 49393002
E/SRL_DEBUG: [BounceRunnable Run] AnimationState (Obj: @87d2f05) -> locked: false, currentVsync: 49392998, lastReported: 49393002

// 20:20:36.504,onLoadMore 被触发(构造后约 316ms,即 mReboundDuration 延迟后回调)
D/SRL_DEBUG: onLoadMore

同一 AnimationState 实例的确认 所有帧日志中 Obj: @87d2f05 对象哈希完全一致,确认 T0~T6 全程操作的是同一主线程的同一 AnimationState 实例,排除了 ThreadLocal 跨线程隔离或对象重新创建的可能。

数据解读:

  1. mLastTime = 49392943 ms (Vsync 帧时钟域,T0 时刻) : T0 时刻,主线程处于 Vsync 渲染锁死期(locked: true)。当前物理时间为 49392919 ms,帧时钟被 Choreographer 前置锁定为期望显示时间 49392943 ms(提前约 24ms)。 currentAnimationTimeMillis() 返回 Math.max(49392943, 49392895) = 49392943mLastTime 锚定此值。由于锁死期内只读不写,state.lastReported 仍停留在 49392895,帧时钟值未被写回 ThreadLocal。
  2. now = 49392931 ms (实时物理时钟域,T1 时刻) : T1 时刻(约 12ms 后),Handler 延迟消息到期,第 1 帧 run() 被调度。时钟已解锁(locked: false),currentAnimationTimeMillis() 直接返回 SystemClock.uptimeMillis() = 49392931 ms
  3. 第 1 帧:跨时钟域差值导致时间步长为负mLastTime 来自帧时钟域(T0),now 来自实时物理时钟域(T1),尽管物理时间向前流逝了约 12ms,帧时钟的 24ms 提前量仍使差值为负: now (49392931)<mLastTime (49392943)now \text{ (49392931)} < mLastTime \text{ (49392943)} now (49392931)<mLastTime (49392943) t= 49392931−493929431000 =−0.012 秒 t = \frac{49392931 - 49392943}{1000} = -0.012 \text{ 秒} t=100049392931−49392943=−0.012 秒 负步长使速度反转,velocity = mVelocity * t = -165.5moveSpinnerInfinitely 接收到负偏移,触发自动加载误判。
  4. 第 2~6 帧:步长恢复正常,但误判已产生 : T2 起时钟完全处于非锁死期,now 持续单调增长,不再出现 now < mLastTime。但第 1 帧的负位移已写入 mOffset 并触发状态机,后续正常帧无法撤销这一误判。

关键结论:帧时钟提前量仅需超过 Handler 调度延迟即可触发该缺陷。本次实测帧时钟提前 24ms,物理延迟约 12ms, t = -0.012s ,产生 velocity = -165.5 (远超 |velocity| ≥ 1 的阈值),最终在约 316ms 后触发 onLoadMore 。该缺陷在任何存在帧调度抖动的设备上均可复现。

2.2.2 Vsync 调度与 Handler 消息的时序交错示意图

以下时序图还原了硬件 Vsync 调度与 Handler 延迟消息在主线程中的调度交错过程,精确到毫秒级:

三、 缺陷流转与全链路剖析

整个 Bug 的流转逻辑横跨 Nested Scrolling 手势判定、Scroller 越界检测、Runnable 动画计算、及自动加载状态机判定。

3.1 链路一:惯性越界捕捉

当惯性滑动(Fling)或手势向下拉导致列表触顶/触底时,系统回调 computeScroll 方法检测越界:

scss 复制代码
@Override
public void computeScroll() {
    int lastCurY = mScroller.getCurrY();
    if (mScroller.computeScrollOffset()) {
        int finalY = mScroller.getFinalY();
        // finalY < 0:向下拉越界(触顶);finalY > 0:向上拉越界(触底)
        if ((finalY < 0 && (mEnableRefresh || mEnableOverScrollDrag) && mRefreshContent.canRefresh())
                || (finalY > 0 && (mEnableLoadMore || mEnableOverScrollDrag) && mRefreshContent.canLoadMore())) {
            if (mVerticalPermit) {
                float velocity;
                // 根据越界方向取正/负速度:触底时取反,触顶时保持正值
                velocity = finalY > 0 ? -mScroller.getCurrVelocity() : mScroller.getCurrVelocity();
                animSpinnerBounce(velocity); // 派发越界回弹
            }
            mScroller.forceFinished(true); // 强行中止 Scroller 惯性
        } else {
            mVerticalPermit = true; // 打开竖直通行证
            final View thisView = this;
            thisView.invalidate();
        }
    }
}

3.2 链路二:动画创建与时钟锚定

animSpinnerBounce 根据速度方向和当前状态创建对应的回弹任务:

ini 复制代码
protected void animSpinnerBounce(final float velocity) {
    if (reboundAnimator == null) {
        if (velocity > 0 && (mState == RefreshState.Refreshing || mState == RefreshState.TwoLevel)) {
            // 正在刷新时触顶越界:回弹到 HeaderHeight
            animationRunnable = new BounceRunnable(velocity, mHeaderHeight);
        } else if (velocity < 0 && (mState == RefreshState.Loading
                || (mEnableFooterFollowWhenNoMoreData && mFooterNoMoreData && mFooterNoMoreDataEffective && isEnableRefreshOrLoadMore(mEnableLoadMore))
                || (mEnableAutoLoadMore && !mFooterNoMoreData && isEnableRefreshOrLoadMore(mEnableLoadMore) && mState != RefreshState.Refreshing))) {
            // 正在加载 / 自动加载模式触底越界:回弹到 -FooterHeight
            animationRunnable = new BounceRunnable(velocity, -mFooterHeight);
        } else if (mSpinner == 0 && mEnableOverScrollBounce) {
            // 普通越界回弹(非刷新/加载状态):smoothDistance = 0,往外拉再滑回原点
            animationRunnable = new BounceRunnable(velocity, 0);
        }
    }
}

进入 BounceRunnable 的构造方法进行时钟锚定:

ini 复制代码
BounceRunnable(float velocity, int smoothDistance) {
    mVelocity = velocity;
    mSmoothDistance = smoothDistance;
    mLastTime = AnimationUtils.currentAnimationTimeMillis(); // T0 时刻:处于帧锁死期内,读取到 Vsync 帧时钟值 (49392943ms)
    mHandler.postDelayed(this, mFrameDelay); // 10ms 后开始第一帧 run() 调度
    if (velocity > 0) {
        mKernel.setState(RefreshState.PullDownToRefresh);
    } else {
        mKernel.setState(RefreshState.PullUpToLoad);
    }
}

3.3 链路三:帧计算与速度逆转

10ms 后,Handler 延迟消息到期,主线程 Looper 调度执行 BounceRunnable.run() 的第一帧(此时帧渲染已结束,时钟处于非锁死期):

scss 复制代码
// SmartRefreshLayout.java:1564
@Override
public void run() {
    if (animationRunnable == this && !mState.isFinishing) {
        // 1. 根据当前位移与目标距离,选择不同的阻尼衰减系数
        if (Math.abs(mSpinner) >= Math.abs(mSmoothDistance)) {
            if (mSmoothDistance != 0) {
                mVelocity *= Math.pow(0.45f, ++mFrame * 2); // 刷新/加载时回弹:强衰减
            } else {
                mVelocity *= Math.pow(0.85f, ++mFrame * 2); // 普通越界回弹:中等衰减
            }
        } else {
            mVelocity *= Math.pow(0.95f, ++mFrame * 2);     // 平滑滚动阶段:弱衰减
        }
        
        // 2. 读取实时物理时钟。T1 时刻:now (49392931ms) < mLastTime (49392943ms),步长为负
        long now = AnimationUtils.currentAnimationTimeMillis();
        float t = 1f * (now - mLastTime) / 1000; // 核心隐患:计算出 t = -0.012f
        
        // 3. 计算本帧位移步长,负步长导致速度方向反转
        float velocity = mVelocity * t;
        
        if (Math.abs(velocity) >= 1) {
            mLastTime = now;
            mOffset += velocity;                  // 位移累加,此处 mOffset 变为负值
            moveSpinnerInfinitely(mOffset);       // 传入负数偏移
            mHandler.postDelayed(this, mFrameDelay);
        } else {
            // 速度衰减至不足 1 时,停止动画并进行状态清理
            if (mViceState.isDragging && mViceState.isHeader) {
                mKernel.setState(RefreshState.PullDownCanceled);
            } else if (mViceState.isDragging && mViceState.isFooter) {
                mKernel.setState(RefreshState.PullUpCanceled);
            }
            animationRunnable = null;
            if (Math.abs(mSpinner) >= Math.abs(mSmoothDistance)) {
                int duration = 10 * Math.min(Math.max((int) SmartUtil.px2dp(Math.abs(mSpinner - mSmoothDistance)), 30), 100);
                animSpinner(mSmoothDistance, 0, mReboundInterpolator, duration);
            }
        }
    }
}

3.4 链路四:异常边界与加载误触

moveSpinnerInfinitely 负责根据偏移量执行阻尼位移,并在末尾判断自动加载条件。以下展示与本缺陷直接相关的两个关键部分(省略了 TwoLevel、Refreshing 等无关分支的阻尼计算):

java 复制代码
protected void moveSpinnerInfinitely(float spinner) {
    final View thisView = this;
    // ... 省略嵌套滚动边界判定和阻尼位移计算(5 个分支:TwoLevel/Refreshing/Loading/正向/负向)
    // 核心逻辑:根据 spinner 的正/负及当前状态,经阻尼公式 y = M(1-100^(-x/H)) 计算实际位移
    // 最终调用 mKernel.moveSpinner((int) y, true) 执行 UI 位移

    // ↓↓↓ 自动加载判定(与本缺陷直接相关)↓↓↓
    if (mEnableAutoLoadMore && !mFooterNoMoreData && isEnableRefreshOrLoadMore(mEnableLoadMore) && spinner < 0
            && mState != RefreshState.Refreshing
            && mState != RefreshState.Loading
            && mState != RefreshState.LoadFinish) {
        // 缺陷点:仅凭 spinner < 0 即判定为上拉露出 Footer,未校验位移来源
        if (mDisableContentWhenLoading) {
            animationRunnable = null;
            mKernel.animSpinner(-mFooterHeight);
        }
        setStateDirectLoading(false); // 直接进入 Loading 状态
        // 延迟 mReboundDuration 后触发 onLoadMore 回调
        mHandler.postDelayed(() -> {
            if (mLoadMoreListener != null) {
                mLoadMoreListener.onLoadMore(SmartRefreshLayout.this); // 误触发!
            } else if (mOnMultiListener == null) {
                finishLoadMore(2000);
            }
            final OnLoadMoreListener listener = mOnMultiListener;
            if (listener != null) {
                listener.onLoadMore(SmartRefreshLayout.this);
            }
        }, mReboundDuration);
    }
}

四、 防御性修复方案

在物理公式层面切断负步长引起的速度方向逆转,可从源头杜绝该缺陷在卡顿掉帧场景下的发生。

4.1 修复 BounceRunnable

防线在 BounceRunnable.run() 方法内,对计算得到的时间增量进行 Math.max(0, ...) 防御性截断,确保步长时间自变量绝对不为负:

ini 复制代码
-long now = AnimationUtils.currentAnimationTimeMillis();
-float t = 1f * (now - mLastTime) / 1000;
+long now = AnimationUtils.currentAnimationTimeMillis();
+float t = Math.max(0, 1f * (now - mLastTime) / 1000); // 强限制时间差不能小于 0
 float velocity = mVelocity * t;

4.2 修复 FlingRunnable

同样在惯性滑动任务 FlingRunnable.run() 中进行时间跨度截断拦截:

ini 复制代码
-long now = AnimationUtils.currentAnimationTimeMillis();
-long span = now - mLastTime;
+long now = AnimationUtils.currentAnimationTimeMillis();
+long span = Math.max(0, now - mLastTime); // 强限制时间跨度不能小于 0

通过此修复,当系统卡顿导致 now < mLastTime(时钟逆序)时,时间步长被截断为 0,位移 mOffset 保持不变,从根本上阻止了负值位移的产生,从而消除该缺陷在帧时序异常场景下的复现。

相关推荐
问心无愧051313 小时前
ctf show web入门259
android·前端·笔记
_李小白14 小时前
【android opencv学习笔记】Day 25: GrabCut 前景提取
android·opencv·学习
Kapaseker14 小时前
Kotlin 的扩展没有你看上去的那么简单
android·kotlin
一颗宁檬不酸14 小时前
Android多线程实现方式
android
黄林晴14 小时前
告别 KMP 选型地狱!klibs.io 上线,全平台库一键筛选太省心
android·kotlin
cyw899814 小时前
m3e向量化mysql某表
android·数据库·mysql
索西引擎14 小时前
【LangChain 1.0】接入 DeepSeek API:从 API Key 申请到流式响应的完整实践
android·java·langchain
山峰哥14 小时前
索引策略与SQL优化:从Explain对比到生产调优的完整方法论
android·java·数据库·sql·性能优化·深度优先
二蛋和他的大花14 小时前
高德地图 Flutter 插件:跨 Android / iOS / HarmonyOS 的完整实现
android·flutter·ios