一個"閏"字引發的事故 : 三星系統重啓分析

背景

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 值.

那麼問題就很清楚了, 我們只需要查下面幾個點

  1. common_data_leap_month 這個 string 字段出現的代碼邏輯

  2. common_data_leap_month 這個 string 字段沒有找到導致運行報錯的原因

分析代碼

首先看 common_data_leap_month 字段出現的代碼邏輯, 既然上面已經列出了函數堆棧, 那麼我們需要直接查看代碼來分析這個問題產生的邏輯, 如何拿到代碼?自然是需要反編譯, 推薦的反編譯工具:TTDeDroid

反編譯需要三星 AOD 的代碼, 可以在 ApkMirror 裏面搜 Always-On-Display,  就可以找到對應的文件, 可以看到三星的 AOD 更新的頻率還是很頻繁的, 通過用戶反饋可以知道, 並非所有的用戶都有這個問題, 且更新到新版本就沒有問題了, 那麼我們推測問題是出在老版本上的( 從堆棧來猜測應該是 V4.0 的版本 )

正常版本_V5.2.05

最新版本是正常的, 沒有 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 文件

出問題版本_V4.1.70

既然新版本沒有問題, 且我們也知道了 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

「理一下」

  1. AOD 從 v3.3.18 升級到 v4.0.57 的引入了這個問題(2018 年 10 月 24 號引入)

  2. 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,  那說明編譯也是沒問題的.

這種情況出現的話, 一般有下面兩種情況

  1. 項目中有同一個 jar 包的不同版本, 因此編譯和運行時使用了不同的 jar 包

  2. 編譯使用的是 Maven, 項目中的依賴由於使用了不同版本的包, 最後打包的時候使用的不是你需要的版本

猜測三星這次出問題的是因爲第二種情況, 主項目和子 modules 使用了不同版本的包, 導致可以編譯通過, 但是最終打包進項目的並不是編譯時候的包, 就出現了運行時的 FATAL EXCEPTION : NoSuchFieldError ( 如果有知道具體原因的可以留言討論一波 )

閏月

羣裏的小夥伴問這種問題編譯的時候沒有報錯, 測試也沒測出來, 兼容性測試也沒有測出來, 這種問題有什麼好的辦法?

個人覺得, 這種問題遇到了就遇到了, 畢竟 10 年纔出現 8 次....

罕見閏四月

「開個玩笑, 這個問題對三星來說絕對是一個大的事故, 不過也貢獻了一個經典的案例, 估計以後其他 App 或者手機廠商都會把這個納入到功能測試中. 至於三星, 國內市場本身就不行了, S20 系列剛有些回暖, 又出現這檔子事, 還是那句話 : 這是命, 得認, 道歉 , 不丟人」

總結

上面的分析過程雖然比較簡單, 但是有一些比較繁瑣的工作, 比如找版本, 反編譯 , 看代碼邏輯等. 最終也算是找到了問題的根本原因 : 編譯導致的問題碰上幾年才遇到一次的閏月. 那麼從這件事我們學到了什麼呢?

  1. 功能測試 : 閏月是日曆中的一個功能 , 不算是常用功能 , 但是相對來說比較專業 , 像這種涉及到專業的地方, 一定要謹慎 , 列出所有可能出現的情況去做測試, 必要的情況下, 交給專業的人來評估測試用例

  2. 涉及到多方依賴編譯的項目, 在編譯的時候要確保引用的版本和本地的版本一致 , 對於多方依賴的模塊, 每次發版本之前最好跟對應的依賴的模塊確認

  3. SystemUI (鎖屏\狀態欄\手勢\多任務\ AOD 等) 模塊和桌面模塊是用戶直接能感受到的模塊, 這些模塊對穩定性的要求要非常高, 因爲一旦這些模塊發生 FATAL , 帶來的影響是非常巨大的, 就像三星這次, 所以這幾個模塊的開發人員也是最辛苦的, 既要承接一些亮點功能的實現, 又要保證穩定性, 同時也位於系統開發和應用開發中間, 兩邊都有很大的耦合, 着實不容易 (媳婦做這一塊 6 年多了, 晚上加個雞腿...)

  4. 廠商提供的系統更新和廠商自己的應用更新(尤其是系統應用) , 一定要及時更新, 每次系統和系統應用更新一般都會修復很多 Bug , 增強穩定性和性能. 系統和系統應用沒有盈利的壓力, 所以更新都是以提升質量爲主, 可以放心更新.

  5. 開發者對這種事情要保持好奇和敬畏 : 好奇可以幫助我學到很多東西, 透過現象看本質 ; 敬畏可以讓我知道自己知識的欠缺, 在龐大的 Android 體系中, 自己知道的不過滄海一粟..

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章