大偵探福老師——幽靈Crash謎蹤案

閒魚Flutter技術的基礎設施已基本趨於穩定,就在咱們準備鬆口氣的時候,一個Crash卻異軍突起衝擊着咱們的穩定性防線!閒魚技術火速成立偵探小組執行嫌犯偵查行動,經理重重磨難終於在一個隱蔽的角落將其繩之以法!html

幽靈Crash

問題要從閒魚Flutter基礎設施上一次大規模升級提及。2018年咱們對閒魚的Flutter基建做了比較大的重構,目標在於提升基建的穩定性和可擴展性。這個過程固然是挑戰重重,在上一次大規模的重構集成發版後,咱們雖然沒有發現很是明顯的異常問題,可是Crash率卻出現了一個比較明顯的增加。雖然整體數值還在可控範圍以內,但這一個Crash卻佔據了幾乎一大半。這個問題引發了咱們警覺,咱們馬上成立專項小組重點進行排查。node

通常Crash Log可以爲咱們定位Crash提供主要信息,咱們一塊兒看看這個Crash的Log:多線程

Thread 0 Crashed:
0   libobjc.A.dylib                 0x00000001c1b42b00 objc_object::release() :16 (in libobjc.A.dylib)
1   libobjc.A.dylib                 0x00000001c1b4338c (anonymous namespace)::AutoreleasePoolPage::pop(void*) :676 (in libobjc.A.dylib)
2   CoreFoundation                  0x00000001c28e8804 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ :28 (in CoreFoundation)
3   CoreFoundation                  0x00000001c28e8534 __CFRunLoopDoTimer :864 (in CoreFoundation)
4   CoreFoundation                  0x00000001c28e7d68 __CFRunLoopDoTimers :248 (in CoreFoundation)
5   CoreFoundation                  0x00000001c28e2c44 __CFRunLoopRun :1880 (in CoreFoundation)
6   CoreFoundation                  0x00000001c28e21cc _CFRunLoopRunSpecific :436 (in CoreFoundation)
7   GraphicsServices                0x00000001c4b59584 _GSEventRunModal :100 (in GraphicsServices)
8   UIKitCore                       0x00000001efb59054 _UIApplicationMain :212 (in UIKitCore)
9   Runner                          0x0000000102df4eb4 main main.m:49 (in Runner)
10  libdyld.dylib                   0x00000001c23a2bb4 _start :4 (in libdyld.dylib)

這是一個很典型的野指針Crash Log,是其中一種俗稱的Over released問題。可是具體是哪一個對象和方法,很難直接從Log上面得知,何況ARC下面的野指針更使人費解。app

一些推測

Crash理因由變動引入的,咱們直覺地從最近發版引入的主要變動去推測。考慮到咱們開始出現問題的版本有幾個比較大的改造,咱們讓相關的同窗從新review了一下本身的代碼,主要關注內存方面的問題。雖然沒有找到很是確切的問題,咱們仍是進行了一次可疑代碼優化,進行技術灰度卻沒有任何效果。在龐大的代碼庫數不清的提交中去找尋毫無頭緒的野指針問題看起來不是一件容易的事情,函數

機型 iOS版本 閒魚版本

咱們詳細的分析了Crash的數據以及用戶操做日誌,而後得出結論這個Crash與機型,系統版本都沒明顯聯繫。可是咱們能夠發現用戶基本上都是在Flutter容器的詳情頁容易崩潰。Flutter不可避免成爲了被懷疑對象,包括咱們本身實現的基礎設施,以及Flutter底層的庫。工具

可是Flutter已經在閒魚應用比較長的一段時間,Flutter底層咱們幾乎肯定是穩定的,否則早就出問題了。這個時候主要懷疑點轉移到了咱們本身實現的組件,主要包括混合棧組件以及一些監控埋點設施。可是咱們隨後將這些懷疑對象經過技術灰度手段一一排除了嫌疑。oop

版本走勢

從版本的Crash率的走勢看,咱們還發現這個問題有一個緩慢增加放量的過程,這難免讓咱們開始懷疑App是否存在相似的慢慢放量的功能需求。然而事實證實,這個方向沒有任何收穫。性能

沒法復現的問題

不斷有用戶向咱們反饋容易遇到閃退,可是咱們本身的設備通過大量嘗試卻沒有復現這個問題。這是最爲頭疼的,從用戶的操做路徑來看並沒有特殊的地方。不管是測試仍是開發同窗都沒法在本身設備上面復現出來,沒法復現的野指針問題很是難以定位。測試

線上監控技術

從變動和問題特徵排除沒有實質性的進展,咱們開始嘗試線上的一些監控方法來協助排查。但願能夠拿到更加詳細的相關信息。優化

