Skip to content

PackageUsage 时间记录

追踪 Android 17 包使用时间的 Binder 通知、reason 分桶、低成本更新和启动恢复。

基于android-17.0.0_r1
AndroidPMSPackageUsagePackageStatePersistence

PackageUsage 时间记录 ​

本文面向已经读过 PMS Binder 接口 和 PackageSetting 数据结构 的读者。它只讨论“包什么时候被使用过”这组时间数据,不讨论 dex 使用记录、UsageStats 的事件数据库或应用休眠策略。读完后,读者应能从 notifyPackageUse() 定位调用者校验、解释 8 种 reason 如何进入数组,并判断为什么 usage 更新不会让所有 Computer snapshot 失效。

1. 两个 owner ​

源码文件:

  • frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
  • frameworks/base/services/core/java/com/android/server/pm/PackageUsage.java
  • frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java

包的使用时间分成“内存状态”和“磁盘格式”两个层次。PackageStateUnserialized 挂在 PackageSetting 上,保存每个 reason 的最近时间;PackageUsage 负责把所有包的数组写入 package-usage.list。文件不是第二个状态 owner,而是这组 transient 字段的独立持久化通道。

2. Reason 分桶 ​

源码文件:frameworks/base/core/java/android/content/pm/PackageManager.java。

Android 17 定义 8 个 NOTIFY_PACKAGE_USE_* reason:activity、service、foreground service、broadcast receiver、content provider、backup、cross-package 和 instrumentation,编号 0 到 7;NOTIFY_PACKAGE_USE_REASONS_COUNT 为 8。

java
public static final int NOTIFY_PACKAGE_USE_ACTIVITY = 0;
public static final int NOTIFY_PACKAGE_USE_SERVICE = 1;
public static final int NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE = 2;
public static final int NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER = 3;
public static final int NOTIFY_PACKAGE_USE_CONTENT_PROVIDER = 4;
public static final int NOTIFY_PACKAGE_USE_BACKUP = 5;
public static final int NOTIFY_PACKAGE_USE_CROSS_PACKAGE = 6;
public static final int NOTIFY_PACKAGE_USE_INSTRUMENTATION = 7;
public static final int NOTIFY_PACKAGE_USE_REASONS_COUNT = 8;

分桶的价值是让消费者区分“最近被启动”“最近被 Provider 访问”等不同活动。它不是计数器,也不保存事件序列;同一 reason 只保留最后一次时间。

3. Binder 通知 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:IPackageManagerImpl.notifyPackageUse。

java
public void notifyPackageUse(String packageName, int reason) {
    final int callingUid = Binder.getCallingUid();
    final int callingUserId = UserHandle.getUserId(callingUid);
    Computer snapshot = snapshotComputer();
    final boolean notify;
    if (snapshot.getInstantAppPackageName(callingUid) != null) {
        notify = snapshot.isCallerSameApp(packageName, callingUid);
    } else {
        notify = !snapshot.isInstantAppInternal(
                packageName, callingUserId, Process.SYSTEM_UID);
    }
    if (!notify) {
        return;
    }
    notifyPackageUseInternal(packageName, reason);
}

该接口没有返回值,调用方也不会得到“已写入”的确认。PMS 只允许 instant app 报告自己;普通调用者不能借此报告不可见的 instant target。普通包的通知则拒绝目标为 instant app 的情况,避免把 instant 生命周期污染到持久 usage 数据。

4. 时间数组 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java、frameworks/base/services/core/java/com/android/server/pm/pkg/PackageStateUnserialized.java。

java
private void notifyPackageUseInternal(String packageName, int reason) {
    long time = System.currentTimeMillis();
    synchronized (mLock) {
        final PackageSetting pkgSetting = mSettings.getPackageLPr(packageName);
        if (pkgSetting == null) {
            return;
        }
        pkgSetting.getPkgState().setLastPackageUsageTimeInMills(reason, time);
    }
}

PackageStateUnserialized.setLastPackageUsageTimeInMills 会懒初始化长度为 8 的数组,并对 reason 做边界检查;负数或大于等于 8 的值被忽略。时间使用 wall clock 的 System.currentTimeMillis(),不是 uptime,因此可跨重启保存,但会受系统时钟调整影响。

java
public PackageStateUnserialized setLastPackageUsageTimeInMills(
        int reason, long time) {
    if (reason < 0 || reason >= PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT) {
        return this;
    }
    getLastPackageUsageTimeInMills()[reason] = time;
    return this;
}

5. Snapshot 边界 ​

usage 通知可能来自高频的 Activity、Service、Provider 访问。如果每次都调用 PackageSetting.onChanged(),会让完整 package snapshot、resolver 和可见性缓存持续失效。Android 17 把这些时间放在 PackageStateUnserialized,更新数组但不触发常规 package state change;需要最新 usage 的消费者直接读取该 transient 状态或等待 PackageUsage 落盘。

