Android UsageStatsService(应用使用统计服务)的学习与介绍

本文最早写于2017年,基于Android O源代码。因当时我修复原生UsageStatsService的一个bug,所以后整理一篇文章,学习下相关知识。文章迁移至此:

一. 简介

UsageStatsService是一个系统服务,其主要通过AMS等,来监测并记录各个应用的使用数据。

UsageStatsService,a service that collects, aggregates, and persists application usage data. This data can be queried by apps that have been granted permission by AppOps.

代码位置:frameworks/base/services/usage/java/com/android/server/usage/UsageStatsService
创建时,其在onStart()方法中会调用如下方法提供服务,

publishLocalService(UsageStatsManagerInternal.class, new LocalService()); // AMS会调用
publishBinderService(Context.USAGE_STATS_SERVICE, new BinderService()); // 给其他Service和APP调用

其中重点关注LocalService,ActivityManagerService有一个成员变量mUsageStatsService,其会统计4个UsageStatsService自定义的事件:

  • MOVE_TO_FOREGROUND
  • MOVE_TO_BACKGROUND
  • CONFIGURATION_CHANGE
  • SYSTEM_INTERACTION

mUsageStatsService的赋值在SystemServer#startCoreServices()方法中,如下:

mActivityManagerService.setUsageStatsManager(LocalServices.getService(UsageStatsManagerInternal.class));


二. 事件

数据的事件类型有7种,全部定义在UsageEvents.java中,如下:

重点介绍以下3个事件,
1. 事件【MOVE_TO_FOREGROUND与MOVE_TO_BACKGROUND】

  • Activity在前台显示时,ActivityStackSupervisor.java中方法reportResumedActivityLocked(),其会调用ActivityManagerService#updateUsageStats(),resumedf参数为true,
  • Activity在后台时,ActivityStack.java中方法startPausingLocked()或removeHistoryRecordsForAppLocked(),其会调用ActivityManagerService#updateUsageStats(),resumedf参数为false,
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()中会调用UsageStatsService的reportEvent方法,来记录下MOVE_TO_FOREGROUND或MOVE_TO_FOREGROUND事件,以及Activity等,这些数据会通过UsageStatsService被保存。

2. 事件【SYSTEM_INTERACTION】
统计此事件的代码调用顺序是:

AcitivityManagerService#applyOomAdjLocked() -> ActivityManagerService#maybeUpdateUsageStatsLocked() -> mUsageStatsService.reportEvent(packages[i], app.userId, UsageEvents.Event.SYSTEM_INTERACTION);

代码:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java#maybeUpdateUsageStatsLocked

其中在方法maybeUpdateUsageStatsLocked中,判断是否向UsageStatsService发送此事件统计的依据之一是变量isInteraction(通过对比app.curProcState)。

举个例子:
百度小米输入法的进程状态由PROCESS_STATE_NONEXISTENT(-1)变更为PROCESS_STATE_BOUND_FOREGROUND_SERVICE(3)时,AMS会向UsageStatsService发送此事件记录。debug log如下:

Checking proc [[com.baidu.input_mi]] state changes: old = -1, new = 3
report SYSTEM_INTERACTION event, package = com.baidu.input_mi

另外,重点注意以下2点:
A. UsageStatsService中,SYSTEM_INTERACTION事件在数据存储时,其event type会被记为0。参考代码:frameworks/base/services/usage/java/com/android/server/usage/IntervalStats.java
B. SYSTEM_INTERACTION事件的上次使用时间,在数据存储时,会被记为0,通过UTC时间转换后,是1970年1月1日

if (eventType != UsageEvents.Event.SYSTEM_INTERACTION) {
     usageStats.mLastTimeUsed = timeStamp;
}

参考代码:frameworks/base/services/usage/java/com/android/server/usage/IntervalStats.java
以上2点是应用使用统计服务的by design逻辑。


三. 数据存储##

UsageStatsService的数据存储在哪里?有一个类在管理UsageStatsDatabase,通过它的源码即可发现,真正的数据持久化是存储在XML中,XML位置:/data/system/usagestats/。XML的所有操作,例如读,写等,都被封装在类UsageStatsXmlV1中,由UsageStatsDatabase调用。

以下介绍4个方面:
1. 缓存与文件存储
UsageStatsService每次在启动时,都会先按照user生成各个UserUsageStatsService,其中每个对象都会先去各自的文件路径下读取数据到内存中。代码如下:

