在项目开发的中经常会遇到使用Timer计时器的时候。例如:活动倒计时、定时隐藏View、计时停止播放等等。提到上述场景在脑海中浮现的往往是Timer+TimerTask;CountDownTimer;Handler。没错这些类都可以很好的完成计时任务,但是在一些场景中往往开发出来的计时器会被系统回收导致计时不准确。例如:Activity中添加了CountDownTimer计时器,Activity在前台的时候计时器可以正常运行,但放置后台一段时间会发现计时器无法继续回调。我相信很多朋友都遇到过这样的问题,并为之烦恼良久。
为了解决计时器放置后台导致无法正常计时的问题。首先要考虑的是如何将计时器放置后台不会被系统GC掉,在Android中不能被系统回收的首选组件当然是Service服务。为什么选择Service不过多描述,不明白的小伙伴可以参考官方文档:服务概览 | Android 开发者 | Android Developers
计时器中需要有定时迭代的对象来计算时间。这个重任就落在了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。
可能有的朋友会有疑问为什么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
)
OnTimerToCallListener接口中包含三个方法。
以上时间单位都是秒。可以根据实际情况该为毫秒。
在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完整代码下载地址如下:https://download.csdn.net/download/laowu119119/86937504
如何使用TimerService:可以根据实际情况在Activity的onCreate中开启服务。onDesttory中关闭服务。通过比绑定服务捆绑需要计时的Activity页面获取TimerService对象,在通过TimerService对象调用addTimerBean方法和removeTimerBean方法添加移除需要计时的实体。