引言
在App使用中,由于系統時間用戶可以隨意更改,在某些特殊情況下會導致獲取到的系統時間不正確問題。本篇代碼使用dart語言進行相關描述。
1.問題分析:
手機系統時間 ≠ 真實時間,當我們做一些需要對時間精度和準確性要求較高的軟件時,如果只通過調用系統API,獲取到的時間不一定是真實的,那么就需要我們單獨去維護一個真實的時間,下面主要分析了連網情況下和斷網情況下兩種時間維護方案。
2.方案一:連網情況下獲取網絡時間:
既然連網了,從網絡獲取時間就行了,獲取到的時間與本地保存的時間同步,當然響應數據有一定的延時,如果想要特別精確再單獨加上響應時間就行了。
Dart代碼示例如下:
///獲取網絡時間Future<DateTime?> _getNetworkTime() async {try {final response = await HttpUtil.getInstance().get(apiUrl);if (response.statusCode == 200) {final Map<String, dynamic> responseData = json.decode(response.data);final String dateTimeString = responseData['datetime'];final DateTime networkTime = DateTime.parse(dateTimeString).toLocal();return networkTime;} else {print('Handle the response error here');return null;}} catch (e) {print('Handle any exceptions that may occur $e');return null;}}
3.方案二:斷網情況下本地時間維護:(重點)
既然斷網了,方案一就一點用沒了,此時有兩種方法獲取時間,一種是調用系統api,一種是獲取本地維護的時間,我們知道系統時間是可以修改的,所以你獲取系統時間的話,得到的不一定是正確的時間,那么只能從本地我們自己維護的時間去拿,那么問題來了本地時間要怎么去維護呢?
我們可以在應用啟動的時候起個定時器1秒鐘循環一次,對本地時間進行累加,不受系統時間影響,前提是本地第一次保存是時間必須是真實的時間系統時間不可靠,那么就把第一次從網絡上獲取的時間去保存下來供后續使用。需要注意的是這種情況下難免會有誤差!
time2 = Timer.periodic(const Duration(seconds: 1), (timer) {if (!isSpite) {CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + 1000);}});
上面的代碼是定時器一秒鐘循環一次,每次對上一次保存的時間進行累加,也就是我們自己維護的時間,這里的1000寫死了,細微誤差暫不考慮在內。
當然這只是開始,如果在你應用使用中突然斷網了,并且系統時間還被修改了,上面方法還可以維護一個真實的時間,那么如果斷網后,你的程序退出了,而且系統時間還被修改了,該去怎么去保證時間的真實性呢?
4.邏輯設計:
這里我使用的方法是,在上面saveRealTime 中我們每次保存的不單是真實時間,還保存的一個系統時間的毫秒數。
///保存一次真實時間及一次系統時間(兩次時間不一定相同)
static void saveRealTime(int timeMillis) {SpUtil.putInt('timeMillis', timeMillis);SpUtil.putInt('systemTimeMillis', DateTime.now().millisecondsSinceEpoch);}
當程序退出時,systemTimeMillis 保存了上次退出時的系統時間,當下次啟動應用時,獲取當前系統時間,與上次保存的系統時間之差就是這段空缺的時間段,然后繼續與本地的真實時間進行累加以達到獲取維護真實時間的目的。
if (currTime - CacheUtil.getLastSystemTime() > 2000) {//當前系統時間 大于上次保存的時間(斷網退出app時 保存的一次時間)//此處有個問題,用于修改時間大于當前時間(此時不用考慮)isSpite = false;var timeDiff = currTime - CacheUtil.getLastSystemTime();CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + timeDiff);}
上面方法只適用 系統時間被修改一次的情況,至于為什么要大于2000毫秒,是因為上面還有個一秒循環一次的定時器在維護時間,只有程序退出了這兩數之差才可能大于2000,因為這兩個定時器是并行的,為了避免小概率事件大于1000有時可能會有問題!
當用戶在程序退出后再斷網,然后修改系統時間,此時上述方法就無力回天了,只能禁用與時間相關的所有功能,待時間恢復正常(連網…)再恢復相關功能使用。
if (currTime - CacheUtil.getLastSystemTime() < 0) {//當前時間小于上次保存的時間,代表時間被惡意修改,禁用所有與時間相關的功能,// 直到連網或者修改系統時間大于當前時間isSpite = true;}
下面是完整代碼示例:
import 'dart:async';
import 'dart:convert';import '../../http/httpUtil.dart';
import '../cache.dart';///
/// 此類內部維護一個真實時間,由于系統時間可被修改,所以系統時間僅供參考
///
/// 用來獲取當前真實時間
///
const String apiUrl = 'https://worldtimeapi.org/api/ip';class RealTimeUtil {late Timer time;late Timer time2;//是否可以使用系統時間bool canUseSystem = false;//時間是否被惡意篡改bool isSpite = false;// 使用靜態變量_instance來存儲單例實例static RealTimeUtil? _instance;// 私有構造函數,防止外部實例化RealTimeUtil._() {init();}// 工廠構造函數,用于返回單例實例factory RealTimeUtil() {// 如果實例尚未初始化,則創建一個新實例_instance ??= RealTimeUtil._();return _instance!;}void init() async {_task();time = Timer.periodic(const Duration(seconds: 10), (timer) {_task();});time2 = Timer.periodic(const Duration(seconds: 1), (timer) {if (!isSpite) {CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + 1000);}});}_task() {_getNetworkTime().then((DateTime? dateTime) {if (dateTime != null) {//獲取網絡時間成功isSpite = false;if ((dateTime.millisecondsSinceEpoch - DateTime.now().millisecondsSinceEpoch).abs() > 5000) {//網絡時間大于系統時間5s 代表本地時間不正確, 5s是個閾值,視實際情況而定canUseSystem = false;CacheUtil.saveRealTime(dateTime.millisecondsSinceEpoch);} else {canUseSystem = true;CacheUtil.saveRealTime(DateTime.now().millisecondsSinceEpoch);}} else {//獲取網絡時間失敗var currTime = DateTime.now().millisecondsSinceEpoch;print('test 獲取網絡時間失敗 currTime = ${currTime - CacheUtil.getLastSystemTime()} isSpite = $isSpite');if (currTime - CacheUtil.getLastSystemTime() > 2000) {//當前系統時間 大于上次保存的時間(斷網退出app時 保存的一次時間)//此處有個問題,用于修改時間大于當前時間(此時不用考慮)isSpite = false;var timeDiff = currTime - CacheUtil.getLastSystemTime();CacheUtil.saveRealTime(CacheUtil.getLastRealTime() + timeDiff);} else if (currTime - CacheUtil.getLastSystemTime() < 0) {//當前時間小于上次保存的時間,代表時間被惡意修改,禁用所有與時間相關的功能,// 直到連網或者修改系統時間大于當前時間isSpite = true;}}});}///獲取真實時間的唯一路徑DateTime getRealTime() {int lastRealTimeMillis = CacheUtil.getLastRealTime();// 創建一個DateTime對象,需要將時間戳轉換為DateTimeDateTime realTime = DateTime.fromMillisecondsSinceEpoch(lastRealTimeMillis);return realTime;}///獲取網絡時間Future<DateTime?> _getNetworkTime() async {try {final response = await HttpUtil.getInstance().get(apiUrl);if (response.statusCode == 200) {final Map<String, dynamic> responseData = json.decode(response.data);final String dateTimeString = responseData['datetime'];final DateTime networkTime = DateTime.parse(dateTimeString).toLocal();return networkTime;} else {print('Handle the response error here');return null;}} catch (e) {print('Handle any exceptions that may occur $e');return null;}}
}
5.總結:
當然上述方法,是以系統時間為參考進行的補救措施,如果使用的是系統啟動時間,而不是系統時間為參考,上面問題就迎刃而解了。這里沒有嘗試該方法,我主要是給大家提供一個思路,而且博主能力有限,很多細節可能未考慮在內,大家有好的方法也可以提出來,共同學習,一起進步。