WhatsApp Wear OS独立应用架构迁移与UI刷新实战

WhatsApp 分享 Wear OS 应用从 Rust 客户端迁回 Android 共享代码库的架构、通知、UI 与更新实践。
WhatsApp 团队详细分享了将 Wear OS 应用从独立的 Rust 便携客户端迁移到 Android 共享代码库的历程。迁移的核心动机是消除第二套独立实现带来的协议与功能重复,并借助 Android 系统成熟的后台管理机制。团队通过依赖注入的可选绑定与多绑定实现功能模块的按需构建,但解耦也带来了功能静默缺失的风险,需用测试比对绑定集合来兜底。在通知层面,他们动态开关通知桥接以避免重复震动,区分提醒与静默更新事件,并将直接回复视为端到端工作流。UI 迁移到 Material 3 时,团队解决了变换重复应用和位置缓存导致的错位问题。最后,针对应用内置的 90 天过期机制,他们设计了多节点更新提醒,确保用户实际收到新版本。
在智能手表上运行独立应用,远比把手机界面缩小复杂得多。WhatsApp 团队近期分享了他们从 Rust 便携客户端迁移到 Android 共享代码库,并在此基础上重构 Wear OS 体验的完整历程。两位工程师 Martin 和 Subhaag 的分享,揭示了独立手表应用在架构、通知、UI 和更新机制上的独特挑战与解法。
为什么放弃 Rust 便携客户端?
WhatsApp 在 Wear OS 上的首次亮相依赖于一个用 Rust 编写的紧凑便携客户端。这种方案让团队快速进入了生产环境,但代价是它成为了 WhatsApp 的第二个独立实现。这意味着每个协议、加密逻辑和功能都需要构建两次,手表应用始终处于追赶状态。
更棘手的是后台管理。便携客户端需要自行处理线程、定时器和套接字,而这些原本应该由 Android 系统管理。Android 对后台执行有严格规则:进入 Doze 模式会失去网络,进程被缓存后执行时间受限,必须依赖服务、Job 或推送来突破限制。手表应用不得不手动接入这些机制,而团队的 Android 核心库早已处理好这一切,并在数十亿设备上得到了充分测试。
迁移目标很明确:保留业务逻辑,移除 UI。最终留下的是一个无头库,以 Kotlin Flows 作为 API 暴露给 UI 层。但面对如此庞大的应用,不可能一次性完成拆解——有些模块将 UI 与基础设施混杂在一起,有些则依赖手表端根本不需要的功能。团队因此并行推进两条路径:持续模块化,以及剥离或覆写暂时无法移除的部分。

