2020 年 5 月 23 号凌晨 1 点 30 左右, 大量三星手机用户的手机出现死机, 无限重启、进 Recovery 等问题, 并且操作不当会导致数据丢失, 并且上了知乎的热点, 售后点更是人满为患
知乎的部分回答中, 大家更是对三星的家属送上了亲切的问候, 甚至有的人已经将这次事故与 Note7 事件、充电门、绿屏门事件相提并论, 甚至预言三星因此会退出国内市场 ; 有的人因为这个丢了 Offer, 有的人准备了很久的资源丢失, 有的人甚至直接把手机砸了…
作为一个 Android 开发者, 我并不想对三星落井下石, 我只想搞清楚到底是什么原因导致了这场事故, 以及我们能从里面学到什么 . 我认为既然是 Android 系统出了问题, 我们有必要从技术的角度来分析为什么会出现这样的问题
历史案例说明(2026-10-14 核查):本文分析 2020 年留下的截图和反编译片段。AOD 版本号是当年的样本标签,不是当前更新建议;本次没有原始 APK 哈希、完整固件和三星内部构建记录,无法重做全部版本的设备复现。以下把日志直接证明的异常、版本对比线索与构建原因推测分开。

当时流传的商场照片也显示演示机进入恢复界面;这不等于已经证明硬件损坏或数据全部丢失。

结论
现有日志直接证明的是:com.android.systemui 的主线程在加载 AOD 相关日期视图时,访问 R$string 的整数字段失败,抛出了 NoSuchFieldError。 这不是已经证明“strings.xml 少了一个闰字”,也不是已经查明三星使用 Maven 时出现了哪种依赖冲突。
AOD 是 Always On Display(息屏显示),用于在熄屏状态显示时间等内容,支持范围取决于机型和设置,不能笼统说只有高端机才有。这里的堆栈来自 SystemUI 锁屏视图的插件调用路径,并不是一个独立 AOD 进程的崩溃记录。
香港天文台的 2020 年公历与农历对照表显示,2020-05-23 是农历闰四月初一。旧代码在 isLeapMonth() 为 true 时访问 common_date_leap_month,与当时日期触发异常的解释相符;这个判断适用于闰月,并不只针对闰四月。原文多处写成 common_data_leap_month,应以日志与代码中的 common_date_leap_month 为准。
SystemUI 反复失败可能进一步触发系统恢复策略,但单张 Recovery 图不能证明每台设备经历了相同的升级恢复过程,更不能推出“所有相关版本都必定进入 Recovery”。受影响范围需要固件、插件版本、区域/语言设置和实际执行路径共同确定;用户所在国家本身不是代码判断条件。
分析
吃瓜群众可以折返了, 感兴趣的 Android 开发者可以继续往下看, 内容虽然简单, 但是个人觉得还是可以看一下的
外部开发者可从日志和版本样本建立线索,但缺少厂商构建配置、运行时类加载记录和回归结果时,不能把推测当作完整根因报告。下面我把当时的分析思路和用到的工具记录下来,也说明这些材料能支持到哪一步。
从现象入手
下图可读到 Android 9 / PPR1 开头的构建信息和 Recovery 选项。进入恢复界面、SystemUI 进程重启、整机重启,是三个需要分别确认的事件。

AOSP Rescue Party从 Android 8.0 起提供逐级恢复机制。以 android-9.0.0_r1 源码为对照,persistent app 的计数按 UID 记录,阈值为 30 秒窗口内达到 5 次;触发后逐级重置设置,最高级才调用恢复模式提示用户清除数据。它不是“任何系统进程 Crash 几次就自动清空手机”的通则。启用条件和厂商实现也可能不同;图中 Erase app data、Safe mode 等界面不能逐项等同于 AOSP 行为。
下面另一张照片提供了更直接的异常线索。我当时猜这是三星自己加的诊断界面,单凭照片还没法确认;知道的朋友也欢迎告知。

