ShortcutManager 的动态快捷方式,真的有人用吗

ShortcutManager 的动态快捷方式,真的有人用吗

ShortcutManager 的动态快捷方式,真的有人用吗


ShortcutManager 的动态快捷方式,真的有人用吗


被忽视的 API:从 Android 7.1 到今天的尴尬地位


ShortcutManager 这个 API 在 Android 7.1(API 25)引入,到现在已经横跨了八个大版本。静态快捷方式通过 XML 声明,动态快捷方式通过 ShortcutManager 运行时添加,这看起来是个很自然的分层设计。但我在实际项目里尝试深度使用动态快捷方式后,越来越怀疑这个 API 的真实可用性——不是技术层面不能用,而是"用了之后值不值"这件事。


让我先从一个具体的崩溃说起。去年维护的一个项目,targetSdk 升到 33 之后,收到少量 but 致命的崩溃:IllegalStateException: Max number of dynamic shortcuts exceeded。堆栈指向我们调用 pushDynamicShortcut() 的地方。这个 API 在 Android 11(API 30)引入,语义是"推送一个动态快捷方式,如果超出上限则按 rank 淘汰最老的"。看起来比 addDynamicShortcuts() 更智能,但问题出在它的淘汰策略和开发者的预期并不一致。


Android 对动态快捷方式的数量限制是 getMaxShortcutCountPerActivity() 返回的值,这个值在绝大多数设备上是 4 或 5。注意是 per activity,不是 per app。如果你的应用有多个 launcher activity,理论上每个都能有独立的配额,但实际实现中很多厂商把这个限制做成了全局的。更坑的是,这个限制在 Android 8.0(API 26)之前是硬编码的,没有公开 API 查询。


// 获取限制,但返回值不可完全信任
int max = getSystemService(ShortcutManager.class).getMaxShortcutCountPerActivity();
// 某些 OEM 设备上,这个值可能是 5,但实际允许添加的只有 4

我们当时的场景是:用户可以在应用内"收藏"多个项目,每个收藏会对应一个动态快捷方式。收藏数超过 5 个时,按说 pushDynamicShortcut() 应该自动淘汰 rank 最低的。但崩溃表明,在某些设备上(主要是 Samsung One UI 4.x 和 5.x),系统没有正确执行淘汰,而是直接抛异常。翻 Samsung 的开发者论坛,发现这是个已知问题,2022 年有人报过,到 2023 年仍未完全修复。


这引出了动态快捷方式的第一个深层问题:它的行为严重依赖 OEM 实现,而 Google 的 CTS 测试对这个 API 的覆盖并不充分。


rank 机制:文档没说清的细节


动态快捷方式有个 setRank() 方法,用于控制排序。文档说 rank 非负,值越高排名越靠前。但实际行为远比这复杂。


我在 Pixel 7(Android 13)和 Samsung S23(Android 13/14)上做了对比测试。创建 5 个动态快捷方式,rank 分别设为 0, 1, 2, 3, 4,然后观察长按图标时弹出的快捷方式列表顺序。Pixel 上完全按 rank 倒序排列,4 在最上面。Samsung S23 上,rank 似乎只影响同组内的排序,系统还会混入"使用频率"因子,导致 rank 4 的项可能出现在 rank 3 下面。


更诡异的是 setLongLived()。这个属性在 Android 11 引入,标记为 true 的快捷方式在 pushDynamicShortcut() 淘汰时会被优先保留。但 "long lived" 的保留优先级和 rank 的交互关系,文档没有明确说明。我实测的结果是:在 Pixel 上,long lived 且 rank 高的确实稳居前位;在小米 13(MIUI 14/Android 13)上,long lived 的项会被系统强制折叠到一个"更多"二级菜单里,即使 rank 设到最高也不出现在首屏。


这意味着如果你想做跨设备的可预测排序,基本不可能。动态快捷方式的"动态"二字,在系统层已经被各种 OEM 策略稀释得差不多了。


更新频率的隐形天花板


动态快捷方式有个容易被忽视的约束:reportShortcutUsed()updateShortcuts() 的调用频率。官方文档提到"频繁调用可能导致系统限制",但没有给出具体阈值。


我在测试里写了个循环,每秒调用一次 updateShortcuts() 更新某个快捷方式的图标。在 Pixel 7 上,大约第 20 次更新后,updateShortcuts() 开始静默失败——返回 true,但实际图标没有变化。没有异常,没有日志,完全静默。这个状态会持续大约 30 秒,然后自动恢复。


// 这种"实时更新"的想法在实践中行不通
for (int i = 0; i < 100; i++) {
    shortcut.setIcon(loadIconForIndex(i));
    manager.updateShortcuts(Arrays.asList(shortcut));
    Thread.sleep(1000); // 即使放慢到 5 秒,Pixel 上大约 50 次后也会触发限制
}

