前言
當客戶端有一些業(yè)務邏輯需要獲取當前時間參與其中并起起到決定性作用時,System.currentTimeMillis()
將不再適用,因為當用戶手動更改為錯誤的日期和時間后,該方法獲取的就是用戶修改后的日期和時間,如果這時的業(yè)務需求為預訂服務或限時秒殺時,調用System.currentTimeMillis()
來獲取當前日期和時間就會出現非常嚴重的錯誤。今天我們就來通過另一種思路獲取當前日期和時間。
原理
通過任意網絡請求,獲取網絡請求頭中的時間信息,再利用Android設備的開機時長和從網絡請求頭中獲取到的時間戳來獲取手機開機時刻的時間點,之后在每次調用獲取時間的方法時只需要開機時刻的時間點加上開機時長即可。
知識點
1.SystemClock.elapsedRealtime()
返回系統啟動到現在的時間,包含設備深度休眠的時間。該時鐘被保證是單調的,即使CPU在省電模式下,該時間也會繼續(xù)計時。
2.Date頭域
http協議的通用頭域中包含的Date頭域,Date頭域表示消息發(fā)送的時間,時間的描述格式由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
/**
* 獲取當前的服務器時間
* 誤差范圍決定于最小的網絡響應時間
* 當還沒有發(fā)起任何網絡請求時,返回本地時間
*/
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
:在調用該方法時,會返回開機時刻的時間點加開機時長來得到當前的時間,這里需要注意一個情況,當未發(fā)生網絡請求時,還是會返回本地時間,所以在調用該方法前要保證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()))
}
}
}
})
}
}