能读出的关键内容是进程 com.android.systemui、PID 2388、异常 java.lang.NoSuchFieldError、字段 common_date_leap_month、类型 I(int),以及声明类 com.samsung.android.uniform.R$string。调用链经过 LocalDateView.getLunarCalendarInChina、updateLocalDate、onFinishInflate 和 SystemUI 的锁屏时钟视图;原文的 LocalDataView 是拼写错误。
因此接下来查两个问题:字段为何在这个日期分支被访问,以及实际加载的类为什么不能解析该字段。FATAL EXCEPTION 是日志标题,具体异常类型应看 NoSuchFieldError。
分析代码
我当时用 TTDeDroid反编译。本次核查时其发布页仍以 v1.8.0(2024-06-12)为最新 release;它封装的工具版本不能代表今天每个反编译器的最新版本。也可直接使用 JADX,但反编译 Java 是重建结果,不保证完整还原源码。
我当时从 APKMirror 找了几个 AOD 版本来对比。严谨对比需同时保留 APK 来源、SHA-256、versionName/versionCode、签名、系统构建及实际安装路径;同名版本可能有不同变体,网上下载包也不必然等于报错设备上的系统包。以下截图沿用当年的版本标签,不冒充本次重新解包或设备复现。
正常版本_V5.2.05
这里“正常版本”是原文对照样本的称呼,不是当前最新版本,也不是所有机型的验收结论。所示分支为:
1 | String month = shouldShowLeapMonth(locale) ? context.getResources().getString(R.string.common_date_leap_month) + months[convertMonth] : months[convertMonth]; |
当 shouldShowLeapMonth(locale) 为 true 时才会访问该字段;截图没有展示这个辅助函数的完整实现,不能仅由方法名补出它的所有条件。旧版直接使用 isLeapMonth(),两者也不能不加区分地当作相同实现。现有样本无法估计这个分支的实际触发比例。

下面 R 文件截图显示的是 com.samsung.android.app.aodservice.R 中的字段,值为 2131689531。这与错误里的 com.samsung.android.uniform.R$string 不是同一个全限定类名;前者有字段不能直接证明后者运行时也有。资源 ID 也不应跨 APK 或版本硬编码比较。

资源解码截图确实显示一个名为 common_date_leap_month、文本为“闰”的条目,但“资源表有条目”与“运行时类有字段”是两层证据。

出问题版本_V4.1.70
我当时把 v4.1.70 列为疑似问题样本:全文搜索只见字段使用,未见定义。

这个结果支持继续检查字段缺失,却不能单靠搜索窗口证明所有 DEX、资源表和动态加载类均已完整检查。下面方法与堆栈中的函数名相符;反编译器显示的行号不等于可靠的源码构建身份。