GCD線程跟蹤技術

從Crash Log咱們能夠到這應該是一個autorelease對象野指針致使的問題,原本應該autorelease進行釋放的對象,在其被AutoReleasePool釋放前就由於某種緣由提早釋放。咱們懷疑是否存在多線程致使的問題,因此咱們採用GCD線程跟蹤技術進行監控。

這個技術的基本原理是hook住GCD的dispatch方法,將block的返回地址經過 __builtin_return_address函數拿到,而後編碼寫入到當前的線程名中,崩潰的時候,從線程名字中解碼得出dispather的返回地址便可定位到是誰dispatch的這個block,而後隨同Crash Log的擴展字段將其上傳到後臺。

GCD是一套C接口,因此咱們採用fishhook去hook,此類底層hook對性能會有必定影響,因此咱們只在專門的技術驗證灰度中採用此項技術。fishhook的大體原理是從新綁定一些C的符號,由於不少共享的庫的符號好比GCD在iOS中是動態綁定到App的可執行文件中的。而目前這部分符號表所在的內存沒有簽名,因此能夠經過MachO提供的接口去進行從新綁定。感興趣的同窗能夠參考Facebook fishhook項目。

咱們準備了一個技術灰度版原本監控這個問題。可能因爲樣本比較小,咱們收集到的返回地址數量很是有限。經過符號解析,得出來的都是一些NSFoundation對象,沒有太多有價值的東西。以前懷疑這問題可能發生在GCD執行的block中,只是收集崩潰的時候GCD上一次調用的返回地址自己也缺少針對性。

指望是美好的,現實是骨感受,最終咱們沒有拿到有用的信息。

線上Zombie的野指針監控

在Debug模式下,Xcode有用強大的工具去幫助你定位野指針。最爲通用的野指針監控工具莫過於NSZombie,若是咱們能在線上開啓Zombie應該可以很容易的抓到野指針對象。淘系基礎設施裏面有線上Zombie的實現。

線上的Zombie實現主要原理hook對象的dealloc方法在dealloc的時候經過runtime的動態性將其轉變成一個Zombie類,當有其它消息發給Zombie對象的時候咱們就能夠根據存儲下來的類型定位到Zombie的對象類型。詳細能夠參考Mike Ash的Let's build NSZombie。不過須要注意的是,這裏面的實現是基於MRC,ARC實現上可能會有差別,基本原理是大體相同的。

咱們在閒魚App中根據基礎提供的文檔將線上Zombie打開進行灰度監控,所幸的是咱們拿到了一些野指針對象。量也不是不少,只有個位數的類型。

多是因爲樣本不夠大,沒有覆蓋到典型的用戶。或許是咱們的監控組件沒法抓到這個特定類型的Crash。最終在排查完全部收集到的野指針對象後,依然沒有解決這個Crash。

線上監控彷佛沒能爲咱們打開突破口。

UI自動化

咱們仍是指望與可以將問題重現出來,這樣能夠迅速經過Xcode定位到問題。從機率上確實不算過高,基於前面手動復現困難的問題,咱們嘗試利用自動化工具去作自動復現嘗試。

SwiftMonkey + 引擎DEBUG

SwiftMonkey是一個比較好的UI自動化工具,集成簡單,並且能夠在Debug模式下面進行自動UI測試。也就是說咱們能夠在保持Xcode各類強大工具備效的前提下進行自動化測試。

咱們採用Local Debug Flutter引擎進行測試以便拿到相關的符號,通過一段時間的自動化測試咱們在模擬器上面抓到了一摸同樣的Crash Log!

這不得不說是一個使人振奮的消息,Xcode抓到的Zombie對象是一個NSMutableArray,這是一個通用對象,彷佛也沒有特別的地方。這個時候咱們須要用到Xcode提供的malloc log和Address sanitizer去跟蹤是誰建立的這個對象。

咱們在模擬器上面打開malloc log以及Address sanitizer復現問題導出MemGraph而後使用

memory history 地址
malloc log MemGraph 地址

最終定位到問題出如今Flutter引擎內部文件 accessibility_bridge.mm 533行:

