在项目开发的中经常会遇到使用Timer计时器的时候。例如:活动倒计时、定时隐藏View、计时停止播放等等。提到上述场景在脑海中浮现的往往是Timer+TimerTask;CountDownTimer;Handler。没错这些类都可以很好的完成计时任务,但是在一些场景中往往开发出来的计时器会被系统回收导致计时不准确。例如:Activity中添加了CountDownTimer计时器,Activity在前台的时候计时器可以正常运行,但放置后台一段时间会发现计时器无法继续回调。我相信很多朋友都遇到过这样的问题,并为之烦恼良久。
为了解决计时器放置后台导致无法正常计时的问题。首先要考虑的是如何将计时器放置后台不会被系统GC掉,在Android中不能被系统回收的首选组件当然是Service服务。
计时器中需要有定时迭代的对象来计算时间。这个重任就落在了Handler身上。Handler中有设置间隔时间发送Message消息的sendEmptyMessageDelayed方法。可以通过它间隔一段时间计算一次时间。代码如下:
@SuppressLint("HandlerLeak")
inner class TimerHandler : Handler() {
var isRunning: Boolean = false
var isCallback: Boolean = false
@SuppressLint("SuspiciousIndentation")
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
//系统时间
currentSystemTime = SystemClock.elapsedRealtime() / 1000L
//计算回调
if (isCallback) {
updateShowTime()
}
if (isRunning) {
timerHandler.sendEmptyMessageDelayed(0, 250)
}
}
}
通过代码看到在TimerHandler中还有两个成员对象:isRunning、isCallback。
- isRunning:用于控制Handler是否能够定时发送Message效果继续迭代。
- isCallback:用于控制计时器的计算和回调分发
可能有的朋友会有疑问为什么sendEmptyMessageDelayed要间隔250毫秒发送一次信息,而不是1秒钟发送一次。因为Handler发送Message消息的时候内部是通过Looper进行轮训迭代的如果1秒钟发送一次不能保证接收Message的时机就是1秒钟。感兴趣的通过可以通过日志看一下,往往接收到Message的时候都有几十毫秒的无法。随意这里的Handler只是起到了一个迭代的作用。
具体时间的计算是通过updateShowTime方法实现,代码如下:
/**
* 更新显示时间
*/
private fun updateShowTime() {
try {
beans.forEach { bean ->
//当前与服务器校准时间
/*
*-----------------------------------------------------------------------------------
* | currentTime | serviceTime | currentSystemTime | systemTime
* | 参数: 81.8秒 20.0秒 71.9秒 10.1秒
* | A 81秒 = 20.0秒 + 71秒(向下取整) - 10秒(向下取整)
* | B 81秒 = 20.0秒 + 72秒(向上取整) - 11秒(向上取整)
* | C 80秒 = 20.0秒 + 71秒(向下取整) - 11秒(向上取整)
* | D 82秒 = 20.0秒 + 72秒(向上取整) - 10秒(向下取整)
*-----------------------------------------------------------------------------------
* 最好使用C方案配置参数
* */
val currentTime = bean.serviceTime + currentSystemTime - bean.systemTime
val status = when {
currentTime < bean.startTime -> {
//未到活动开始时间
TimerStatus._TIMER_PREPARE
}
currentTime in bean.startTime..bean.endTime -> {
//活动时间中
TimerStatus._TIMER_RUNNING
}
else -> {
//活动结束
TimerStatus._TIMER_OVER
}
}
try {
when (status) {
TimerStatus.TIMER_PREPARE -> {
//校准时间与开始时间结果
val prepareTime = bean.startTime - currentTime
bean.listener?.updatePrepareTime(currentTime, prepareTime)
}
TimerStatus.TIMER_RUNNING -> {
//校准时间与开始时间结果
val runTime = currentTime - bean.startTime
//剩余时间结果
val remainingTime = bean.endTime - bean.startTime - runTime
//
bean.listener?.updateRunningTimer(currentTime, runTime, remainingTime)
}
else -> {
//校准时间与开始时间结果
val overTime = currentTime - bean.endTime
//
bean.listener?.updateOverTimer(currentTime, overTime)
}
}
}catch (e: Exception){
Log.i(mTag,"Timer_Callback_Exception:[${e.printStackTrace()}]")
}
}
} catch (e: Exception) {
// 循环异常不处理
Log.i(mTag, "Timer_Exception:[${e.printStackTrace()}]")
}
}
在updateShowTime方法中会循环获取需要计算计时的对象。该实体类如下:
/**
* 网络直播计时器对象
*/
data class TimerBean(
var serviceTime: Long = -2L,
var startTime: Long = -1L,
var endTime: Long = 0L,
var systemTime: Long = SystemClock.elapsedRealtime() / 1000L,
var listener: OnTimerToCallListener? = null
)
1. serviceTime:系统时间或者接口返回的服务器时间(秒)
2. startTime:开始时间
3. endTime:结束时间
4. systemTime:当前手机运行时间
5. listener:回调接口
OnTimerToCallListener接口中包含三个方法。
1. updatePrepareTime(calibrationTime: Long, prepareTime: Long):在startTime时间之前回调的准备方法。calibrationTime为校准时间;prepareTime为距离startTime节点的剩余时间
2. updateRunningTimer(calibrationTime: Long, runTime: Long, remainingTime: Long):在startTime和endTime之间的执行中方法。calibrationTime为校准时间;runTime为超过startTime的执行时间;remainingTime为距离endTime结束节点的剩余时间
3. updateOverTimer(calibrationTime: Long, overTime: Long):大于endTime时间节点的结束方法。calibrationTime为校准时间;overTime为大于endTime时间节点超出的时间。
以上时间单位都是秒。可以根据实际情况该为毫秒。
在updateShowTime中的currentTime是通过serviceTime加上手机运行时间获得。
val currentTime = bean.serviceTime + currentSystemTime - bean.systemTime
每次Handler执行handleMessage方法的时候都会获取一次新的手机运行时间赋值给currentSystemTime。例如:服务器时间(serviceTime)可能是1667955040(2022-11-09 08:50:40)获取服务时间的时候取得的手机运行时间systemTime假设是10秒在handleMessage迭代的时候收取的currentSystemTime时间假设为5010秒。通过上面的公式:1667955040 + 5010 - 10 = 1667960040(2022-11-09 10:14:00)就可以获得一个准的时间,在通过与startTime和endTime的比对就知道当前处于什么状态,调用对应的回到方法通知页面更新UI显示。
备注:除此还需要注意遍历集合处理并发问题(已处理)。在update中通过tye-catch的方法捕获了ArrayList执行next方法的异常,并未解决并发问题。因为Handle是每250毫秒发送一次Message所以可以不用处理。
如何使用TimerService:可以根据实际情况在Activity的onCreate中开启服务。onDesttory中关闭服务。通过比绑定服务捆绑需要计时的Activity页面获取TimerService对象,在通过TimerService对象调用addTimerBean方法和removeTimerBean方法添加移除需要计时的实体。