原反编译片段如下;SECCalendarFeatures、转换器与 Time 的具体实现依赖样本,不能当作标准 Android API 的可独立编译示例:
1 | private String getLunarCalendarInChina(Context context) { |
三元表达式只在闰月分支访问该字段;数组边界检查不能防住字段解析失败。要判断这一行运行时到底读取了哪个 R 类,应检查 DEX 指令/字段引用和实际 ClassLoader,而不是只看 Java 视图。
问题出现的时间
原版本记录可以整理成下面的证据表:
| 样本版本 | 当时留下的材料 | 可以支持的结论 |
|---|---|---|
| v3.3.18 | 搜索图同时列出 app.aodservice 与 uniform 的同名字段 | 该次反编译结果能找到两处定义 |
| v4.0.57 | 搜索图只见使用;原笔记标记日期为 2018-10-24 | 是疑似引入点,尚缺完整产物核验 |
| v4.1.70 | 使用点及方法截图 | 与异常路径相符,不能替代设备复现 |
| v4.2.24 / v4.2.44 | APK 列表分别标为 2019-05-03 / 2019-06-24 | 证明截图所列版本日期,不单独证明修复内容或 OTA 覆盖 |
| v5.2.05 | 新分支、R 字段和资源条目截图 | 是后续对照样本,不是当前“最新版” |

v3.3.18 的图同时显示两个包名下的定义:

v4.0.57 的图只留下使用点:

根据当时找到的样本,我把修复点放在 v4.2.44,但本次没有该版本修复提交或完整二进制对比来独立确认。不能把范围写成“v4.0.57~v4.2.44 全部有问题”,尤其又同时称 v4.2.44 已修复;更不能据 APK 列表日期断言所有机型都已收到更新。原图注的 4.3.44 也已按图中 4.2.44 更正。
农历是阴阳历,不是只按月相的纯阴历;闰月也不是十几年才有一次。对于这里的 isLeapMonth 分支,应覆盖各个闰月,而不是只等待下一次闰四月。
编译问题
看到这里,我当时最想问的是:字段找不到,那它是怎么编译过的呢?
NoSuchFieldError属于运行时链接错误:调用代码要访问的字段,与实际加载类的定义不兼容。要分开检查三个层次:
- 编译时是否能解析指定全限定 R 类中的字段。
- 最终 DEX/运行时加载的同名类是否具有匹配名称和类型的字段。
- 整数资源 ID 是否能在当前 Resources/资源表中取到相应条目。
第 2 层失败可以发生在 getString 真正执行之前;第 3 层缺失通常应看 Resources.NotFoundException。因此,下面这张“闰”字符串截图不能单独解释日志里的 NoSuchFieldError。

资源也不一定直接写在主模块的 strings.xml:库资源和构建合并同样能生成 R 符号。Java 常量规则还会使常量值在编译时进入调用代码;反编译器展示 R.string.xxx,不一定意味着 DEX 保留同名字段读取。当前工程也不能沿用“R 一定都是 final”的假设:AGP 8.0起默认生成 non-final、non-transitive R。这是现代构建背景,不是 2018 年三星故障的原因。
依赖版本混用、打包时类/资源裁剪、插件与宿主不匹配、ClassLoader 选中了不同定义,都值得排查;仅凭截图无法在这些候选中做出选择,更无法认定“第二种 Maven 问题”。本篇能定位运行时字段解析失败和触发分支,尚不能确认三星内部哪一步构建导致了不匹配。
若重新分析,应保留原始包,用 apkanalyzer核对 Manifest/DEX,用 AAPT2核对资源表,并对齐运行时类来源和系统插件组合。修复后还需测试实际发布产物,而非只确认 IDE 中的 Java 文件存在该字段。
现代补充:Android 16 引入 Crash Recovery 模块,包含 PackageWatchdog、RescueParty 等组件。旧文中的源码路径和恢复策略应按系统版本追踪,不能直接作为新系统排障合同。
开个玩笑, 这个问题对三星来说绝对是一个大的事故, 不过也贡献了一个经典的案例, 估计以后其他 App 或者手机厂商都会把这个纳入到功能测试中. 至于三星, 国内市场本身就不行了, S20 系列刚有些回暖, 又出现这档子事, 还是那句话 : 这是命, 得认, 道歉, 不丢人
想必三星对这一天也会铭心刻骨
总结
分析思路本身不算复杂,但找版本、反编译、看代码逻辑,这些工作还是挺繁琐的。这个案例的直接线索是“日历分支触发运行时字段解析失败”。日历日期可核对,异常类型也可从截图确认;具体构建失配原因、完整受影响版本和每台设备的恢复过程,仍缺少原始产物与运行证据。
功能测试:覆盖普通月/闰月、月末/跨年、时区、语言与区域、锁屏视图重建和插件升级。把时间作为可控输入,在测试中验证边界,不依赖现实日期偶然触发。
依赖与发布:核对编译依赖、最终 DEX、资源表、运行时类来源及宿主/插件组合;对实际发布包做回归,不能以“编译通过”代替兼容性检查。
SystemUI (锁屏\状态栏\手势\多任务\ AOD 等) 模块和桌面模块是用户直接能感受到的模块, 这些模块对稳定性的要求要非常高, 因为一旦这些模块发生 FATAL, 带来的影响是非常巨大的, 就像三星这次, 所以这几个模块的开发人员也是最辛苦的, 既要承接一些亮点功能的实现, 又要保证稳定性, 同时也位于系统开发和应用开发中间, 两边都有很大的耦合, 着实不容易 (媳妇做这一块 6 年多了, 晚上加个鸡腿…)
更新:优先使用厂商为对应机型、地区和系统提供的官方更新,并查看发布说明、备份重要数据。更新可以包含修复,也可能引入回归;不能因系统应用“不以盈利为目的”就保证所有更新都没有风险,更不应随意安装不匹配的系统 APK。
开发者对这种事情要保持好奇和敬畏 : 好奇可以帮助我学到很多东西, 透过现象看本质 ; 敬畏可以让我知道自己知识的欠缺, 在庞大的 Android 体系中, 自己知道的不过沧海一粟..
这个问题对三星来说绝对是一个大的事故, 不过也贡献了一个经典的案例, 估计以后其他 App 或者手机厂商都会把这个纳入到功能测试中. 至于三星, 国内市场本身就不行了, S20 系列刚有些回暖, 又出现这档子事, 还是那句话 : 这是命, 得认, 道歉, 不丢人」
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