for (int i = 0; i < mCurrentStats.length; i++) {
    mCurrentStats[i] = mDatabase.getLatestUsageStats(i);
    ......

此后每次外界reportEvent,都会先更新内存中的数据,相当于缓存。那什么时候内存中的数据会更新至文件中呢?主要有以下几种情况:

  • 手机关机
  • 系统时间跳变(人为修改系统时间或者时间随网络自动校准)
  • 一天结束时,因为daily下面xml文件存储一天的数据,此时需下次新建文件
  • 等等
    数据在内存中保存在mCurrentStats变量中。

2. 数据目录按daily,monthly,weekly,yearly四个文件夹存储,每个文件夹中包含若干个XML文件

3. 每一个XML中的数据
打开一个XML,即可看到,存储的数据包括三类,package上次访问和使用的时间,configurations,event-log。

注意:
XML中package的lastEvent字段,event-log的type字段,都是指上面介绍过的事件类型。但是注意,事件类型是有7种,但真正记录在XML中,除了4种(MOVE_TO_FOREGROUND,MOVE_TO_BACKGROUND,END_OF_DAY,CONTINUE_PREVIOUS_DAY)记录其int值,其他的事件(CONFIGURATION_CHANGE,SYSTEM_INTERACTION,USER_INTERACTION)都记录为0,所以在XML中看到事件类型为0,那么是指这三种。这段逻辑是在数据记录的IntervalStats#update方法中。

另外,格外注意SYSTEM_INTERACTION这个事件在存储时,上次访问时间是不记录真实时间的,取初始默认值0,转换成现代时间,就是1970年1月1日

4. 数据随时间跳变而调整
手机系统时间会每秒发生变化,但也会发生跳变,常见的方式是2种,一是人为修改时间,二是系统时间通过网络自动校准(SIM卡或WiFi等)。

举个例子,手机第一次使用,未联网校准时,手机时间是错误的,可能显示为1970年3月25日,这时候用户在手机的操作,各个应用的上次使用时间肯定是被记录为1970年3月25日。但手机联网后,时间被通过网络校准为2017年11月29日。那这样会有一个情况?UsageStatsService中统计的时间仍然记录为1970年3月25日吗?显然不会的,Google的工程师想到了这一点,因此在UsageStatsService中有一个巧妙的机制,来保证记录时间的准确性。

  • A. UsageStatsService中有一个方法checkAndGetTimeLocked,此方法会在每次reportEvent记录应用事件时,获取系统时间,在获取的同时呢,它也记录了上一次使用的系统时间。通过差值计算,能够判定出系统的时间是否发生了跳变,例如人为的修改,通过网络进行的时间自动校准等。如果判定系统时间发生了跳变,UsageStatsService会调用onTimeChanged()方法,它会负责更新UsageStatsService记录的时间,以便他们能够跟随系统时间跳变,而相应更新
void onTimeChanged(long oldTime, long newTime) {
    persistActiveStats();
    mDatabase.onTimeChanged(newTime - oldTime);
    loadActiveStats(newTime);
}​
  • B. 这里的时间是直接存储在XML中吗?例如XML中Activity1,上次使用时间:2017年11月29日XX时XX分XX秒。不是这样的,这里的设计也它的特别之处,
    • 首先时间的存储全部是按毫秒来存储的,此毫秒也就是对比1970年1月1日,换算来的差值。
    • 其次时间的存储分为2部分,这里有一个公式,应用的上次使用时间 = XML文件名 + XML中此应用的上次使用时间。注意:XML文件的名字不是随便起的,是用某个基准时间的毫秒值来存储的。举个例子,XML文件名为:1511953275497,打开文件,其中的数据如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <usagestats version="1" endTime="2936118">
        <packages>
        <package lastTimeActive="2936118" package="com.android.settings" timeActive="78268" lastEvent="2" />
        .......  

那么在查询上次使用数据时,Settings的上次使用时间为:1511953275497 + 2936118 = 1511956211615,通过对比1970年1月1日,换算为日常时间是大约是2017/11/29 19:50:11。所以这就是手机中设置Settings上次的使用时间。

这样设计的好处是,当系统时间跳变时,只需要更新XML的文件名时间,XML中所有的值不需要逐条更新。那么通过算加法得到的时间,也就是正确的时间了。再举个例子:

    1. 用户手机时间为1970年3月25日,这时UsageStatsService中XML的文件名为:7142400,其中设置的上次使用时间,在XML中存储的值是10000,那么设置的上次使用时间是:7142400 + 10000 = 7152400,换算为正常时间是:1970年3月25日。(注意,举例中时间用得秒,并非毫秒,实际XML中存储的都是毫秒)
    1. 用户手机时间通过跳变,校准为2017年11月29日。这时UsageStatsService通过onTimeChanged方法,XML中文件名变为1511953275497,在XML中存储的值还是10000,那么设置的上次使用时间是: 1511953275 + 10000 = 1511963275,换算为正常时间是:2017年11月29日。(注意,举例中时间用得秒,并非毫秒,实际XML中存储的都是毫秒)

通过以上这样的机制,UsageStatsService中记录的时间,就会随着系统时间的跳变(人为修改或网络校准)而保持为正确的值。用户查询时也不会感到诧异。





作者:kevin song,2020.6.22于南京市建邺区

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342