Doze 模式是 Android 6.0 引入的低电耗模式:当设备未充电且屏幕关闭一段时间后,系统会推迟后台 CPU 和网络活动,以延长续航。Wear OS 设备电池容量更小,Doze 的触发阈值通常更激进,这对需要保持实时消息推送的手表应用构成了直接挑战。应用若要在 Doze 下继续工作,必须使用高优先级 Firebase Cloud Messaging 消息、前台服务或精确的闹钟,但这些机制都受到系统严格限制。WhatsApp 的便携客户端由于自行管理套接字和定时器,必须逐一适配这些规则,而 Android 核心库已经将这些机制封装为经过大规模验证的抽象,这也是团队决定迁回共享代码库的重要动因。
模块化与依赖注入的取舍
为了将代码排除在构建之外,简单的条件语句并不够用,因为它无法真正移除代码。团队重度依赖依赖注入(DI)的可选绑定和多绑定机制。通过可选绑定,依赖可以被声明为可空,例如调用后端;如果手表构建包含提供实现的模块,注入的后端就存在且调用有效;如果省略该模块,注入值为 null,API 可以返回不允许。多绑定原理相同,但针对集合——消息处理器集合可以声明为多绑定,每个功能模块通过 into set 添加自己的处理器。如果不支持支付,整个支付模块可以不出现在构建中,注入的集合自然不会包含对应处理器。
这种解耦是一把双刃剑。直接引用时,删除模块会破坏构建;而使用可选或多绑定后,没有任何东西引用该模块,构建会通过,功能悄然消失或回退到默认行为。团队就曾踩过这个坑:重构将某个功能移入独立模块,但手机应用和手表应用都没有包含它,结果语音消息的时长显示为零秒。解决方案是补上测试——比较手机应用和手表应用之间关键接口的多绑定集合,确保正确的实现始终被绑定。
依赖注入中的“可选绑定”与“多绑定”是 Dagger/Hilt 等框架提供的两种声明方式。可选绑定允许某个依赖被声明为可空,当对应模块未包含在构建中时注入 null,调用方可以据此判断功能是否可用。多绑定则用于将多个实现聚合到一个集合中,例如 @IntoSet 可以把每个功能模块贡献的处理器收集到同一个 Set<MessageHandler> 里。这两种机制使得构建系统可以真正移除未使用的模块及其代码,而不是靠运行时条件分支屏蔽功能,从而减小安装包体积并降低攻击面。但代价是编译期无法发现模块缺失,因此需要额外的测试或静态检查来保证绑定集合的完整性。
清单与资源的优先级博弈
复用模块能节省工作量,但模块声明的所有内容都会合并进清单文件。手机的启动 Activity 会添加一个多余的应用图标,不必要的内容提供者也会随应用启动。清单合并工具提供了 tools:node="remove" 属性来移除组件,移除后压缩器也能一并移除代码。如果仍有代码需要读取该 Activity,可以用 activity alias 重定向到新类。但需要注意,这些属性只对低优先级清单生效,合并顺序决定了某样东西是否被移除。
资源也是同样的逻辑。复用带来了免费的手机字符串,但有些面向手机的功能手表无法实现。例如每条聊天开头的加密提示,在手机上以“了解更多”链接结尾,但手表上链接背后空无一物。团队在手表的高优先级模块中声明相同 key,将字符串替换为“请在手机上了解更多”,资源合并会优先采用手表的版本。他们还构建了一个检查工具,确保覆盖被显式列出,否则构建失败。

通知:从重复震动到静默更新
默认情况下,Wear OS 会桥接或镜像手机通知。这很好用,但前提是手机保持连接。WhatsApp 希望独立运作,因此应用会发布自己的通知。但如果桥接保持开启,每条消息都会震动两次。关闭桥接看似简单,但静态清单标志从安装时就生效,早于用户登录——如果用户安装后没有登录,就会突然失去所有通知。
团队在运行时通过桥接管理器来设置:登录时关闭桥接,登出时重新开启。但并没有 API 可以读回该值,而且他们观察到手表会丢失这个设置。于是他们在手表离开充电器或应用更新时重新写入。
另一个通知痛点是重复提醒。此前,头像更新、媒体下载和回执这些本应静默更新通知的事件,却都在提醒用户。修复后,新消息到达时手表只震动一次,其他非提醒事件则静默刷新通知。团队强调,当通知是核心用户旅程之一时,必须确保每次提醒都真正值得用户注意。

