Android UsageStatsService:要點解析
1、UsageStatsService作用是什麼?
這是一個Android私有service,主要作用是收集使用者使用每一個APP的頻率、使用時常;
2、如何通過UsageStatsService獲取使用者使用APP的資料?
(1)必須要具備系統許可權;(APP內建在/system/app下)
(2)必須要在manifest中申明許可權:PACKAGE_USAGE_STATS;例如:
[html] view plain copy
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
(3)呼叫UsageStatsService.getAllPkgUsageStats or UsageStatsService.getPkgUsageStats 介面獲取使用者使用APP頻率:
[java] view plain copy
//相當於:IBinder oRemoteService = ServiceManager.getService("usagestats"); Class<?> cServiceManager = Class.forName("android.os.ServiceManager"); Method mGetService = cServiceManager.getMethod("getService", java.lang.String.class); Object oRemoteService = mGetService.invoke(null, "usagestats"); // 相當於:IUsageStats mUsageStatsService = IUsageStats.Stub.asInterface(oRemoteService) Class<?> cStub = Class.forName("com.android.internal.app.IUsageStats$Stub"); Method mUsageStatsService = cStub.getMethod("asInterface", android.os.IBinder.class); Object oIUsageStats = mUsageStatsService.invoke(null, oRemoteService); // 相當於:PkgUsageStats[] oPkgUsageStatsArray =mUsageStatsService.getAllPkgUsageStats(); Class<?> cIUsageStatus = Class.forName("com.android.internal.app.IUsageStats"); Method mGetAllPkgUsageStats = cIUsageStatus.getMethod("getAllPkgUsageStats", (Class[]) null); Object[] oPkgUsageStatsArray = (Object[]) mGetAllPkgUsageStats.invoke(oIUsageStats, (Object[]) null); //相當於 //for (PkgUsageStats pkgUsageStats: oPkgUsageStatsArray) //{ // 當前APP的包名: // packageName = pkgUsageStats.packageName // 當前APP的啟動次數 // launchCount = pkgUsageStats.launchCount // 當前APP的累計使用時間: // usageTime = pkgUsageStats.usageTime // 當前APP的每個Activity的最後啟動時間 // componentResumeTimes = pkgUsageStats.componentResumeTimes //} Class<?> cPkgUsageStats = Class.forName("com.android.internal.os.PkgUsageStats"); for (Object pkgUsageStats : oPkgUsageStatsArray) { String packageName = (String) cPkgUsageStats.getDeclaredField("packageName").get(pkgUsageStats); int launchCount = cPkgUsageStats.getDeclaredField("launchCount").getInt(pkgUsageStats); long usageTime = cPkgUsageStats.getDeclaredField("usageTime").getLong(pkgUsageStats); Map<String, Long > componentResumeMap = (Map<String, Long>) cPkgUsageStats.getDeclaredField("componentResumeTimes").get(pkgUsageStats); }
3、UsageStatsService工作原理是什麼?
(3-1)首先是service的初始化,主要分為兩步;
第一步是從/data/system/usagestats/usage-history.xml檔案中讀取每個APP中每個Activity最後啟動的時間;
[java] view plain copy
//初始化/data/system/usagestats/usage-history.xml mHistoryFile = new AtomicFile(new File(mDir, FILE_HISTORY)); //解析xml檔案,將xml解析成為資料,並存儲 readStatsFromFile();
第二步是從“/data/system/usagestats/usage-當前日誌”檔案中解析今天的使用記錄的資料:
[java] view plain copy
//指定檔案字首為usage,根據日誌生成檔案字尾,例如usage-20140817
mFileLeaf = getCurrentDateStr(FILE_PREFIX);
//解析檔案,為檔案流的格式,直接讀取即可
readStatsFromFile();
其中“/data/system/usagestats/usage-當前日誌”檔案格式為2精製的序列化流,可直接從中讀取相應的物件即可;如果對應檔案不存在,則建立它;
(3-2)在初始化service完成後,需要組資料儲存:
從usage-history.xml檔案中解析出來的資料,放在:
[java] view plain copy
// key為包名,value為Map<String, Long>,記錄該包下的Activity名以及該Activity最後啟動時間
final private Map<String, Map<String, Long>> mLastResumeTimes;
從“/data/system/usagestats/usage-當前日誌”解析出來的資料,放在:
[java] view plain copy
// String為包名,PkgUsageStatsExtended儲存相應的啟動資訊
final private Map<String, PkgUsageStatsExtended> mStats;
可以發現此處封裝了一個內部類PkgUsageStatsExtended做資料儲存,其實PkgUsageStatsExtended還封裝了許多操作,後面會逐步涉及,目前PkgUsageStatsExtended主要儲存了這些資訊:
[java] view plain copy
private class PkgUsageStatsExtended {
//每個Activity最後啟動時間
final HashMap<String, TimeStats> mLaunchTimes
= new HashMap<String, TimeStats>();
//當前APP啟動次數
int mLaunchCount;
//當前APP使用時間
long mUsageTime;
//當前APP最後一次呼叫onPause時間
long mPausedTime;
//當前APP最後一個呼叫onResume時間
long mResumedTime;
//更新mResumedTime為當前時間
void updateResume(String comp, boolean launched) {
if (launched) {
mLaunchCount ++;
}
mResumedTime = SystemClock.elapsedRealtime();
}
//更新mPausedTime為當前時間,並且使用時常=mPausedTime - mResumedTime
void updatePause() {
mPausedTime = SystemClock.elapsedRealtime();
mUsageTime += (mPausedTime - mResumedTime);
}
//記錄activity次數
void addLaunchCount(String comp) {
TimeStats times = mLaunchTimes.get(comp);
if (times == null) {
times = new TimeStats();
mLaunchTimes.put(comp, times);
}
times.incCount();
}
//記錄activity啟動時間
void addLaunchTime(String comp, int millis) {
TimeStats times = mLaunchTimes.get(comp);
if (times == null) {
times = new TimeStats();
mLaunchTimes.put(comp, times);
}
times.add(millis);
}
/*more code
*/
}
(3-3)當應用啟動一個Activity時,UsageStatsService會發生什麼行為?
ActivityManagerService會呼叫UsageStatsService.noteResumeComponent方法,在該方法中會有以下操作:
[java] view plain copy
public void noteResumeComponent(ComponentName componentName) {
enforceCallingPermission();
String pkgName;
synchronized (mStatsLock) {
/*some code
*/
final boolean samePackage = pkgName.equals(mLastResumedPkg);
//1、mIsResumed會在onResume中變為true,在onPause中變為false
if (mIsResumed) {
if (mLastResumedPkg != null) {
//2、這裡是為了避免沒有呼叫onPause的情況出現,理論上不存在
PkgUsageStatsExtended pus = mStats.get(mLastResumedPkg);
if (pus != null) {
//3、增加保護,呼叫一次updatePause
pus.updatePause();
}
}
}
final boolean sameComp = samePackage
&& componentName.getClassName().equals(mLastResumedComp);
//5、內部資料更新,記錄最後一次啟動的Activity
mIsResumed = true;
mLastResumedPkg = pkgName;
mLastResumedComp = componentName.getClassName();
PkgUsageStatsExtended pus = mStats.get(pkgName);
if (pus == null) {
pus = new PkgUsageStatsExtended();
mStats.put(pkgName, pus);
}
//6、更新Activity啟動時間,如果使用者是從一個App啟動進入另外一個APP,那麼需要App標識啟動次數+1
pus.updateResume(mLastResumedComp, !samePackage);
if (!sameComp) {
//7、同上,Activity啟動次數+1
pus.addLaunchCount(mLastResumedComp);
}
Map<String, Long> componentResumeTimes = mLastResumeTimes.get(pkgName);
if (componentResumeTimes == null) {
componentResumeTimes = new HashMap<String, Long>();
mLastResumeTimes.put(pkgName, componentResumeTimes);
}
//8、更新componentResumeTimes
componentResumeTimes.put(mLastResumedComp, System.currentTimeMillis());
}
}
(3-4)當退出一個Activity,UsageStatsService會發生什麼行為?
ActivityManagerService會呼叫UsageStatsService.notePauseComponent方法,UsageStatsService會更新當前展示Activity的OnPause時間:
[java] view plain copy
public void notePauseComponent(ComponentName componentName) {
/*some code
*/
synchronized (mStatsLock) {
/*some code
*/
//1、mIsResumed會在onResume中變為true,在onPause中變為false
mIsResumed = false;
PkgUsageStatsExtended pus = mStats.get(pkgName);
if (pus == null) {
// Weird some error here
Slog.i(TAG, "No package stats for pkg:"+pkgName);
return;
}
//2、更新onPause的時間
pus.updatePause();
}
//3、視情況而定確認是否需要將記憶體資料儲存成檔案
writeStatsToFile(false, false);
}
(3-5)現在,我們已經明白APP啟動資料如何在記憶體中流轉了,那麼什麼時候系統將資料持久化成檔案?
UsageStatsService通過writeStatsToFile方法將資料持久化成檔案,函式原型如下:
[java] view plain copy
/**
* 在特定條件下,將mStats或者mLastResumeTimes寫入到檔案中,由使用者操作觸發
* @params force 強制將mStats例項化到檔案中
*
* @params forceWriteHistoryStats 強制將mLastResumeTimes例項化到檔案中
*/
private void writeStatsToFile(final boolean force, final boolean forceWriteHistoryStats) {
}
具體觸發writeStatsToFile的時機有:
1、使用者關機,觸發writeStatsToFile(true, true);
2、使用者開啟一個新應用,觸發writeStatsToFile(false,false);
3、在notePauseComponent最後,呼叫writeStatsToFile(false,false);
writeStatsToFile觸發寫檔案操作的條件有:
1、關機強制觸發;
2、當前日期與最後一次寫檔案日期不同;
3、除使用者關機外,兩次寫檔案間隔必須在30分鐘以上;
(3-6)解除安裝APP後,已經記錄的資料會被清除;
===================================以下內容是本人修復===========================================
android 7.0上是20分鐘寫檔案,其他版本未知
public final class UsageStats implements Parcelable {
/**
* {@hide}
*/
public String mPackageName;//包名
/**
* {@hide}
*/
public long mBeginTimeStamp;
/**
* {@hide}
*/
public long mEndTimeStamp;
/**
* Last time used by the user with an explicit action (notification, activity launch).
* {@hide}
*/
public long mLastTimeUsed;//上次開始時間
/**
* {@hide}
*/
public long mTotalTimeInForeground;//一共使用的時間
/**
* {@hide}
*/
public int mLaunchCount;//啟動的次數
/**
* {@hide}
*/
public int mLastEvent;
每次啟動時會觸發 IntervalStats.java 中的update方法:
void update(String packageName, long timeStamp, int eventType) {
UsageStats usageStats = getOrCreateUsageStats(packageName);
// TODO(adamlesinski): Ensure that we recover from incorrect event sequences
// like double MOVE_TO_BACKGROUND, etc.
if (eventType == UsageEvents.Event.MOVE_TO_BACKGROUND ||
eventType == UsageEvents.Event.END_OF_DAY) {
if (usageStats.mLastEvent == UsageEvents.Event.MOVE_TO_FOREGROUND ||
usageStats.mLastEvent == UsageEvents.Event.CONTINUE_PREVIOUS_DAY) {
//注意:timeStamp就是上次的結束時間,usageStats.mLastTimeUsed就是上次開始時間
usageStats.mTotalTimeInForeground += timeStamp - usageStats.mLastTimeUsed;
}
}
if (isStatefulEvent(eventType)) {
usageStats.mLastEvent = eventType;
}
if (eventType != UsageEvents.Event.SYSTEM_INTERACTION) {
usageStats.mLastTimeUsed = timeStamp;
}
usageStats.mEndTimeStamp = timeStamp;
if (eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
usageStats.mLaunchCount += 1;
}
endTime = timeStamp;
}