handler实现精确计时的两种方式

时间:2024-04-05 08:42:27

首先说下关于handler自身的误差:

  如果使用handler.postDealyed(……, 1000)方式来进行每秒的计时,是不准确的,是的,有很大误差,误差的原因在于在你收到消息,到你重新发出handler.postDealyed的时间,并不是瞬间完成的,这里面有很多逻辑处理的时间,即使没有逻辑处理的时间,handler本身也是耗损性能的,所以消息并不可能按照理想的1000延迟来进行发送,这就导致了误差的累积。

下面我们介绍的handler精确计时的两种方式分别来自Android源码的两个控件:TextClock和CountDownTimer:

第一种方式是TextClock对于handler的时间修正:

    private final Runnable mTicker = new Runnable() {
        public void run() {
            if (mStopTicking) {
                return; // Test disabled the clock ticks
            }
            onTimeChanged();

            long now = SystemClock.uptimeMillis();
            long next = now + (1000 - now % 1000);

            getHandler().postAtTime(mTicker, next);
        }
    };

我们之前是通过postDelay来触发消息事件的,但这里系统使用了postAtTime,这样就是设置了在某一个时间点抛出handler消息,前面的

long now = SystemClock.uptimeMillis();
long next = now + (1000 - now % 1000);

精确控制了1秒的整点时间,因此系统可以在每一个整秒的时间点发出消息。

代码如下:

mCalHandler.post(mTicker);
    /**
     * 精确修正时间
     */
    private Handler mCalHandler = new Handler(Looper.getMainLooper());
    private final Runnable mTicker = new Runnable() {
        public void run() {
            long now = SystemClock.uptimeMillis();
            long next = now + (1000 - now % 1000);

            mCalHandler.postAtTime(mTicker, next);
            Log.e("buder", now + "");
        }
    };

看一下效果,基本误差都在2毫秒内

handler实现精确计时的两种方式

参考博客:https://www.jianshu.com/p/56f37988428b

第二种方式我们可以直接参考CountDowntimer的源码:

注意CountdownTimer本身也是不准确的,需要我们手动补偿误差时间,也是我们修改的核心点

public abstract class CountDownTimer {

    /**
     * Millis since epoch when alarm should stop.
     */
    private final long mMillisInFuture;

    /**
     * The interval in millis that the user receives callbacks
     */
    private final long mCountdownInterval;

    private long mStopTimeInFuture;
    
    /**
    * boolean representing if the timer was cancelled
    */
    private boolean mCancelled = false;

    /**
     * @param millisInFuture The number of millis in the future from the call
     *   to {@link #start()} until the countdown is done and {@link #onFinish()}
     *   is called.
     * @param countDownInterval The interval along the way to receive
     *   {@link #onTick(long)} callbacks.
     */
    public CountDownTimer(long millisInFuture, long countDownInterval) {
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;
    }

    /**
     * Cancel the countdown.
     */
    public synchronized final void cancel() {
        mCancelled = true;
        mHandler.removeMessages(MSG);
    }

    /**
     * Start the countdown.
     */
    public synchronized final CountDownTimer start() {
        mCancelled = false;
        if (mMillisInFuture <= 0) {
            onFinish();
            return this;
        }
        mStopTimeInFuture = SystemClock.elapsedRealtime() + mMillisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }


    /**
     * Callback fired on regular interval.
     * @param millisUntilFinished The amount of time until finished.
     */
    public abstract void onTick(long millisUntilFinished);

    /**
     * Callback fired when the time is up.
     */
    public abstract void onFinish();


    private static final int MSG = 1;


    // handles counting down
    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            synchronized (CountDownTimer.this) {
                if (mCancelled) {
                    return;
                }

                final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();

                if (millisLeft <= 0) {
                    onFinish();
                } else {
                    long lastTickStart = SystemClock.elapsedRealtime();
                    onTick(millisLeft);

                    // take into account user's onTick taking time to execute
                    long lastTickDuration = SystemClock.elapsedRealtime() - lastTickStart;
                    long delay;

                    if (millisLeft < mCountdownInterval) {
                        // just delay until done
                        delay = millisLeft - lastTickDuration;

                        // special case: user's onTick took more than interval to
                        // complete, trigger onFinish without delay
                        if (delay < 0) delay = 0;
                    } else {
                        delay = mCountdownInterval - lastTickDuration;

                        // special case: user's onTick took more than interval to
                        // complete, skip to next interval
                        while (delay < 0) delay += mCountdownInterval;
                    }

                    sendMessageDelayed(obtainMessage(MSG), delay);
                }
            }
        }
    };
}

日常我们计时较短一般10分钟以内使用这个countdownTimer时,偶尔会遇到剩下最后一秒钟没有走完,这个还是因为里面的实现handler计时不准确导致的,下面代码是我们日常使用countDownTimer的示例:

countDownTimer.start();
/**
     * 第一个参数表示总时间,第二个参数表示间隔时间。意思就是每隔一秒会回调一次方法onTick,然后10秒之后会回调onFinish方法
     */
    private CountDownTimer countDownTimer = new CountDownTimer(1000 * 100, 1000) {
        @Override
        public void onTick(long millisUntilFinished) {
            //秒转化成 00:00形式一
//            timeView2.setText(formatTime1(millisUntilFinished) + "");

            timeView2.setText(formatTime2(millisUntilFinished / 1000));
            Log.e("hehehe ", millisUntilFinished + " ");
        }

        @Override
        public void onFinish() {

        }
    };

看下结果:

handler实现精确计时的两种方式

很明显已经有十几毫秒的误差了。

时间不到零或者有误差该怎么办?很简单,就是认为的添加上误差的毫秒数,只要不超过1000就可以,例如:

handler实现精确计时的两种方式

或者直接修改countdownTimer源码的构造函数:

     /**
     * @param millisInFuture    总倒计时时间
     * @param countDownInterval 倒计时间隔时间
     */
    public CustomCountDownTimer(long millisInFuture, long countDownInterval) {
        // 解决秒数有时会一开始就减去了2秒问题(如10秒总数的,刚开始就8999,然后没有不会显示9秒,直接到8秒)
        if (countDownInterval > 1000) millisInFuture += 15;
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;
    }

millisInFuture += 15;

其实就是补充上误差的几毫秒,让起始时间不产生跳变2秒的问题。当然上面的900毫秒还是在构造函数里的15毫秒都是需要根据总计时数来动态调整的。

相关文章