这带来一个明确边界:一个已经创建的 Computer snapshot 不保证包含之后的最新 usage 时间。snapshot 保证包结构和用户状态的一致性,不是所有运行统计字段的时间点快照。

6. 文件格式 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageUsage.java。

PackageUsage 继承 AbstractStatsBase,使用 AtomicFile 写入 package-usage.list。文件头为 PACKAGE_USAGE__VERSION_1,每个后续行包含包名和 8 个十进制时间戳:

text
PACKAGE_USAGE__VERSION_1
com.example.app 1710000000000 0 0 0 1710000001234 0 0 0

写入时跳过 null package、没有 PkgState 的条目和 getLatestPackageUseTimeInMills() == 0 的包;文件权限设为 0640,owner 为 system,group 为 PACKAGE_INFO_GID。AtomicFile.finishWrite 成功后替换旧文件,异常则 failWrite 保留备份。

java
if (pkgSetting == null || pkgSetting.getPkgState() == null
        || pkgSetting.getPkgState().getLatestPackageUseTimeInMills() == 0L) {
    continue;
}

7. 写入时机 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java。

PackageUsage 不在每次 notifyPackageUse 后立即写文件。PMS 在 shutdown 中调用:

java
synchronized (mLock) {
    mPackageUsage.writeNow(mSettings.getPackagesLocked());
    // 其后继续同步 settings / restrictions 文件
}

具体的 AbstractStatsBase 调度还可能由其他 PMS 路径触发,但 shutdown 的 writeNow 是明确的最后落盘屏障。异常掉电时,最近一段 usage 可能只存在内存中;这不会破坏包安装状态,因为 usage 不是 packages.xml 的事务字段。

8. 启动恢复 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java、frameworks/base/services/core/java/com/android/server/pm/PackageUsage.java。

PMS 完成包扫描、确定保留的 package settings 后调用 mPackageUsage.read(packageSettings)。读取版本头后,每行必须包含包名加 8 个 token;格式错误会停止解析并记录 warning。文件中已经卸载的包会被跳过,仍存在的包把 8 个时间写回 PackageStateUnserialized。

java
PackageSetting pkgSetting = pkgSettings.get(packageName);
if (pkgSetting == null) {
    continue;
}
for (int reason = 0;
        reason < PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT;
        reason++) {
    pkgSetting.getPkgState().setLastPackageUsageTimeInMills(
            reason, parseAsLong(tokens[reason + 1]));
}

读取发生在扫描确定包身份之后,因此 usage 文件不能反向创建一个不存在的 PackageSetting。文件缺失是正常的首次启动场景;文件损坏只影响 usage 恢复,不会让包扫描整体回滚。

9. 消费者边界 ​

usage 时间可被包管理策略用于最近使用排序、后台优化或调试输出,但它不是 DexUseManagerLocal 的 secondary dex 记录,也不是 UsageStats 的完整事件历史。getLatestPackageUseTimeInMills() 只是 8 个槽位的最大值;如果需要知道哪种行为触发了最近使用,必须读取对应 reason 槽位。

调试时应把三类时间分开:wall-clock package usage、uptime-based watchdog 时间、ART dex use 的 last-used 时间。它们的时钟源、持久化文件和消费者都不同,不能直接比较毫秒值。

10. 失败定位 ​

现象检查点解释
通知后数组不变reason 范围、instant app 校验、目标包是否存在非法 reason 和不允许的目标会静默返回
查询看到旧时间snapshot 创建时点usage 更新不触发完整 snapshot invalidation
重启后最近使用时间丢失PackageUsage.writeNow、AtomicFile、异常关机高频更新可能尚未落盘
文件读取失败版本头、每行 token 数、long 解析只影响 usage 恢复,包状态仍可继续启动
某包行被忽略pkgSettings.get(packageName)文件不能为已卸载包重新创建状态
最近使用时间异常跳变System.currentTimeMillis()系统时钟校准会影响 wall-clock 数据

11. 源码练习 ​

  1. 从 notifyPackageUse 追踪 instant app 与普通包的两条校验分支,说明为什么它们的目标包规则相反。
  2. 给定 8 个 reason 中只有 Provider 槽位非零,判断 getLatestPackageUseTimeInMills() 返回什么,以及如何定位具体 reason。
  3. 构造一个包含 7 个时间戳的损坏行,追踪 PackageUsage.readVersion1LP 的异常边界,并说明为什么不会创建新的 PackageSetting。
  4. 比较 usage 更新、PackageStateMutator 状态更新和 DexUseManagerLocal 更新,列出各自的锁、文件和 snapshot 影响。