这个限制没有 API 查询,没有文档说明,全靠黑箱测试推断。对于想拿动态快捷方式做"最近播放"或"实时状态"展示的场景,这几乎是致命的。你没法告诉用户"快捷方式暂时冻结了",因为应用层完全感知不到这个状态。


相比之下,通知渠道(Notification Channel)虽然也有频率限制,但至少 NotificationManager.areNotificationsEnabled()getNotificationChannel() 给了明确的查询能力。ShortcutManager 在这方面像个半成品。


图标适配:Vector Drawable 的陷阱


动态快捷方式的图标通过 Icon.createWithBitmap()Icon.createWithResource() 设置。后者支持 vector drawable,但这里有个版本差异的坑。


Android 8.0(API 26)之前,Icon.createWithResource() 传入的 drawable 如果是 vector,在某些设备上会直接崩溃。Android 8.0 到 10 之间,vector 会被系统栅格化,但栅格化的尺寸固定为 48dp,如果你的 vector 使用了 android:widthandroid:height 之外的尺寸定义,可能显示模糊。Android 11 开始支持自适应图标,但 ShortcutInfo.BuildersetIcon() 接受的 Icon 对象并不直接支持 AdaptiveIconDrawable,你需要手动提取 foreground 和 background 合成 bitmap。


我们当时的做法是统一走 Icon.createWithBitmap(),自己用 Canvas 把 vector 画成指定大小的 bitmap。这引入了额外的内存和 CPU 开销,而且 bitmap 尺寸该选多大也是个问题。系统 launcher 可能会在不同场景显示不同尺寸的快捷方式图标:长按菜单里一种,拖拽到桌面后另一种,Android 12 的 splash screen 可能又一种。没有统一标准。


// 我们最终采用的"安全"做法,但明显不优雅
Bitmap bitmap = Bitmap.createBitmap(192, 192, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_shortcut);
drawable.setBounds(0, 0, 192, 192);
drawable.draw(canvas);
Icon icon = Icon.createWithBitmap(bitmap);

这个 192x192 是猜的。Android 12 的 AdaptiveIconDrawable 规范里提到 108dp 的 safe zone,但快捷方式图标没有对应的官方尺寸指导。实际测试中,192x192 在大多数设备上足够清晰,但在某些平板上(Samsung Tab S8,2560x1600 分辨率)仍然能看到轻微锯齿。


意图解析:被系统改写的 action


动态快捷方式的点击行为通过 setIntent() 设置。这里有个行为差异:静态快捷方式的 intent 会原样传递到 activity,而动态快捷方式的 intent 可能被系统 launcher 改写。


具体来说,如果你设置了 Intent.ACTION_MAINCATEGORY_LAUNCHER,某些 launcher(包括 Pixel 原生的 Launcher3)会额外添加 FLAG_ACTIVITY_NEW_TASKFLAG_ACTIVITY_CLEAR_TOP。这在大多数情况下没问题,但如果你的应用有多个 task affinity 的 activity,或者依赖特定的 launch mode,就可能出现 activity 栈被意外重置的情况。


我遇到的具体场景:应用有个 singleTask 的 MainActivity,和一个 standard 的 DetailActivity。动态快捷方式指向 DetailActivity,期望的行为是如果 MainActivity 已在后台,则在其 task 上启动 DetailActivity。实际在 OnePlus 11(OxygenOS 13)上,点击快捷方式会新建一个 task 启动 DetailActivity,MainActivity 的 task 被留在后台。这是因为系统 launcher 给 intent 加了 FLAG_ACTIVITY_NEW_TASK,而 singleTask 的 MainActivity 没有参与解析,导致 DetailActivity 被放到了默认 task。


修复方案是在 DetailActivity 也设 taskAffinity 与 MainActivity 一致,并且处理 onNewIntent()。但这属于"为了适配快捷方式而改动应用架构",成本明显不对等。


权限与可见性:Android 13 的变更


Android 13(API 33)引入了 POST_NOTIFICATIONS 运行时权限,很多人忽略了它对快捷方式的连带影响。通知和快捷方式在系统 UI 层面是耦合的——长按应用图标弹出的菜单里,快捷方式和通知渠道设置并列显示。如果用户拒绝了通知权限,某些系统 launcher 会隐藏或弱化快捷方式的展示。


在 Pixel 上测试:拒绝通知权限后,动态快捷方式仍然显示,但图标被加上了灰色遮罩,暗示"这个应用的通知被关闭了"。在 Samsung One UI 5.1 上,拒绝通知权限后,动态快捷方式直接从长按菜单消失,静态快捷方式保留。这个行为差异没有文档说明,也不是 CTS 的一部分。


