前言
当客户端有一些业务逻辑需要获取当前时间参与其中并起起到决定性作用时,System.currentTimeMillis()
将不再适用,因为当用户手动更改为错误的日期和时间后,该方法获取的就是用户修改后的日期和时间,如果这时的业务需求为预订服务或限时秒杀时,调用System.currentTimeMillis()
来获取当前日期和时间就会出现非常严重的错误。今天我们就来通过另一种思路获取当前日期和时间。
原理
通过任意网络请求,获取网络请求头中的时间信息,再利用Android设备的开机时长和从网络请求头中获取到的时间戳来获取手机开机时刻的时间点,之后在每次调用获取时间的方法时只需要开机时刻的时间点加上开机时长即可。
知识点
1.SystemClock.elapsedRealtime()
返回系统启动到现在的时间,包含设备深度休眠的时间。该时钟被保证是单调的,即使CPU在省电模式下,该时间也会继续计时。
2.Date头域
http协议的通用头域中包含的Date头域,Date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date:Mon,31Dec200104:25:57GMT。Date描述的时间表示世界标准时。
3.OkHttp拦截器
需要在项目中集成OkHttp网络请求库,利用OkHttp的拦截器Interceptor的拦截功能拦截网络请求返回的Date头域。
实现
项目地址 https://github.com/kevin-mob/timecalibration
TimeUtil工具类
class TimeUtil {
companion object {
/* 手机开机时刻的时间点 */
private var bootStartUpTime: Long = -1
/**
* 获取当前的服务器时间
* 误差范围决定于最小的网络响应时间
* 当还没有发起任何网络请求时,返回本地时间
*/
fun getCurrentTimeMillis(): Long {
//开机时刻的时间+开机时长 = 当前的服务器时间
return if (bootStartUpTime == -1L) {
System.currentTimeMillis()
} else {
bootStartUpTime + SystemClock.elapsedRealtime()
}
}
/**
* 根据服务器时间更新手机开机时刻的时间
*/
fun updateTime(lastServiceTime: Long) {
bootStartUpTime = lastServiceTime - SystemClock.elapsedRealtime()
}
}
}
首先我们需要定义一个获取时间的工具类,包含获取时间和更新时间两个方法。
updateTime
:用于计算出手机的开机时刻,这个方法的实现很简单,就是Date头域获取时间后减去开机时长,并通过bootStartUpTime来记录这个值
getCurrentTimeMillis
:在调用该方法时,会返回开机时刻的时间点加开机时长来得到当前的时间,这里需要注意一个情况,当未发生网络请求时,还是会返回本地时间,所以在调用该方法前要保证APP有联网操作。
ServerTimeInterceptor OkHttp拦截器
class ServerTimeInterceptor : Interceptor {
/**
* 记录服务器请求最小响应时间
*/
var minRequestResponseTime = Long.MAX_VALUE
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val startTime = System.nanoTime()
val response = chain.proceed(request)
val requestResponseTime = System.nanoTime() - startTime
val headers: Headers? = response.headers()
calibrationTime(requestResponseTime, headers?.get("Date"))
return response
}
private fun calibrationTime(responseTime: Long, serverDateStr: String?) {
serverDateStr?.let {
// 本次响应时间小于最小响应时间,更新时间
if (responseTime < minRequestResponseTime) {
HttpDate.parse(serverDateStr)?.let {
TimeUtil.updateTime(it.time)
minRequestResponseTime = responseTime
}
}
}
}
}
自定义ServerTimeInterceptor拦截器,拦截response header中的Date头域,并记录一个响应时间,当之后再次拦截到网络请求时,对比本次和之前的响应时间,只有本次响应时间小于最小响应时间才更新时间,这样会使时间更加接近世界标准时。
使用
给OkHttp设置拦截器,并产生网络请求后,TimeUtil.getCurrentTimeMillis()
就可以获取当前时间了。
class MainActivity : AppCompatActivity() {
private val okHttpClient: OkHttpClient =
OkHttpClient.Builder().addInterceptor(ServerTimeInterceptor()).build()
private val request: Request = Request.Builder().url("https://www.xcc.cn/static/img/logo.aacc5cc.png").build()
private val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS", Locale.getDefault())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun requestNetwork(view: View) {
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call?, e: IOException) {
Log.d(TAG, "onFailure: ${e.message}")
System.currentTimeMillis()
}
@Throws(IOException::class)
override fun onResponse(call: Call?, response: Response) {
findViewById<TextView>(R.id.tv_hello)?.let {
runOnUiThread {
it.text = format.format(Date(TimeUtil.getCurrentTimeMillis()))
}
}
}
})
}
}