Android应用统计-使用时长及次数统计(四)

关键字: 应用统计 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应用统计-使用时长及次数统计(五)

    推荐阅读