更隐蔽的是 ShortcutManager.getDynamicShortcuts() 的返回值。即使系统 UI 隐藏了快捷方式,这个 API 仍然返回它们,状态是 ENABLED。应用层无法区分"用户看得见"和"用户看不见"的状态。没有对应的 listener 或回调来通知可见性变化。


实际采用率:数据层面的冷思考


Google 在 Android 开发者峰会 2022 提到,使用动态快捷方式的应用数量"稳步增长",但没有给出具体数字。我从另一个角度观察:AOSP 的 Launcher3 代码里,动态快捷方式的实现代码在过去三年几乎没有功能级更新,主要是 bugfix 和适配新主题。作为对比,App Widget 的代码在 Android 12/13 有大规模重构(引入 targetCellWidth/targetCellHeight、新的预览协议),通知系统在 Android 13 有权限模型革新。ShortcutManager 的"被忽视"是肉眼可见的。


第三方 launcher 的支持更是参差不齐。Nova Launcher 支持动态快捷方式,但 rank 排序完全按自己的逻辑;Microsoft Launcher 在 2022 年某个版本曾完全禁用动态快捷方式,原因是"性能问题",后来恢复但限制了每个应用的显示数量。


从应用开发者的角度,我查了几个知名应用的实现。Twitter(现在的 X)在 2021 年之前有动态快捷方式"新建推文",后来移除了;Spotify 的"最近播放"列表在 Android 上用的是静态快捷方式配合定期更新,而非动态 API;只有 Google 自己的应用(Gmail、Drive、Photos)相对积极地使用动态快捷方式,但它们的实现深度绑定 GMS,有系统级特权。


替代方案:为什么 Widget 反而更可靠


如果动态快捷方式的问题这么多,那替代方案是什么?对于"展示可变内容"这个需求,App Widget 反而在 Android 12 之后变得更可用了。


RemoteViews 虽然还是老架构,但 targetCellWidth/targetCellHeight 让尺寸适配更精确;setExact() 配合 WorkManager 可以实现准实时的内容更新;最重要的是,Widget 的更新频率限制是明确的——updatePeriodMillis 最小 30 分钟,或者通过 broadcast 主动触发——开发者可以预期和规划。


对于"快速入口"这个需求,通知的 direct reply action 或 bubble 可能是更好的选择。通知有成熟的渠道管理、权限模型、用户控制界面,而且跨设备一致性远好于快捷方式。


我并不是说动态快捷方式完全没用。对于"固定不超过 4 个、内容变化不频繁、不需要精确排序"的场景,比如"最近打开的文件"或"收藏的联系人",它仍然是个轻量选项。但"轻量"的另一面是"能力边界模糊",你很难确定哪些行为是跨设备稳定的。


一个未文档化的行为:备份与恢复


最后说个冷门的。Android 的自动备份机制(android:allowBackup="true")会保存动态快捷方式吗?文档说会备份 ShortcutManager 的数据,但实际测试发现,在 Android 12+ 的云端备份中,动态快捷方式经常丢失,静态快捷方式保留。


根因是动态快捷方式的 intent 可能包含 file:// URI 或指向应用私有数据的 extra,这些在跨设备恢复时无法解析。系统备份服务会选择性丢弃"看起来不可恢复"的快捷方式,但判断逻辑没有文档。我们曾在应用内用 ShortcutManager.getDynamicShortcuts() 在恢复后检查,发现列表为空,但用户明明之前添加过。


没有回调通知这个丢失。应用需要自己维护一份"应该有哪些动态快捷方式"的元数据,在 BackupAgent 或启动时做一致性检查。这又把一个"系统应该处理"的事情推给了应用层。


写在最后的个人判断


回顾这些踩坑经历,我对 ShortcutManager 的动态快捷方式持谨慎态度。它不像某些 API 是"设计坏了",而是"设计得太理想化,假设了一个所有 launcher 行为一致、所有 OEM 严格遵循 CTS、所有用户都理解快捷方式语义的世界"。这个世界不存在。


如果 Google 真的想让这个 API 更有用,至少需要:公开查询当前 launcher 对快捷方式的支持特性;给 updateShortcuts() 明确的速率限制和状态反馈;统一 long lived 和 rank 的交互语义;提供可见性变化的回调。但看 AOSP 的提交历史,这些改进的优先级都不高。


所以回到标题的问题:动态快捷方式真的有人用吗?有,但用得好的往往是系统级应用或深度绑定特定 launcher 的。对于普通第三方应用,投入产出比很难算得过来。我倾向于把动态快捷方式当作"锦上添花"而非"核心功能",做好静态快捷方式,动态部分用通知或 Widget 兜底,可能是更务实的选择。

kotlinx.serialization 的稳定性,生产环境敢用吗 2026-07-09

评论区