關於 UsageStatsManager.queryUsageStats 的注意事項及 UsageStatsService 的簡單原理
問題背景:
目前需求統計應用的當天使用情況,在 5.0 以上有許可權 android.permission.PACKAGE_USAGE_STATS
,獲取到該許可權後可以通過 UsageStatsManager.queryUsageStats(int intervalType, long beginTime, long endTime)
方法查詢到應用的使用情況。
問題描述:
第一天下午使用一些應用後,可以查詢到相應應用的當天使用記錄。第二天早上七點開啟手機,在確定未使用這些應用的情況下,還是查詢到了這些應用的當天使用記錄。因為查詢的是當天使用記錄,所以在過去了一個自然日後,應該查詢不到應用的使用記錄才對。
問題分析:
queryUsageStats
有三個引數,分別是 intervalType
、beginTime
和 endTime
。先說說 beginTime
和 endTime
,它們分別表示待查詢時間範圍的起始時間和結束時間。而 intervalType
表示時間間隔的型別,它有5個值:
INTERVAL_DAILY
:天儲存級別的資料;INTERVAL_WEEKLY
:星期儲存級別的資料;INTERVAL_MONTHLY
:月儲存級別的資料;INTERVAL_YEARLY
:年儲存級別的資料;INTERVAL_BEST
:根據給定時間範圍選取最佳時間間隔型別。
既然有了待查詢時間範圍,那麼時間型別又有什麼用呢,這就是導致上述問題發生的原因了。測試中查詢的時間範圍預設是當天零點到當前時間,而時間型別用的是 INTERVAL_DAILY
。在實際查詢結果中發現查詢到的使用記錄並不是按給定時間範圍來的,而是從前一天的某個時間點開始的。
我們先看下 queryUsageStats
返回的資料,它是一個 UsageStats
型別的資料集合,其中有幾個關鍵欄位:
mBeginTimeStamp
:查詢範圍的起始時間;mEndTimeStamp
:查詢範圍的起結束時間;mLastTimeUsed
:應用最後一次使用結束時的時間;mTotalTimeInForeground
:查詢範圍內應用在前臺的累積時長;mLaunchCount
:查詢範圍內應用的開啟次數。
進一步分析查詢結果,發現其中的 mBeginTimeStamp
和 mEndTimeStamp
並不是傳入的時間範圍,而是和 intervalType
有關,比如在 INTERVAL_DAILY
情況下進行如下測試:
查詢時間範圍 2018-10-31 00:00:00
到 2018-10-31 18:00:00
,查詢結果中 mBeginTimeStamp
和 mEndTimeStamp
分別為 2018-10-30 08:00:00
和 2018-10-31 07:59:59
。
查詢時間範圍 2018-10-30 10:00:00
到 2018-10-31 18:00:00
,查詢結果中 mBeginTimeStamp
和 mEndTimeStamp
分別為 2018-10-30 08:00:00
和 2018-10-31 07:59:59
。
查詢時間範圍 2018-10-31 10:00:00
到 2018-10-31 18:00:00
,查詢結果中 mBeginTimeStamp
和 mEndTimeStamp
分別為 2018-10-31 08:00:00
和 2018-10-31 19:17:32
(當前系統時間)。
從以上3次測試結果可以看出,beginTime
、endTime
與 mBeginTimeStamp
、mEndTimeStamp
沒有顯而易見的關係。我們再看一下 queryUsageStats
引數的註釋:
通過註釋可以理解 beginTime
會包含在查詢結果的時間範圍內,即:
beginTime >= mBeginTimeStamp && beginTime <= mEndTimeStamp
綜上可以得出結論,queryUsageStats
的查詢範圍依賴 intervalType
而定,比如 INTERVAL_DAILY
型別會查詢一天內的使用情況,並且根據傳入的 beginTime
計算出該天的起始和結束時間。這裡的天並非自然日的概念,而是受時區影響的一天,即北京時間(東八區)從 08:00:00 開始算一天,但在少數機型上也發現從 00:00:00 開始算一天。不過可以肯定的是 mBeginTimeStamp
會受到時區的影響,比如當前是東八區,修改到東十區,一天的起始時間會相應增加2小時。
解決方案:
因為是系統的 api,查詢的時間段無法調整到更精確的範圍,只能考慮增加對 mLastTimeUsed
的判斷,這個欄位是應用最後一次使用結束時的時間,可以判斷 mLastTimeUsed
是不是在 beginTime
和 endTime
之間來確定使用者當天是否有使用過應用。
但是如果想判斷使用者當天是否有使用應用達到一定時長,就不能很準確的判斷了。因為 mTotalTimeInForeground
統計的是 mBeginTimeStamp
到 mEndTimeStamp
期間應用在前臺的累計時長,所以即使結合 mLastTimeUsed
判斷出使用者當天有開啟過應用,但還是無法準確得知當天開啟應用後的使用時長。
拓展閱讀:
在測試期間還發現一個問題:先使用某個應用一定時間,查詢出當天的使用記錄,然後修改手機系統時間到一星期後,查詢發現當天仍有使用記錄,且兩次查詢結果除日期變化外其餘都相同。猜測使用記錄是不是以一種相對時間的方式儲存在系統中的,當查詢時會根據當前時間計算出具體時間戳,所以進一步研究驗證。
這裡就不得不提一下 UsageStatsService
了,UsageStatsManager.queryUsageStats
實際上是通過 UsageStatsService.queryUsageStats
進行查詢的。UsageStatsService
是一個系統服務,其主要通過 AMS 等來收集、聚合和保留應用程式使用資料,可以通過 AppOps 授予應用程式許可權來查詢此資料。
UsageStatsService
記錄的應用行為事件全部定義在 UsageEvents
類中,在 UsageStats
中也有欄位 mLastEvent
記錄了應用的最後一次行為。這裡說一下幾個常見的事件:
-
MOVE_TO_FOREGROUND
:當 ActivityonResume
時,ActivityManagerService
會呼叫UsageStatsService.noteResumeComponent
方法記錄下此事件,標記應用切換到前臺。 -
MOVE_TO_BACKGROUND
:當 ActivityonPause
時,ActivityManagerService
會呼叫UsageStatsService.notePauseComponent
方法記錄下此事件,標記應用切換到後臺。
UsageStatsService
在接收到事件變化時會通過 writeStatsToFile
方法將資料持久化成檔案,通過這裡可以得知 UsageStatsService
是通過檔案來儲存應用的使用資料的。
那麼檔案又是如何被管理的呢?這裡需要引出 UsageStatsDatabase
,通過它將資料持久化儲存在了 XML 檔案中,並提供從 XML 資料庫查詢 UsageStats
資料的介面,XML 檔案儲存在 /data/system/usagestats/
路徑下。資料目錄按 daily、monthly、weekly、yearly 四個資料夾儲存。XML 的所有操作,例如讀、寫等,都被封裝在 UsageStatsXmlV1
中,由 UsageStatsDatabase
進行呼叫。
關於 UsageStatsService
的資料儲存,還有很重要的一點需要提到,那就是快取。UsageStatsService
在每次啟動時,都會先按照 user 生成各個 UserUsageStatsService
,其中每個物件都會先去各自的檔案路徑下讀取資料到記憶體中。此後每次外界 reportEvent
時,都會先更新記憶體中的資料,相當於快取。那麼什麼時候會把記憶體中的資料更新到檔案中呢?主要有以下幾種情況:
- 手機關機
- 系統時間跳變
- 一天結束時
從這裡引出了我們一開始提到的問題,在修改系統時間後,使用記錄為什麼會跟隨日期變化。
首先,手機系統時間會發生跳變,常見形式有人為修改時間或系統通過網路自動校準。比如,手機第一次使用,未聯網校準時,手機時間是錯誤的,可能顯示為1970年1月1日,這時候產生的應用使用資料會被記錄為1970年1月1日。但手機聯網後,時間通過網路校準為2018年11月1日。那麼 UsageStatsService
中統計的時間會仍然為1970年1月1日嗎?Google 早在設計之初就考慮到了這點,在 UsageStatsService
中有一個巧妙的機制來保證記錄時間的準確性。
UsageStatsService
中有一個 checkAndGetTimeLocked
方法,此方法會在每次 reportEvent
或 queryUsageStats
時會去檢查系統時間。
通過程式碼可以看出,checkAndGetTimeLocked
會計算出當前系統時間與預期系統時間之間的差值,當這個差值大於 TIME_CHANGE_THRESHOLD_MILLIS
(2s)時,UsageStatsService
會呼叫 onTimeChanged
方法,它會負責更新 UsageStatsService
記錄的時間,以便它們能夠跟隨系統時間跳變而相應更新。這就是直接導致開始提到的修改手機系統時間後,查詢發現當天仍有使用記錄,且前後兩次查詢結果除日期變化外其餘都相同的原因。
既然使用記錄的時間會隨系統時間修改而變動,那麼它是否如猜測一般是以一種相對時間的形式儲存的呢?首先,我們來看下 daily 資料夾:
從上圖中可以看出 XML 檔案是以當天日期起點的時間戳命名的。如 1541030400000 即 2018-11-01 08:00:00。其次,在 daily 資料夾下,XML 檔案是以天級別儲存的,每天會新建一個檔案。這裡也可以聯想到之前分析的 UsageStatsManager.queryUsageStats
不會根據傳入的時間範圍精確查詢,而是以天級別查詢的,其實就是讀取分析了當天的 XML 檔案。且檔案命名中的時間是受時區影響的,也就導致了 daily 級別的查詢結果不是按自然日來的。接下來看一下 XML 檔案的具體內容:
從檔案中可以看出,使用記錄是以毫秒級別的差值進行儲存的,這個差值是以檔名中的時間點為錨定的。在查詢使用記錄時會結合兩者計算出確切的時間,比如,應用的上次使用時間 = XML 檔名 + XML 中此應用的上次使用時間。
通過以上機制,當系統時間跳變時,UsageStatsService
會通過 onTimeChanged
方法更新 XML 檔名中的時間,而 XML 中的資料並不需要變動即可隨著系統時間的跳變而保持準確值。