关键字: 应用统计 Android源码 应用使用时长 应用使用次数上篇文章讲到LocalService以及BinderService都是调用了UserUsageStatsService的相关函数接口,实现相关功能。以下,主要分为记录和查询两个方面对UserUsageStatsService进行解析。
数据的记录
上篇文章曾讲到,不论是Event还是ConfigurationChange,都是调用UserUsageStatsService.reportEvent(event)这一函数,用来记录event和config数据。
void reportEvent(UsageEvents.Event event) {
if (DEBUG) {
Slog.d(TAG, mLogPrefix + "Got usage event for " + event.mPackage
+ "[" + event.mTimeStamp + "]: "
+ eventToString(event.mEventType));
}if (event.mTimeStamp >= mDailyExpiryDate.getTimeInMillis()) {
// Need to rollover
//切换文件(因为一个文件仅记录当天的数据,如果数据的时间超过了当天的标记时间,则需要新建另一文件,记录第二天的数据)
rolloverStats(event.mTimeStamp);
}
final IntervalStats currentDailyStats = mCurrentStats[UsageStatsManager.INTERVAL_DAILY];
final Configuration newFullConfig = event.mConfiguration;
if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE &&
currentDailyStats.activeConfiguration != null) {
// Make the event configuration a delta.
event.mConfiguration = Configuration.generateDelta(
currentDailyStats.activeConfiguration, newFullConfig);
}// Add the event to the daily list.
//将event数据计入统计数据中。
if (currentDailyStats.events == null) {
currentDailyStats.events = new TimeSparseArray<>();
}
currentDailyStats.events.put(event.mTimeStamp, event);
//依次更新日,周,月,年的统计数据
for (IntervalStats stats : mCurrentStats) {
if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE) {
stats.updateConfigurationStats(newFullConfig, event.mTimeStamp);
} else {
stats.update(event.mPackage, event.mTimeStamp, event.mEventType);
}
}
//关键:下一函数最终会调用上篇文章讲到的flushToDiskLocked(),用于将数据写入文件,而flushToDiskLocked()调用的是UserUsageStatsService.persistActiveStats()
notifyStatsChanged();
}
综上可知,但凡记录数据,最终均需要调用UserUsageStatsService.persistActiveStats(),具体如下
void persistActiveStats() {
if (mStatsChanged) {
Slog.i(TAG, mLogPrefix + "Flushing usage stats to disk");
try {
for (int i = 0;
i < mCurrentStats.length;
i++) {
mDatabase.putUsageStats(i, mCurrentStats[i]);
}
mStatsChanged = false;
} catch (IOException e) {
Slog.e(TAG, mLogPrefix + "Failed to persist active stats", e);
}
}
}
UsageStatsDatabase,进行读写操作的主要接口,会在相关目录下建立或者查询daily,weekly,monthly,yearly四个xml文件,并操作之。
mDatabase.putUsageStats()的具体源码如下。先从注释上来说,这是一个写文件的函数,而且它还特意注明,写入文件不会立刻执行,所以在第三篇文章中的LocalService中会有一个prepareShutdown()函数,会在系统关机前将保存于内存中的相关数据写入到文件中。这就导致内存中的数据是具有实时性的,但是文件中的数据不具备实时性,万一出现意外情况,比如说拔除手机电池,这样一来内存中的数据无法及时写入文件。再次开机,系统会读取文件中的数据,并以这些数据为准,显然这些数据是丢失一部分的,不完全的。以上是当前5.1版本的源码逻辑的不足之一。
/**
* Update the stats in the database. They may not be written to disk immediately.
*/
public void putUsageStats(int intervalType, IntervalStats stats) throws IOException {
synchronized (mLock) {
if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
throw new IllegalArgumentException("Bad interval type " + intervalType);
}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();
}
}
接下来,函数如下,依次调用UsageStatsXml.write(AtomicFile file, IntervalStats stats) –>UsageStatsXml.write(OutputStream out, IntervalStats stats) –> UsageStatsXmlV1.write(XmlSerializer xml, IntervalStats stats) ,将数据写入xml文件中。
public static void write(AtomicFile file, IntervalStats stats) throws IOException {
FileOutputStream fos = file.startWrite();
try {
write(fos, stats);
file.finishWrite(fos);
fos = null;
} finally {
// When fos is null (successful write), this will no-op
file.failWrite(fos);
}
}private 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();
}/**
* Writes the stats object to an XML file. The {@link XmlSerializer}
* has already written the
tag, but attributes may still
* be added.
*
* @param xml The serializer to which to write the packageStats data.
* @param stats The stats object to write to the XML file.
*/
public static void write(XmlSerializer xml, IntervalStats stats) throws IOException {
XmlUtils.writeLongAttribute(xml, END_TIME_ATTR, stats.endTime - stats.beginTime);
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);
xml.startTag(null, CONFIGURATIONS_TAG);
final int configCount = stats.configurations.size();
for (int i = 0;
i < configCount;
i++) {
boolean active = stats.activeConfiguration.equals(stats.configurations.keyAt(i));
writeConfigStats(xml, stats, stats.configurations.valueAt(i), active);
}
xml.endTag(null, CONFIGURATIONS_TAG);
xml.startTag(null, EVENT_LOG_TAG);
final int eventCount = stats.events != null ? stats.events.size() : 0;
for (int i = 0;
i < eventCount;
i++) {
writeEvent(xml, stats, stats.events.valueAt(i));
}
xml.endTag(null, EVENT_LOG_TAG);
}
从最后调用的UsageStatsXmlV1.write(XmlSerializer xml, IntervalStats stats) 的代码上可以看出,正如第二篇文章所介绍的xml文件那样,数据先写入统计数据Stats,再写入ConfigStats数据,最后在把Event数据写入文件。针对以上三个write***函数,可见其源码如下:
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);
}private static void writeConfigStats(XmlSerializer xml, final IntervalStats stats, final ConfigurationStats configStats, boolean isActive) throws IOException {
xml.startTag(null, CONFIG_TAG);
// Write the time offset.
XmlUtils.writeLongAttribute(xml, LAST_TIME_ACTIVE_ATTR,
configStats.mLastTimeActive - stats.beginTime);
XmlUtils.writeLongAttribute(xml, TOTAL_TIME_ACTIVE_ATTR, configStats.mTotalTimeActive);
XmlUtils.writeIntAttribute(xml, COUNT_ATTR, configStats.mActivationCount);
if (isActive) {
XmlUtils.writeBooleanAttribute(xml, ACTIVE_ATTR, true);
}// Now write the attributes representing the configuration object.
Configuration.writeXmlAttrs(xml, configStats.mConfiguration);
xml.endTag(null, CONFIG_TAG);
}private static void writeEvent(XmlSerializer xml, final IntervalStats stats,final UsageEvents.Event event) throws IOException {
xml.startTag(null, EVENT_TAG);
// Store the time offset.
XmlUtils.writeLongAttribute(xml, TIME_ATTR, event.mTimeStamp - stats.beginTime);
XmlUtils.writeStringAttribute(xml, PACKAGE_ATTR, event.mPackage);
if (event.mClass != null) {
XmlUtils.writeStringAttribute(xml, CLASS_ATTR, event.mClass);
}
XmlUtils.writeIntAttribute(xml, TYPE_ATTR, event.mEventType);
if (event.mEventType == UsageEvents.Event.CONFIGURATION_CHANGE
&& event.mConfiguration != null) {
Configuration.writeXmlAttrs(xml, event.mConfiguration);
}xml.endTag(null, EVENT_TAG);
}
以上三个函数皆是将数据写入xml文件中,但是,值得注意的是,所有关于时间的节点,记录的并非是绝对时间,而是类似于 usageStats.mLastTimeUsed - stats.beginTime 这样的减去beginTime之后的相对时间。
结语:
本文主要介绍了关于Android系统中统计各个app的使用情况的解决方案,主要是介绍了在统计数据的时候,将数据写入对应文件这一具体流程,以及写入数据的详细情况。接下来的文章中将会详细阐述如何读取这些记录的数据,并且使用他们。
转载请注明出处。
【Android应用统计-使用时长及次数统计(四)】github:UseTimeStatistic
参考文献:
Android 5.1相关源码目录
Android UsageStatsService:要点解析
Android5.1应用打开次数获取
上一篇:Android应用统计-使用时长及次数统计(三)
下一篇:Android应用统计-使用时长及次数统计(五)