PAYATHON 2026

FamilyActivitySelection token 能否长期可靠存储和使用?

排障赵师傅

结论

FamilyActivitySelection 及其 tokens 可以编码保存,但 token 不适合作为永久稳定的应用标识。后端保存的旧选择,也不能保证可以无限期恢复使用。

设计错误处理时,需要区分以下情况:

  • JSONDecoder 解码成功,只说明数据能还原成对应类型,不能证明 token 当前仍有效。
  • Apple 没有承诺 token 失效时一定抛出 DecodingError,因此不能靠解码异常检测 token 是否失效。
  • 给 store.shield.applications 等属性赋值时,没有逐个 token 的执行结果或错误回调。赋值完成不能证明目标应用已被屏蔽。
  • 回调 token 与保存值不相等,不等于旧 token 已无法应用 shield。

已有开发者报告,旧 token 仍能添加、移除 shield,并显示应用标签,却无法通过 == 与 ShieldConfigurationDataSource 收到的 token 匹配。因此,不能认定 token 一旦变化,旧值就一定会被静默忽略。相关问题记录

iOS 16、17、18、26 是否仍有问题?

多个版本都出现过 token 不匹配的报告,但这些记录不足以判断每个大版本及其所有补丁版本的情况。

版本可以确认的情况
iOS 162023 年已有同类报告,但帖子没有明确列出系统构建号,不能仅凭发帖时间判断具体哪个 iOS 16 版本受影响。早期报告
iOS 17有明确针对 17.4.1、17.5.1 的报告,表现为扩展回调 token 与用户选择时保存的 token 不匹配。版本报告
iOS 18陆续有 18.1 beta、18.4 beta 4、18.5 beta 1 的报告;Apple 工程师也在该讨论中确认过问题仍存在。不过,这些记录并未验证所有 iOS 18 正式版本。持续跟踪讨论
iOS 26除 beta 阶段外,26.2.1 也有明确的不匹配报告,不能认为升级到 iOS 26 就能解决问题。26.2.1 报告

Apple DTS 在上述跟踪讨论中将其认定为 bug,跟踪编号为 FB14111223。DTS 当时说明,由于 tokens 是不透明值,没有代码层面的绕过方案。DTS 回复

这些记录主要涉及 ApplicationToken,也有涉及 WebDomainToken 的情况,不能据此断定 ActivityCategoryToken 在所有版本中都有完全相同的故障表现。

至于是否有官方处理 API,需要结合版本判断。Apple 当前文档列出了分别用于刷新应用、类别和网站 tokens 的 ManagedSettingsStore.refresh(_:),以及 TokenExpiryMessage 过期消息。接入前应核对所用 SDK 的 availability 声明和部署目标。这些能力不能直接套用到所有 iOS 16/17/18/26 版本,也不能证明历史上的不匹配问题已全部修复。刷新 API、过期消息

为什么解码成功不能证明 token 有效?

JSON 解码、写入 shield 设置和扩展回调匹配,各自能确认的事情不同:

阶段成功能说明什么不能说明什么
JSON 解码当前解码器能从数据构造对象token 仍被系统认可
写入 shield 设置应用已提交配置每个目标都已受到限制
扩展回调匹配当前 token 能匹配本地记录这种匹配关系会永久保持

Codable 负责外部表示与对象之间的转换。JSON 损坏、字段缺失、类型不匹配等问题可能导致解码异常。文档并未要求 JSONDecoder 检查 token 的授权状态或系统内部映射是否仍然有效。Codable 文档

代码需要处理数据仍能解码、后续行为却不符合预期的情况。遇到 DecodingError 时,也不能直接归因为 token 过期。

ManagedSettingsStore 文档还明确说明,系统不保证应用指定的设置一定决定设备行为;最终有效状态由系统根据收到的设置确定。因此,即使能读回 store.shield.applications,也不能据此确认实际拦截效果。ManagedSettingsStore 文档

存储和恢复流程应怎样调整?

用自己的 ID 标识 profile

给每个 profile 分配独立的 UUID 或后端 ID,用来关联调度、设置和用户可见名称。tokens 则作为该 profile 的选择数据保存。

以下内容都不适合作为长期业务主键:

  • token 的 hashValue;
  • token 编码后的 JSON 或 Base64;
  • 从 token 内部数据猜测出的应用标识。

