2020 年 5 月 23 号凌晨 1 点 30 左右, 大量三星手机用户的手机出现死机, 无限重启、进 Recovery 等问题, 并且操作不当会导致数据丢失, 并且上了知乎的热点, 售后点更是人满为患
知乎的部分回答中, 大家更是对三星的家属送上了亲切的问候, 甚至有的人已经将这次事故与 Note7 事件、充电门、绿屏门事件相提并论, 甚至预言三星因此会退出国内市场 ; 有的人因为这个丢了 Offer , 有的人准备了很久的资源丢失, 有的人甚至直接把手机砸了...
知乎热点甚至商场里的机器都变砖了
商场作为一个 Android 开发者, 我并不想对三星落井下石 , 我只想搞清楚到底是什么原因导致了这场事故 , 以及我们能从里面学到什么 . 我认为既然是 Android 系统出了问题, 我们有必要从技术的角度来分析为什么会出现这样的问题
结论先行, 对于不喜欢看长文的吃瓜群众来说, 直接看结论即可:
「这次事故表现是一部分三星用户的手机系统中关键系统服务重复 Crash 并强制进入 Recovery 界面. 关键系统服务指的是三星的 SystemUI中的 AOD 服务, 由于是系统服务, Crash 到一定的次数之后, 就会强制进入 Recovery 界面, 所以 大部分用户看到的都是 Recovery 界面(下面有图)」
「AOD 全称 Always On Display, 中文翻译是息屏显示, 就是你按电源键锁屏后, 在屏幕上显示时间、天气、图案等的服务, 这个只有部分高端机型才有这个功能.」
「AOD Crash 的原因是 2020 年 5 月 23 是闰四月, AOD 显示阴历的时候, 需要显示闰四月, 所以在代码中会走到显示闰月这个一般很难走进的分支条件, 走进这个条件之后, 需要获取 common_data_leap_month 这个字段, 但是由于代码编译出现了 Bug, 导致无法找到这个字段, 所以该进程直接报了 FATAL EXCEPETION, 进程重启, 重启之后还是要获取这个字段, 再重启, 如此反复 , 最终触发系统的自救措施, 进入 Recovery 界面」
「这也是为什么只有中国用户才会出现这个问题, 就是因为 AOD 在 5 月 23 号需要显示"闰"四月 , 但是没找到 "闰" 这个字, 所以就挂了 . 所以并不是千年虫 , 也不是服务器被黑, 更不是三星故意恶心人, 这种编译导致的 Bug , 再碰上几年一遇的闰月 , 遇到了就认了吧 , 老老实实道歉, 不丢人.」
吃瓜群众可以折返了, 感兴趣的 Android 开发者可以继续往下看, 内容虽然简单, 但是个人觉得还是可以看一下的
对于三星的开发人员来说, 分析这个 Crash 非常简单, 直接在监控里面捞 Log 就可以了, 从后面的分析来看, 这个问题也很快被发现, 并进行了修复(持续了半年左右);但是对这个问题感兴趣的其他开发者来说, 需要借助其他的工具
不过分析的过程也非常简单, 这里会把自己分析的思路和用到的工具记录下来, 方便大家使用
上面结论有说, Persist 进程频繁 Crash 会导致系统触发自救, 进入 Recovery 界面, 所以用户很多反馈截图大家看到都是 Recovery 界面 , 如下
Recovery 界面不过也有用户的界面直接显示了报错信息(我猜测是三星这边自己加的功能吧, 知道的麻烦告知一下), 这个界面对我们分析代码来说很重要
开发者对于这个堆栈是最熟悉不过了, 这是在一帧的渲染流程中, AOD 的 LocalDataView 在初始化的时候, 调用 getLunarCalendarInChina 方法出错了, LunarCalendar 是阴历的意思, 报错主要是因为找不到 common_data_leap_month 这个 string 值.
那么问题就很清楚了, 我们只需要查下面几个点
common_data_leap_month 这个 string 字段出现的代码逻辑
common_data_leap_month 这个 string 字段没有找到导致运行报错的原因
首先看 common_data_leap_month 字段出现的代码逻辑, 既然上面已经列出了函数堆栈, 那么我们需要直接查看代码来分析这个问题产生的逻辑, 如何拿到代码?自然是需要反编译, 推荐的反编译工具:TTDeDroid
反编译需要三星 AOD 的代码, 可以在 ApkMirror 里面搜 Always-On-Display, 就可以找到对应的文件, 可以看到三星的 AOD 更新的频率还是很频繁的, 通过用户反馈可以知道, 并非所有的用户都有这个问题, 且更新到新版本就没有问题了, 那么我们推测问题是出在老版本上的( 从堆栈来猜测应该是 V4.0 的版本 )
最新版本是正常的, 没有 Crash 的情况
首先我们先看一下最新版本这一段代码的逻辑
String month = shouldShowLeapMonth(locale) ? context.getResources().getString(R.string.common_date_leap_month) + months[convertMonth] : months[convertMonth]
这个就是说如果需要显示闰月, 那就取 common_date_leap_month 的值, 全局搜索 common_date_leap_month 发现最新版本里面是有定义这个值的 .
这里就可以看到 common_data_leap_month 字段出现的代码逻辑 : 「只有需要显示闰月的时候, 才会去获取 common_date_leap_month 这个字段的值, 其他 99.9% 的时候都不会触发这个值的获取」 .
代码逻辑R.java 文件里面存在的 common_date_leap_month, 说明是存在的, 查看对应的 string.xml 中也有这个字段的定义
R 文件 xml 文件既然新版本没有问题, 且我们也知道了 common_date_leap_month 这个字段的代码逻辑 , 那么我们从老版本来看 common_date_leap_month 这个字段没有找到的原因.
这里找的这个老版本是有问题的, 使用这个版本(这几个版本) 的用户到了 23 号会出现频繁 Crash 的现象. 之所以我认为他是有问题的 , 是因为全局搜索 common_date_leap_month 字段, 发现 R 文件里面没有对应的字段, 对应的 string.xml 里面也没有这个字段和他对应的值, 也就是说 , 这里代码只使用, 没有定义和赋值( 那怎么编译过的呢 ???)
只有使用,没有声明和赋值上面对应的代码逻辑如下, 可以看到函数名和行数和报错是一致的, InChina....
对应的代码和行数具体对应的代码:
private String getLunarCalendarInChina(Context context) {
if (sSolarLunarConverter == null) {
sSolarLunarConverter = SECCalendarFeatures.getInstance().getSolarLunarConverter();
if (sSolarLunarConverter == null) {
return "";
}
}
Time time = new Time();
time.set(Calendar.getInstance().getTimeInMillis());
sSolarLunarConverter.convertSolarToLunar(time.getYear(), time.getMonth(), time.getMonthDay());
String[] months = context.getResources().getStringArray(R.array.common_LunarMonth);
String[] days = context.getResources().getStringArray(R.array.common_LunarDay);
int convertMonth = sSolarLunarConverter.getMonth();
int convertDay = sSolarLunarConverter.getDay() - 1;
ACLog.d(TAG, "Lunar month and day : " + convertMonth + ", " + convertDay);
if (convertMonth < 0 || convertMonth >= months.length || convertDay < 0 || convertDay >= days.length) {
ACLog.e(TAG, "getLunarCalendarInChina, array out of bound month = " + months.length + ", days = " + days.length);
return "";
}
String chinaLunar = (sSolarLunarConverter.isLeapMonth() ? context.getResources().getString(R.string.common_date_leap_month) + months[convertMonth] : months[convertMonth]) + days[convertDay];
String str = chinaLunar;
return chinaLunar;
}
根据我这边的调查, 发现这个问题其实在 AOD 这个应用从 v3.3.18 升级到 v4.0.57 的时候就出现了, 但是中间一直都没有出问题, 没有闰四月, 用户也就不会有问题, 测试也没有测出来, 直到 2019 年 6 月 24 号发布的 v4.2.44 版本才修复了这个问题
4.3.44 版本修复v3.3.18 版本我们可以看到, common_date_leap_month 这个字段还是存在的
v3.3.18升级到 v4.0.57(第一个出问题的版本) 之后 , 这个字段就没有了( 那怎么编译过的呢 ???)
v4.0.57「理一下」:
AOD 从 v3.3.18 升级到 v4.0.57 的引入了这个问题(2018 年 10 月 24 号引入)
AOD 从 v4.2.24 升级到 v4.2.44 解决了这个问题(2019 年 6 月 24 号 修复)
这期间所有 AOD 版本在 v4.0.57 - v4.2.44 却从来没有升级的机型, 都会在 2020-5-23 号这一天进入 Recovery 模式.
上面一个很重要的点就是编译问题, Android 开发者都知道, 如果我在代码中写 getString(R.string.common_date_leap_month) , 那我得在 strings.xml 里面定义这个 common_date_leap_month, 然后给他赋值, 比如 "闰" , 这样才能在 R 文件中看到这个字段, 我们才能使用 getString(R.string.common_date_leap_month) 这样的语法去调用 ; 否则在编译阶段就会出现问题 , 编译提示 R.string.common_date_leap_month 不存在
罪魁祸首但是通过上面的分析我们发现, 频繁 Crash 的版本就是因为找不到 common_date_leap_month 这个字段才 Crash 的, 既然找不到那也应该编译不过才对, 但是既然我们拿到了 apk, 那说明编译也是没问题的.
这种情况出现的话, 一般有下面两种情况
项目中有同一个 jar 包的不同版本, 因此编译和运行时使用了不同的 jar 包
编译使用的是 Maven, 项目中的依赖由于使用了不同版本的包, 最后打包的时候使用的不是你需要的版本
猜测三星这次出问题的是因为第二种情况, 主项目和子 modules 使用了不同版本的包, 导致可以编译通过, 但是最终打包进项目的并不是编译时候的包, 就出现了运行时的 FATAL EXCEPTION : NoSuchFieldError ( 如果有知道具体原因的可以留言讨论一波 )
群里的小伙伴问这种问题编译的时候没有报错, 测试也没测出来, 兼容性测试也没有测出来, 这种问题有什么好的办法?
个人觉得, 这种问题遇到了就遇到了, 毕竟 10 年才出现 8 次....
罕见闰四月「开个玩笑, 这个问题对三星来说绝对是一个大的事故, 不过也贡献了一个经典的案例, 估计以后其他 App 或者手机厂商都会把这个纳入到功能测试中. 至于三星, 国内市场本身就不行了, S20 系列刚有些回暖, 又出现这档子事, 还是那句话 : 这是命, 得认, 道歉 , 不丢人」
上面的分析过程虽然比较简单, 但是有一些比较繁琐的工作, 比如找版本, 反编译 , 看代码逻辑等. 最终也算是找到了问题的根本原因 : 编译导致的问题碰上几年才遇到一次的闰月. 那么从这件事我们学到了什么呢?
功能测试 : 闰月是日历中的一个功能 , 不算是常用功能 , 但是相对来说比较专业 , 像这种涉及到专业的地方, 一定要谨慎 , 列出所有可能出现的情况去做测试, 必要的情况下, 交给专业的人来评估测试用例
涉及到多方依赖编译的项目, 在编译的时候要确保引用的版本和本地的版本一致 , 对于多方依赖的模块, 每次发版本之前最好跟对应的依赖的模块确认
SystemUI (锁屏\状态栏\手势\多任务\ AOD 等) 模块和桌面模块是用户直接能感受到的模块, 这些模块对稳定性的要求要非常高, 因为一旦这些模块发生 FATAL , 带来的影响是非常巨大的, 就像三星这次, 所以这几个模块的开发人员也是最辛苦的, 既要承接一些亮点功能的实现, 又要保证稳定性, 同时也位于系统开发和应用开发中间, 两边都有很大的耦合, 着实不容易 (媳妇做这一块 6 年多了, 晚上加个鸡腿...)
厂商提供的系统更新和厂商自己的应用更新(尤其是系统应用) , 一定要及时更新, 每次系统和系统应用更新一般都会修复很多 Bug , 增强稳定性和性能. 系统和系统应用没有盈利的压力, 所以更新都是以提升质量为主, 可以放心更新.
开发者对这种事情要保持好奇和敬畏 : 好奇可以帮助我学到很多东西, 透过现象看本质 ; 敬畏可以让我知道自己知识的欠缺, 在庞大的 Android 体系中, 自己知道的不过沧海一粟..