NSMutableArray* newChildren =
        [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
    for (NSUInteger i = 0; i < newChildCount; ++i) {
      SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
      child.parent = object;
      [newChildren addObject:child];
    }
    object.children = newChildren;

這個問題把咱們帶到了Flutter的Accessibility(通用->輔助功能)支持模塊,咱們跟用戶通過了交流,並無發現用戶有打開相關的輔助功能。

雖然Log是一摸同樣的,咱們有點不相信咱們追尋的Crash是因爲這個緣由致使的。這的確是Flutter在Accessibility的一個坑,可是跟咱們用戶交流的情形不一致。並且模擬器上面容易出現,咱們將測試包裝到手機上卻沒法在復現這問題。很顯然,用戶都是真機,模擬器或許不能說明問題。此時咱們尚未信心確認這個問題,開輔助功能的人應該是很少的。

這感受好像在黑暗中看到光亮,一瞬間又被黑暗淹沒了,咱們彷佛又來到了一個死衚衕。究竟是哪裏出問題了?

用戶面對面

線上交流

在問題排查的過程當中咱們一直跟用戶保持良好的交流。工程師們主動聯繫用戶,不少用戶也熱心響應咱們的訪問,給咱們錄製了很多崩潰現場的視頻。咱們能夠看到那些反饋問題的用戶很容易出現,可是不出現的用戶基本上沒有這個問題。咱們開始懷疑跟帳號的關係,可能有一些ABTest的參數全部影響。線上的交流雖然給了咱們很多有用的信息,可是依然沒有實質性突破。

線下面對面

咱們開始尋找願意協助咱們現場排查問題用戶,咱們重點找了幾個很是容易出現問題的杭州用戶打算上門現場Debug。在和用戶進行了深刻交流之後,其中一個用戶願意已訪問園區方式來現場協助工程師排查問題。

咱們選了用戶有時間的一個週末而後拿到用戶的手機進行了調試,果真在用戶的手機上很是容易復現。並且就是咱們前面提到的accessibility_bridge.mm處的崩潰,爲何以前再模擬器上那麼容易出現呢?

原來在引擎的代碼中若是是模擬器的話是默認打開Accessibility的,而真機是取決於系統的設置。

#if TARGET_OS_SIMULATOR
  // There doesn't appear to be any way to determine whether the accessibility
  // inspector is enabled on the simulator. We conservatively always turn on the
  // accessibility bridge in the simulator, but never assistive technology.
  platformView->SetSemanticsEnabled(true);
  platformView->SetAccessibilityFeatures(flags);
#else
  bool enabled = UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning();
  if (enabled)
    flags |= static_cast<int32_t>(blink::AccessibilityFeatureFlag::kAccessibleNavigation);
  platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled());
  platformView->SetAccessibilityFeatures(flags);
#endif

原來這名用戶打開了iOS的閱讀屏幕功能: UIAccessibilityIsSpeakScreenEnabled, 這致使Flutter輔助支持模塊被打開。咱們立刻聯繫其它用戶確認,基本上用戶都打開了「閱讀屏幕」功能。至此,咱們基本確認就是這個問題所致。咱們隨後進行了一個小範圍禁用Accessibility的灰度實驗確認就是這問題致使的Crash。

在通過止血修復之後,咱們繼續尋找野指針的源頭。問題出在這個autorelease的NSMutableArray對象,這個代碼看起來也沒什麼明顯問題。FLutter引擎的iOS使用MRC進行內存管理。咱們繼續review相關的代碼, 終於在SemanticsObject類發現了一段奇怪的代碼:

- (void)dealloc {
  for (SemanticsObject* child in _children) {
    child.parent = nil;
  }
  [_children removeAllObjects];
  [_children dealloc];
  _parent = nil;
  [_container release];
  _container = nil;
  [super dealloc];
}

注意其中的[_children dealloc];,這裏不該該直接調用dealloc,而只須要release,這或許就是MRC難以免的誤寫吧。問題定位到,修復也就是分分鐘鐘的事情。

後來咱們發現其實這個問題最近已經在Flutter官方master分支上修復了,只是咱們本身維護的引擎還沒有同步對應的代碼。

至此,問題獲得圓滿解決,Crash率恢復到正常水平。

總結

爲了排查這個問題,咱們從多個方向同時進行了不一樣的嘗試。具體來講從代碼變動跟蹤,線上監控技術,UI自動化以及深刻閱讀相關源碼等方式同時去推動問題的解決。須要特別強調的是,跟用戶的緊密交流也是解決問題的關鍵,俗話說知彼知己方能百戰不殆,只有充分理解須要解決的問題才能更有效的將其解決。

問題的復現與否一般對於解決方案相當重要,一個可以復現的問題基本可以在現代的IDE提供的強大工具的幫助下方便定位到。一開始咱們也是苦於沒能找到復現的路徑,原來這個Crash卻被掩蓋在一個並不常見的系統設置下面,同時深藏於Flutter複雜的引擎深部。好在有熱心用戶願意協助咱們排查問題爲咱們提供精確的問題現場,才得以最終成功將其確認並解決。

 

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索