文章將會同步到個人微信公眾號:Android部落格
3、健康數(shù)據(jù)記錄項目
這個項目遇到的主要問題是應用使用時長和使用次數(shù)不準確的問題。原因要從應用的業(yè)務邏輯以及源碼中去查找。
一般我們獲取應用使用數(shù)據(jù)詳情的方法是:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private ArrayList<AppLaunchInfoBean> getAppLaunchInfoBean(long start, long end) {
final UsageStatsManager usageStatsManager = (UsageStatsManager) mContext.getSystemService("usagestats");
UsageEvents usageEvents = usageStatsManager.queryEvents(start, end);
return getAppLaunchInfoBeanList(usageEvents, end);
}
3.1 業(yè)務邏輯
當每次打開應用的時候,通過上述方法去取使用數(shù)據(jù),或者每次從應用其他頁面回到首頁的時候去取,將取到的數(shù)據(jù)持久化保存到本地數(shù)據(jù)庫。
這種使用方式看起來很合理,但是測試人員總是反饋應用使用時長和次數(shù)不準確。到這里就需要從源碼找原因了。
3.2 UsageStatsManager源碼追溯
我們都知道linux從init.rc腳本啟動了Zygote,Zygote 通過fork創(chuàng)建了system_server進程,這個集成所屬的類是SystemServer,在他的run方法中啟動了一些列的系統(tǒng)服務,我們重點關注UsageStatsService何時啟動。
SystemServer
private void run() {
mSystemServiceManager = new SystemServiceManager(mSystemContext);
LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);
startCoreServices();
}
private void startCoreServices() {
mSystemServiceManager.startService(UsageStatsService.class);
mActivityManagerService.setUsageStatsManager(LocalServices.getService(UsageStatsManagerInternal.class));
}
SystemServiceManager統(tǒng)一管理系統(tǒng)服務,交給它去啟動服務,并且將啟動之后的服務交給ActivityManagerService調度。
SystemServiceManager
public <T extends SystemService> T startService(Class<T> serviceClass) {
final String name = serviceClass.getName();
final T service;
Constructor<T> constructor = serviceClass.getConstructor(Context.class);
service = constructor.newInstance(mContext);
startService(service);
return service;
}
public void startService(@NonNull final SystemService service) {
// Register it.
mServices.add(service);
// Start it.
long time = SystemClock.elapsedRealtime();
try {
service.onStart();
} catch (RuntimeException ex) {
}
warnIfTooLong(SystemClock.elapsedRealtime() - time, service, "onStart");//50ms
}
這里可以看到通過反射的方式調用了UsageStatsService的構造函數(shù),構造完成之后通過startService方法啟動這個服務:
UsageStatsService
public class UsageStatsService extends SystemService implements UserUsageStatsService.StatsUpdatedListener {
public UsageStatsService(Context context) {
super(context);
}
}
//start方法比較長,只提取比較重要的方法
@Override
public void onStart() {
//第一部分
mUserManager = (UserManager) getContext().getSystemService(Context.USER_SERVICE);
//第二部分
mHandler = new H(BackgroundThread.get().getLooper());
//第三部分
File systemDataDir = new File(Environment.getDataDirectory(), "system");
mUsageStatsDir = new File(systemDataDir, "usagestats");
mUsageStatsDir.mkdirs();
//第四部分
publishLocalService(UsageStatsManagerInternal.class, new LocalService());
publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService());
//第五部分
getUserDataAndInitializeIfNeededLocked(UserHandle.USER_SYSTEM, mSystemTimeSnapshot);
}
private UserUsageStatsService getUserDataAndInitializeIfNeededLocked(int userId, long currentTimeMillis) {
UserUsageStatsService service = mUserState.get(userId);
if (service == null) {
service = new UserUsageStatsService(getContext(), userId, new File(mUsageStatsDir, Integer.toString(userId)), this);
service.init(currentTimeMillis);
mUserState.put(userId, service);
}
return service;
}
UsageStatsService的構造函數(shù)比較簡單,重點分析start方法:
- 第一部分中獲取了
UserManager,這個是為多用戶的情況下處理數(shù)據(jù)準備的 - 第二部分中創(chuàng)建了一個Handler,用于處理數(shù)據(jù),比如存儲數(shù)據(jù)到磁盤,他的looper其實來自于HandlerThread,因為BackgroundThread繼承自HandlerThread。
- 第三部分是在/data/system/目錄下創(chuàng)建一個usagestats的文件夾,用于創(chuàng)建文件存放數(shù)據(jù)。
- 第四部分中,其實是將LocalService對象添加到LocalServices的集合中,而LocalService是UsageStatsService的內部類;
publishBinderService做的事情就是將BinderService添加到ServiceManager中。BinderService的定義是:
private final class BinderService extends IUsageStatsManager.Stub {}
我們知道IUsageStatsManager.Stub是對客戶端提供的代理對象,客戶端獲取到對象進行對應的操作,而具體的操作函數(shù)就定義在BinderService覆寫的方法中。
- 第五部分意在初始化一個
UserUsageStatsService類,在初始化的時候回傳遞userId,根據(jù)這個userId創(chuàng)建對應的文件夾存儲不同用戶的數(shù)據(jù):
UserUsageStatsService
UserUsageStatsService(Context context, int userId, File usageStatsDir, StatsUpdatedListener listener) {
mDatabase = new UsageStatsDatabase(usageStatsDir);
mCurrentStats = new IntervalStats[UsageStatsManager.INTERVAL_COUNT];
}
UserUsageStatsService構造函數(shù)中又創(chuàng)建了一個UsageStatsDatabase對象,以及IntervalStats類型的數(shù)組。
前者主要功能是往xml文件中寫數(shù)據(jù),后者的主要功能是處理不同時間間隔的數(shù)據(jù)。
UsageStatsDatabase
public UsageStatsDatabase(File dir) {
mIntervalDirs = new File[] {
new File(dir, "daily"),
new File(dir, "weekly"),
new File(dir, "monthly"),
new File(dir, "yearly"),
};
mVersionFile = new File(dir, "version");
mSortedStatFiles = new TimeSparseArray[mIntervalDirs.length];
}
這里又分不同的時間屬性創(chuàng)建文件夾存放數(shù)據(jù)。
UserUsageStatsService最后會調用init方法,這個方法的目的是讀取已有的數(shù)據(jù),沒有相關的數(shù)據(jù)就初始化創(chuàng)建。
到這里基本的初始化工作就完成了。
3.3 客戶端獲取數(shù)據(jù)源碼追蹤
客戶端的調用代碼是:
usageStatsManager.queryEvents(start, end);
追蹤一下這個代碼的調用棧:
UsageStatsManager
public UsageEvents queryEvents(long beginTime, long endTime) {
try {
UsageEvents iter = mService.queryEvents(beginTime, endTime, mContext.getOpPackageName());
if (iter != null) {
return iter;
}
} catch (RemoteException e) {
}
return sEmptyResults;
}
這里的mService是IUsageStatsManager類型,是服務端的操作對象,對應的是服務端UsageStatsService的內部類BinderService,也就是對應的調用其中的方法:
UsageStatsService.BinderService
@Override
public UsageEvents queryEvents(long beginTime, long endTime, String callingPackage) {
if (!hasPermission(callingPackage)) {
return null;
}
try {
return UsageStatsService.this.queryEvents(userId, beginTime, endTime,
obfuscateInstantApps);
} finally {
Binder.restoreCallingIdentity(token);
}
}
UsageEvents queryEvents(int userId, long beginTime, long endTime, boolean shouldObfuscateInstantApps) {
final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
return service.queryEvents(beginTime, endTime, shouldObfuscateInstantApps);
}
在queryEvents方法中調用了外部類的queryEvents方法,而在這個方法中最終是調用到了UserUsageStatsService的queryEvents方法:
UserUsageStatsService
UsageEvents queryEvents(final long beginTime, final long endTime, boolean obfuscateInstantApps) {
final ArraySet<String> names = new ArraySet<>();
List<UsageEvents.Event> results = queryStats(UsageStatsManager.INTERVAL_DAILY,
beginTime, endTime, new StatCombiner<UsageEvents.Event>() {
@Override
public void combine(IntervalStats stats, boolean mutable, List<UsageEvents.Event> accumulatedResult) {
final int startIndex = stats.events.firstIndexOnOrAfter(beginTime);
final int size = stats.events.size();
for (int i = startIndex; i < size; i++) {
UsageEvents.Event event = stats.events.get(i);
names.add(event.mPackage);
if (event.mClass != null) {
names.add(event.mClass);
}
accumulatedResult.add(event);
}
}
});
String[] table = names.toArray(new String[names.size()]);
Arrays.sort(table);
return new UsageEvents(results, table);
}
這里調用的時候如果不設置時間間隔,默認是INTERVAL_DAILY,看看具體的queryStats方法:
private <T> List<T> queryStats(int intervalType, final long beginTime, final long endTime, StatCombiner<T> combiner) {
//第一部分
final IntervalStats currentStats = mCurrentStats[intervalType];
//第二部分
List<T> results = mDatabase.queryUsageStats(intervalType, beginTime, truncatedEndTime, combiner);
//第三部分
if (beginTime < currentStats.endTime && endTime > currentStats.beginTime) {
combiner.combine(currentStats, true, results);
}
return results;
}
- 第一部分從當前內存中里面取,因為INTERVAL_DAILY的數(shù)據(jù)被report的時候,一開始是放到mCurrentStats里面存起來。
mCurrentStats是IntervalStats數(shù)組類型,而IntervalStats里面維護了一個EventList對象,這個對象里面持有一個ArrayList<UsageEvents.Event> mEvents,維護應用使用詳情數(shù)據(jù)。
- 第二部分從本地磁盤的xml文件取需要的時間間隔內的數(shù)據(jù)。在取到數(shù)據(jù)之后回調combine方法將數(shù)據(jù)存放到一個List中:
UsageStatsDatabase
public <T> List<T> queryUsageStats(int intervalType, long beginTime, long endTime, StatCombiner<T> combiner) {
final TimeSparseArray<AtomicFile> intervalStats = mSortedStatFiles[intervalType];
int startIndex = intervalStats.closestIndexOnOrBefore(beginTime);
int endIndex = intervalStats.closestIndexOnOrBefore(endTime);
final IntervalStats stats = new IntervalStats();
final ArrayList<T> results = new ArrayList<>();
for (int i = startIndex; i <= endIndex; i++) {
final AtomicFile f = intervalStats.valueAt(i);
UsageStatsXml.read(f, stats);
if (beginTime < stats.endTime) {
combiner.combine(stats, false, results);
}
}
return results;
}
- 第三部分是將內存和磁盤的數(shù)據(jù)合并起來。
到這里我們就知道,取數(shù)據(jù)的時候是內存和磁盤數(shù)據(jù)的合集,那么究竟該怎么取數(shù)據(jù)才能比較準確呢?看看系統(tǒng)怎么存儲數(shù)據(jù)的。
3.4 系統(tǒng)存數(shù)據(jù)源碼追蹤
記得UsageStatsService在系統(tǒng)初始化的時候,會將他的一個對象設置到AMS,這里就是數(shù)據(jù)存儲被觸發(fā)的地方:
ActivityManagerService
void updateUsageStats(ActivityRecord component, boolean resumed) {
if (resumed) {
if (mUsageStatsService != null) {
mUsageStatsService.reportEvent(component.realActivity, component.userId, UsageEvents.Event.MOVE_TO_FOREGROUND);
}
} else {
if (mUsageStatsService != null) {
mUsageStatsService.reportEvent(component.realActivity, component.userId, UsageEvents.Event.MOVE_TO_BACKGROUND);
}
}
}
updateUsageStats方法在三個地方被調用:
- ActivityStackSupervisor,reportResumedActivityLocked
- ActivityStack,startPausingLocked
- ActivityStack,removeHistoryRecordsForAppLocked
從這三個方法名稱可以看出來一般都是Activity從前臺切換到后臺,或從后臺到前臺時會觸發(fā)這個方法。
從updateUsageStats方法中可以看出,分為MOVE_TO_FOREGROUND,MOVE_TO_BACKGROUND調用reportEvent方法。
這里的mUsageStatsService是UsageStatsManagerInternal類型,記得在UsageStatsService的start方法中有publishLocalService(UsageStatsManagerInternal.class, new LocalService());方法,這里UsageStatsManagerInternal是type,LocalService是type對應的service,而LocalService繼承自UsageStatsManagerInternal,因此這里具體操作在UsageStatsService的內部類LocalService中。
UsageStatsService.LocalService
private final class BinderService extends IUsageStatsManager.Stub {
@Override
public void reportEvent(ComponentName component, int userId, int eventType) {
UsageEvents.Event event = new UsageEvents.Event();
event.mPackage = component.getPackageName();
event.mClass = component.getClassName();
// This will later be converted to system time.
event.mTimeStamp = SystemClock.elapsedRealtime();
event.mEventType = eventType;
mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
}
}
這里新建一個UsageEvents.Event對象,將包名,組件名,時間,類型填充起來,通過UsageStatsService onStart方法中初始化的mHandler中串行的處理消息:
UsageStatsService
class H extends Handler {
public H(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_REPORT_EVENT:
reportEvent((UsageEvents.Event) msg.obj, msg.arg1);
break;
case MSG_FLUSH_TO_DISK:
flushToDisk();
break;
}
}
}
調用外部類的reportEvent方法:
UsageStatsService
void reportEvent(UsageEvents.Event event, int userId) {
final UserUsageStatsService service = getUserDataAndInitializeIfNeededLocked(userId, timeNow);
service.reportEvent(event);
}
UserUsageStatsService
void reportEvent(UsageEvents.Event event) {
final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
// Add the event to the daily list.
if (currentDailyStats.events == null) {
currentDailyStats.events = new EventList();
}
if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) {
currentDailyStats.events.insert(event);
}
for (IntervalStats stats : mCurrentStats) {
switch (event.mEventType) {
default: {
stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
break;
}
}
}
notifyStatsChanged();
}
- 第一步先把數(shù)據(jù)放到Daily所屬的文件中,也就是放到內存中。
- 第二步,調用IntervalStats的update方法實施更新??纯催@里一連串的操作:
IntervalStats
void update(String packageName, long timeStamp, int eventType) {
UsageStats usageStats = getOrCreateUsageStats(packageName);
usageStats.mEndTimeStamp = timeStamp;
if (eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
usageStats.mLaunchCount += 1;
}
endTime = timeStamp;
}
UsageStats getOrCreateUsageStats(String packageName) {
UsageStats usageStats = packageStats.get(packageName);
if (usageStats == null) {
usageStats = new UsageStats();
usageStats.mPackageName = getCachedStringRef(packageName);
usageStats.mBeginTimeStamp = beginTime;
usageStats.mEndTimeStamp = endTime;
packageStats.put(usageStats.mPackageName, usageStats);
}
return usageStats;
}
到這里可以知道每一種時間類型對應的IntervalStats對象里面維持一個UsageStats對象,這個對象里面包含了包名,開始使用時間,結束使用時間數(shù)據(jù)。
數(shù)據(jù)都準備好了,接下來調用notifyStatsChanged:
UserUsageStatsService
private void notifyStatsChanged() {
if (!mStatsChanged) {
mStatsChanged = true;
mListener.onStatsUpdated();
}
}
而這里的mListener是UsageStatsService傳遞過來的,對應的onStatsUpdated在這個類中實現(xiàn):
UsageStatsService
private static final long TEN_SECONDS = 10 * 1000;
private static final long TWENTY_MINUTES = 20 * 60 * 1000;
private static final long FLUSH_INTERVAL = COMPRESS_TIME ? TEN_SECONDS : TWENTY_MINUTES;
@Override
public void onStatsUpdated() {
mHandler.sendEmptyMessageDelayed(MSG_FLUSH_TO_DISK, FLUSH_INTERVAL);
}
還是在這個H類中處理,這里的FLUSH_INTERVAL是20分鐘,也就是要間隔這么長時間才去寫數(shù)據(jù)到磁盤:
UsageStatsService.H
class H extends Handler {
public H(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_FLUSH_TO_DISK:
flushToDisk();
break;
}
}
}
UsageStatsService
void flushToDisk() {
synchronized (mLock) {
flushToDiskLocked();
}
}
private void flushToDiskLocked() {
final int userCount = mUserState.size();
for (int i = 0; i < userCount; i++) {
UserUsageStatsService service = mUserState.valueAt(i);
service.persistActiveStats();
}
mHandler.removeMessages(MSG_FLUSH_TO_DISK);
}
還是到UserUsageStatsService類中處理,而且有多個用戶的話,為多個用戶分別存儲數(shù)據(jù):
UserUsageStatsService
void persistActiveStats() {
if (mStatsChanged) {
try {
for (int i = 0; i < mCurrentStats.length; i++) {
mDatabase.putUsageStats(i, mCurrentStats[i]);
}
mStatsChanged = false;
} catch (IOException e) {
}
}
}
這里將為各個時間間隔類型的文件中都寫入數(shù)據(jù)。接下來在UsageStatsDatabase中調用putUsageStats方法:
UsageStatsDatabase
public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
synchronized (mLock) {
//第一部分
AtomicFile f = mSortedStatFiles[intervalType].get(stats.beginTime);
if (f == null) {
f = new AtomicFile(new File(mIntervalDirs[intervalType],
Long.toString(stats.beginTime)));
mSortedStatFiles[intervalType].put(stats.beginTime, f);
}
//第二部分
UsageStatsXml.write(f, stats);
stats.lastTimeSaved = f.getLastModifiedTime();
}
}
TimeSparseArray<AtomicFile>[] mSortedStatFiles,繼承自LongSpareArray
第一部分中,先獲取mSortedStatFiles中對應時間的文件是否存在,不存在的話就按照對應的時間間隔類型新建一個,創(chuàng)建完成之后將時間作為key,文件對象作為value添加到TimeSparseArray集合中。這個類型是有序的,而且會先通過二分查找這個key,如果存在,就要覆寫數(shù)據(jù)了。
第二部分通過調用
UsageStatsXml.write方法執(zhí)行寫xml操作:
UsageStatsXml
private static final String USAGESTATS_TAG = "usagestats";
static void write(OutputStream out, IntervalStats stats) throws IOException {
FastXmlSerializer xml = new FastXmlSerializer();
xml.setOutput(out, "utf-8");
xml.startDocument("utf-8", true);
xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
xml.startTag(null, USAGESTATS_TAG);
xml.attribute(null, VERSION_ATTR, Integer.toString(CURRENT_VERSION));
UsageStatsXmlV1.write(xml, stats);
xml.endTag(null, USAGESTATS_TAG);
xml.endDocument();
}
開始標簽是USAGESTATS_TAG,通過UsageStatsXmlV1寫數(shù)據(jù):
UsageStatsXmlV1
public static void write(XmlSerializer xml, IntervalStats stats) throws IOException {
xml.startTag(null, PACKAGES_TAG);
final int statsCount = stats.packageStats.size();
for (int i = 0; i < statsCount; i++) {
writeUsageStats(xml, stats, stats.packageStats.valueAt(i));
}
xml.endTag(null, PACKAGES_TAG);
}
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats, final UsageStats usageStats) throws IOException {
xml.startTag(null, PACKAGE_TAG);
// Write the time offset.
XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR, usageStats.mLastTimeUsed - stats.beginTime);
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
if (usageStats.mAppLaunchCount > 0) {
XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
}
writeChooserCounts(xml, usageStats);
xml.endTag(null, PACKAGE_TAG);
}
到這里我們發(fā)現(xiàn)包名,時長,使用次數(shù),mLastEvent都被寫入磁盤了。
mLastEvent對應的是前臺或后臺事件,是int類型,前臺為1,后臺為2,一天的結束時間事件為3。
3.5 項目問題復盤
- 結合源碼分析問題
Android9.0以后將應用使用詳情的大多數(shù)數(shù)據(jù)都寫到磁盤了,但是Android 9.0以下的版本中沒有將應用使用次數(shù)寫到磁盤。另外還要面臨延遲20分鐘寫磁盤的操作,如果每次都從磁盤取數(shù)據(jù),在Android 9.0以下的版本中讀取的的次數(shù)一定是不準確的。
相關的版本差異如下:
//Android 7.1
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
final UsageStats usageStats) throws IOException {
xml.startTag(null, PACKAGE_TAG);
// Write the time offset.
XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
usageStats.mLastTimeUsed - stats.beginTime);
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
xml.endTag(null, PACKAGE_TAG);
}
//Android 8.1
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
final UsageStats usageStats) throws IOException {
xml.startTag(null, PACKAGE_TAG);
// Write the time offset.
XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
usageStats.mLastTimeUsed - stats.beginTime);
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
writeChooserCounts(xml, usageStats);
xml.endTag(null, PACKAGE_TAG);
}
//Android 9.0
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
final UsageStats usageStats) throws IOException {
xml.startTag(null, PACKAGE_TAG);
// Write the time offset.
XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
usageStats.mLastTimeUsed - stats.beginTime);
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
if (usageStats.mAppLaunchCount > 0) {
XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
}
writeChooserCounts(xml, usageStats);
xml.endTag(null, PACKAGE_TAG);
}
//Android 10.0
private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
final UsageStats usageStats) throws IOException {
xml.startTag(null, PACKAGE_TAG);
// Write the time offset.
XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
usageStats.mLastTimeUsed - stats.beginTime);
XmlUtils.writeLongAttribute(xml, LAST_TIME_VISIBLE_ATTR,
usageStats.mLastTimeVisible - stats.beginTime);
XmlUtils.writeLongAttribute(xml, LAST_TIME_SERVICE_USED_ATTR,
usageStats.mLastTimeForegroundServiceUsed - stats.beginTime);
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, usageStats.mPackageName);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, usageStats.mTotalTimeInForeground);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_VISIBLE_ATTR, usageStats.mTotalTimeVisible);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_SERVICE_USED_ATTR,
usageStats.mTotalTimeForegroundServiceUsed);
XmlUtils.writeIntAttribute(xml, LAST_EVENT_ATTR, usageStats.mLastEvent);
if (usageStats.mAppLaunchCount > 0) {
XmlUtils.writeIntAttribute(xml, APP_LAUNCH_COUNT_ATTR, usageStats.mAppLaunchCount);
}
writeChooserCounts(xml, usageStats);
xml.endTag(null, PACKAGE_TAG);
}
到后面,寫入的數(shù)據(jù)顆粒度越來越小,比如應用可見時長,前臺服務的時長等都被寫入磁盤。這是因為后面Android在設置中也做了應用使用詳情功能,如果這些數(shù)據(jù)不寫入的話,數(shù)據(jù)會有出入。
3.5.1 問題解決方案
項目早期,這個App屬于系統(tǒng)級別的App,我們可以通過監(jiān)聽滅屏廣播,在滅屏之后立即獲取上一次滅屏到此次滅屏時間段內的應用使用數(shù)據(jù),雖然這段時間間隔會大于20分鐘,但是滅屏之后,最新的數(shù)據(jù)會先被寫入內存,而之前的數(shù)據(jù)在大于20分鐘會被寫入磁盤導致一部分次數(shù)的數(shù)據(jù)丟失,但是出現(xiàn)的概率比較低,可以接受。
到項目后期,App的系統(tǒng)級別屬性被去掉,只能作為一個普通App開發(fā)了,這里一方面修改framework,將應用使用次數(shù)也持久化到磁盤;如果framework的這個patch沒有集成的話,可以在另外一個系統(tǒng)級服務中實現(xiàn)之前早期項目App的那一套保存數(shù)據(jù)邏輯,將數(shù)據(jù)即時存到本地數(shù)據(jù)庫,并對外提供數(shù)據(jù)接口,同時加強權限判斷,避免被亂用。這樣App就可以獲取到最新的數(shù)據(jù)了。