现在,WhatsApp 的 Wear OS 通知还支持直接回复。借助 Android 的通知动作与远程输入 API,用户可以直接输入、语音或点击快速回复。发送成功后会标记聊天已读并清除通知;发送失败则提供单击重试。团队将这一能力视为完整的端到端工作流,而非简单的按钮。
Wear OS 的通知桥接机制默认会将手机上的通知镜像到手表,前提是手机与手表保持蓝牙或 Wi-Fi 连接。桥接由系统级服务管理,应用可以通过 NotificationBridgeManager 在运行时请求关闭或开启桥接,但该 API 只提供写入能力,没有对应的读取接口来确认当前状态。此外,部分 OEM 设备将桥接状态与通知权限绑定,权限重置可能导致桥接被意外关闭。因此,WhatsApp 团队选择在登录、登出、手表离开充电器以及应用更新等关键节点重新写入桥接状态,以弥补系统未提供状态回读和持久化保证的缺陷。
UI 迁移:不只是换皮
共享基础设施就位后,团队将注意力转向让 WhatsApp 在手腕上真正有归属感。第一步是迁移到 Material 3。在圆形屏幕上,每个像素都很宝贵,Material 3 提供了合适的组件和设计基础。但设计系统迁移从来不只是视觉迁移,更是行为迁移。新旧代码需要在开关背后并存,那些视觉上与 Figma 匹配的屏幕仍然表现出滚动、右滑返回等基础手势失效的问题。
动态屏幕让事情更复杂。消息到达时行会移动,气泡形状各异。团队遇到了两个典型问题。第一,气泡卡片的变换被应用了两次——父卡片应用变换后,又传递给了子气泡,导致子气泡承受了双倍变换。修复方式是只在父卡片层级应用一次变换,确保“一项变换只作用于一项”。第二,小号淡色气泡出现在屏幕中央本应全尺寸的位置。原因是尺寸设置被 remember 缓存,同时也缓存了旧位置;新消息到来使旧气泡上移时,变换仍基于旧位置。去掉 remember 调用、每次根据气泡最新位置计算变换后,问题得以解决。行移动时,变换必须随之移动。

Jetpack Compose 中的 remember 用于在重组之间缓存计算结果,避免重复计算。但当缓存的值依赖于外部可变状态(例如消息列表变化导致的气泡位置改变)时,remember 会保留旧值,导致 UI 与实际位置脱节。在动态列表中,气泡的变换矩阵需要基于最新布局坐标实时计算,若将位置或尺寸缓存,后续动画和变换就会以过期数据为基准。正确的做法是对于位置相关的派生状态不使用 remember,或使用 remember 配合正确的 key 参数,确保状态变化时缓存失效。这一原则在 WhatsApp 修复气泡错位问题时得到了直接体现。
登出安全与更新触达
登出看似简单,实则暗藏风险。取消手表与账户的关联时,用户期望消息随之一并消失。团队需要删除消息、密钥和设置,但后台线程可能仍在写入,导致 SQL 异常,甚至写入新创建的数据库。Android 提供了清除应用用户数据的 API,它能清除数据并停止应用,但也会重置权限。在某些 OEM 设备上,通知桥接与通知权限绑定,使用该 API 会一并丢失手机通知。
团队的方案是自行停止进程:构建一个极小的服务和辅助进程,不包含依赖注入和工作管理器,因此不会启动应用的其他部分。登出后,该服务杀死应用内所有其他进程,然后才删除数据。由于没有使用清除应用用户数据 API,权限得以保留,通知桥接可以重新开启。
最后,团队提醒了一个容易被忽视的产品问题:每个 WhatsApp 应用版本都有内置的 90 天过期时间。用户可能卡在旧版本上,而新版本尚未推送到手表,导致应用停止工作,活跃用户数出现断崖式下跌。为此,他们将更新本身做成了产品的一部分:在应用过期前 15 天开始提示,并在剩余 7、5、3、2、1 天时重复提醒,用户可以选择忽略或跳转 Play Store 更新。正如他们所总结的,开发者的工作不止于发布更新,而是直到用户真正收到更新才算完成。
相关推荐

合理使用制度为何对AI时代人人有益
从Hacker News讨论出发,解析合理使用(Fair Use)原则在AI训练与内容创作中的法律逻辑、争议焦点及其对创作者与技术生态的双重影响。

Salesforce 3.75亿美元收购Rune,AI物流技术补强国防业务
Salesforce 据报道以 3.75 亿美元收购国防科技初创公司 Rune,这家 AI 物流公司的技术将用于补强 Salesforce 的 Missionforce 国防平台,也延续了 Salesforce 近期的并购节奏。

Mistral Large 4开源万亿参数,微软6万员工自愿离职
Mistral Large 4 开源万亿参数多模态模型,编码智能体评测得分 48;闪迪财报显示 AI 推理重塑 NAND 需求;谷歌 Gemini 上线应用内文件编辑,OpenAI 上调 Codex 额度,微软向 6 万员工提供自愿离职方案。