将旧数据保存在后端不会延长 token 的有效期。恢复当前设备的调度时,建议提前把已同步的选择写入 App Group,供 DeviceActivityMonitorExtension 读取,避免调度触发后还要等待网络请求完成。

单独处理授权撤销

Apple 明确说明,用户、家长或监护人撤销授权后,授权期间通过 FamilyActivitySelection 获得的 tokens 会失效。这是文档规定的生命周期,应与异常的 token 不匹配问题分开处理。FamilyActivitySelection 文档

主应用应观察 AuthorizationCenter.shared 的授权变化。确认授权被撤销后,将旧选择标记为需要重新选择。恢复授权后,也不要直接把旧数据重新标记为有效。当前状态为 .approved,并不能证明历史 token 从未经历过授权撤销。授权状态文档

处理无法匹配的回调

ShieldConfigurationDataSource 收到无法匹配的 token 时,应返回通用 shield UI,并记录诊断信息。

ShieldActionDelegate 也应完成回调。如果无法确定 token 所属的 profile,应执行产品预先定义的保守动作,例如 .close,避免误清除其他 profile 的限制。

未知 token 只能作为异常线索。排查时还要检查共享数据是否过旧、profile 是否已删除,以及当前回调是否对应类别屏蔽,不能一律判定为 token 已过期。

在需要恢复时引导用户重新选择

论坛中有开发者通过在 FamilyActivityPicker 中取消并重新选择受影响项目来恢复功能。不过,这种办法可能只暂时有效,不能保证问题不再出现。恢复办法及局限

无需把每次重新启用 profile 都强制重新选择设为框架要求。可以在确认授权撤销、持续无法匹配,或用户反馈限制异常时,引导用户重新选择。选择更新后,应同步保存,并更新引用旧 tokens 的相关监控配置。

代码示例:区分解码与提交配置

示例只涵盖应用和应用类别的恢复。submitShieldConfiguration 正常返回,表示解码完成且配置已提交,不代表系统已经验证或执行了每个 token。

import Foundation
import FamilyControls
import ManagedSettings

struct SavedProfileSelection: Codable {
    let schemaVersion: Int
    let profileID: UUID
    let selection: FamilyActivitySelection
}

enum SelectionRestoreError: Error {
    case unsupportedSchema(Int)
}

func encodeSelection(
    profileID: UUID,
    selection: FamilyActivitySelection
) throws -> Data {
    let snapshot = SavedProfileSelection(
        schemaVersion: 1,
        profileID: profileID,
        selection: selection
    )

    return try JSONEncoder().encode(snapshot)
}

func submitShieldConfiguration(
    from data: Data,
    to store: ManagedSettingsStore
) throws {
    let snapshot = try JSONDecoder().decode(
        SavedProfileSelection.self,
        from: data
    )

    guard snapshot.schemaVersion == 1 else {
        throw SelectionRestoreError.unsupportedSchema(
            snapshot.schemaVersion
        )
    }

    let selection = snapshot.selection

    store.shield.applications =
        selection.applicationTokens.isEmpty
        ? nil
        : selection.applicationTokens

    store.shield.applicationCategories =
        selection.categoryTokens.isEmpty
        ? nil
        : .specific(selection.categoryTokens)
}

调用方需要分别处理数据读取失败、DecodingError 和自定义 schema 错误。不要用 try? 忽略所有错误,也不要把它们统一展示为“token 已过期”。

schemaVersion 只用于管理自己的数据结构,无法刷新 token。传入的 store 应与该 profile 的调度关系一致。多个 profile 并行工作时,要明确各自使用的 store 和清理范围。

排查时需要记录什么?

建议至少记录系统版本及构建号、应用版本、profile ID、选择数据修订号和授权变化。同时记录调度回调是否执行、数据是否解码成功,以及提交 shield 配置的时间。

发生不匹配时,记录回调类型和匹配结果即可,日常日志应避免直接输出完整 token。如果能够复现,应及时采集 sysdiagnose,通过 Feedback Assistant 提交,并关联已有问题编号。

验证时,先确认扩展按时执行,再确认数据恢复,最后在真机上检查实际屏蔽效果和 shield 交互。JSON 能解码、store 中能读到 token,都不足以确认长期使用的可靠性。

备注:内容仅供参考。