溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

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

發(fā)布時間:2020-08-08 10:55:53 來源:ITPUB博客 閱讀:112 作者:火鍋小仙女 欄目:web開發(fā)

閑魚Flutter技術(shù)的基礎設施已基本趨于穩(wěn)定,就在我們準備松口氣的時候,一個Crash卻異軍突起沖擊著我們的穩(wěn)定性防線!閑魚技術(shù)火速成立偵探小組執(zhí)行嫌犯偵查行動,經(jīng)理重重磨難終于在一個隱蔽的角落將其繩之以法!

幽靈Crash

問題要從閑魚Flutter基礎設施上一次大規(guī)模升級說起。2018年我們對閑魚的Flutter基建作了比較大的重構(gòu),目標在于提高基建的穩(wěn)定性和可擴展性。這個過程當然是挑戰(zhàn)重重,在上一次大規(guī)模的重構(gòu)集成發(fā)版后,我們雖然沒有發(fā)現(xiàn)非常明顯的異常問題,但是Crash率卻出現(xiàn)了一個比較明顯的增長。雖然總體數(shù)值還在可控范圍之內(nèi),但這一個Crash卻占據(jù)了幾乎一大半。這個問題引起了我們警覺,我們立刻成立專項小組重點進行排查。

一般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下面的野指針更令人費解。

一些推測

Crash理因由變更引入的,我們直覺地從最近發(fā)版引入的主要變更去推測??紤]到我們開始出現(xiàn)問題的版本有幾個比較大的改造,我們讓相關的同學重新review了一下自己的代碼,主要關注內(nèi)存方面的問題。雖然沒有找到非常確切的問題,我們還是進行了一次可疑代碼優(yōu)化,進行技術(shù)灰度卻沒有任何效果。在龐大的代碼庫數(shù)不清的提交中去找尋毫無頭緒的野指針問題看起來不是一件容易的事情,

機型 iOS版本 閑魚版本

我們詳細的分析了Crash的數(shù)據(jù)以及用戶操作日志,然后得出結(jié)論這個Crash與機型,系統(tǒng)版本都沒明顯聯(lián)系。但是我們可以發(fā)現(xiàn)用戶基本上都是在Flutter容器的詳情頁容易。Flutter不可避免成為了被懷疑對象,包括我們自己實現(xiàn)的基礎設施,以及Flutter底層的庫。

但是Flutter已經(jīng)在閑魚應用比較長的一段時間,F(xiàn)lutter底層我們幾乎確定是穩(wěn)定的,不然早就出問題了。這個時候主要懷疑點轉(zhuǎn)移到了我們自己實現(xiàn)的組件,主要包括混合棧組件以及一些監(jiān)控埋點設施。但是我們隨后將這些懷疑對象通過技術(shù)灰度手段一一排除了嫌疑。

版本走勢

從版本的Crash率的走勢看,我們還發(fā)現(xiàn)這個問題有一個緩慢增長放量的過程,這不免讓我們開始懷疑App是否存在類似的慢慢放量的功能需求。然而事實證明,這個方向沒有任何收獲。

無法復現(xiàn)的問題

不斷有用戶向我們反饋容易遇到閃退,但是我們自己的設備經(jīng)過大量嘗試卻沒有復現(xiàn)這個問題。這是最為頭疼的,從用戶的操作路徑來看并無特殊的地方。無論是測試還是開發(fā)同學都無法在自己設備上面復現(xiàn)出來,無法復現(xiàn)的野指針問題非常難以定位。

線上監(jiān)控技術(shù)

從變更和問題特征排除沒有實質(zhì)性的進展,我們開始嘗試線上的一些監(jiān)控方法來協(xié)助排查。希望可以拿到更加詳細的相關信息。

線程跟蹤技術(shù)

從Crash Log我們可以到這應該是一個autorelease對象野指針導致的問題,本來應該autorelease進行釋放的對象,在其被AutoReleasePool釋放前就因為某種原因提前釋放。我們懷疑是否存在多線程導致的問題,所以我們采用線程跟蹤技術(shù)進行監(jiān)控。

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

是一套C接口,所以我們采用fishhook去hook,此類底層hook對性能會有一定影響,所以我們只在專門的技術(shù)驗證灰度中采用此項技術(shù)。fishhook的大致原理是重新綁定一些C的符號,因為很多共享的庫的符號比如在iOS中是動態(tài)綁定到App的可執(zhí)行文件中的。而目前這部分符號表所在的內(nèi)存沒有簽名,所以可以通過MachO提供的接口去進行重新綁定。感興趣的同學可以參考Facebook fishhook項目。

我們準備了一個技術(shù)灰度版本來監(jiān)控這個問題。可能由于樣本比較小,我們收集到的返回地址數(shù)量非常有限。通過符號解析,得出來的都是一些NSFoundation對象,沒有太多有價值的東西。之前懷疑這問題可能發(fā)生在執(zhí)行的block中,只是收集的時候上一次調(diào)用的返回地址本身也缺乏針對性。

期望是美好的,現(xiàn)實是骨感覺,最終我們沒有拿到有用的信息。

線上Zombie的野指針監(jiān)控

在Debug模式下,Xcode有用強大的工具去幫助你定位野指針。最為通用的野指針監(jiān)控工具莫過于NSZombie,如果我們能在線上開啟Zombie應該能夠很容易的抓到野指針對象。淘系基礎設施里面有線上Zombie的實現(xiàn)。

線上的Zombie實現(xiàn)主要原理hook對象的dealloc方法在dealloc的時候通過runtime的動態(tài)性將其轉(zhuǎn)變成一個Zombie類,當有其它消息發(fā)給Zombie對象的時候我們就可以根據(jù)存儲下來的類型定位到Zombie的對象類型。詳細可以參考Mike Ash的 Let's build NSZombie 。不過需要注意的是,這里面的實現(xiàn)是基于MRC,ARC實現(xiàn)上可能會有差異,基本原理是大致相同的。

我們在閑魚App中根據(jù)基礎提供的文檔將線上Zombie打開進行灰度監(jiān)控,所幸的是我們拿到了一些野指針對象。量也不是很多,只有個位數(shù)的類型。

可能是由于樣本不夠大,沒有覆蓋到典型的用戶?;蛟S是我們的監(jiān)控組件無法抓到這個特定類型的Crash。最終在排查完所有收集到的野指針對象后,依然沒有解決這個Crash。

線上監(jiān)控似乎沒能為我們打開突破口。

UI自動化

我們還是期望與能夠?qū)栴}重現(xiàn)出來,這樣可以迅速通過Xcode定位到問題。從概率上確實不算太高,基于前面手動復現(xiàn)困難的問題,我們嘗試利用自動化工具去做自動復現(xiàn)嘗試。

SwiftMonkey + 引擎DEBUG

SwiftMonkey是一個比較好的UI自動化工具,集成簡單,而且可以在Debug模式下面進行自動UI測試。也就是說我們可以在保持Xcode各種強大工具有效的前提下進行自動化測試。

我們采用Local Debug Flutter引擎進行測試以便拿到相關的符號,經(jīng)過一段時間的自動化測試我們在模擬器上面抓到了一摸一樣的Crash Log!

這不得不說是一個令人振奮的消息,Xcode抓到的Zombie對象是一個NSMutableArray,這是一個通用對象,似乎也沒有特別的地方。這個時候我們需要用到Xcode提供的malloc log和Address sanitizer去跟蹤是誰創(chuàng)建的這個對象。

我們在模擬器上面打開malloc log以及Address sanitizer復現(xiàn)問題導出MemGraph然后使用

memory history 地址
malloc log MemGraph 地址

最終定位到問題出現(xiàn)在Flutter引擎內(nèi)部文件 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(通用->輔助功能)支持模塊,我們跟用戶經(jīng)過了交流,并沒有發(fā)現(xiàn)用戶有打開相關的輔助功能。

雖然Log是一摸一樣的,我們有點不相信我們追尋的Crash是由于這個原因?qū)е碌?。這的確是Flutter在Accessibility的一個坑,但是跟我們用戶交流的情形不一致。而且模擬器上面容易出現(xiàn),我們將測試包裝到手機上卻無法在復現(xiàn)這問題。很顯然,用戶都是真機,模擬器或許不能說明問題。此時我們還沒有信心確認這個問題,開輔助功能的人應該是不多的。

這感覺好像在黑暗中看到光亮,一瞬間又被黑暗淹沒了,我們似乎又來到了一個死胡同。到底是哪里出問題了?

用戶面對面

線上交流

在問題排查的過程中我們一直跟用戶保持良好的交流。工程師們主動聯(lián)系用戶,很多用戶也熱心響應我們的訪問,給我們錄制了不少現(xiàn)場的視頻。我們可以看到那些反饋問題的用戶很容易出現(xiàn),但是不出現(xiàn)的用戶基本上沒有這個問題。我們開始懷疑跟賬號的關系,可能有一些ABTest的參數(shù)所有影響。線上的交流雖然給了我們不少有用的信息,但是依然沒有實質(zhì)性突破。

線下面對面

我們開始尋找愿意協(xié)助我們現(xiàn)場排查問題用戶,我們重點找了幾個非常容易出現(xiàn)問題的杭州用戶打算上門現(xiàn)場Debug。在和用戶進行了深入交流以后,其中一個用戶愿意已訪問園區(qū)方式來現(xiàn)場協(xié)助工程師排查問題。

我們選了用戶有時間的一個周末然后拿到用戶的手機進行了調(diào)試,果然在用戶的手機上非常容易復現(xiàn)。而且就是我們前面提到的accessibility_bridge.mm處的崩潰,為什么之前再模擬器上那么容易出現(xiàn)呢?

原來在引擎的代碼中如果是模擬器的話是默認打開Accessibility的,而真機是取決于系統(tǒng)的設置。

#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輔助支持模塊被打開。我們馬上聯(lián)系其它用戶確認,基本上用戶都打開了“閱讀屏幕”功能。至此,我們基本確認就是這個問題所致。我們隨后進行了一個小范圍禁用Accessibility的灰度實驗確認就是這問題導致的Crash。

在經(jīng)過止血修復以后,我們繼續(xù)尋找野指針的源頭。問題出在這個autorelease的NSMutableArray對象,這個代碼看起來也沒什么明顯問題。FLutter引擎的iOS使用MRC進行內(nèi)存管理。我們繼續(xù)review相關的代碼, 終于在SemanticsObject類發(fā)現(xiàn)了一段奇怪的代碼:

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

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

后來我們發(fā)現(xiàn)其實這個問題最近已經(jīng)在Flutter官方master分支上修復了,只是我們自己維護的引擎尚未同步對應的代碼。

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

總結(jié)

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

問題的復現(xiàn)與否通常對于解決方案至關重要,一個能夠復現(xiàn)的問題基本能夠在現(xiàn)代的IDE提供的強大工具的幫助下方便定位到。一開始我們也是苦于沒能找到復現(xiàn)的路徑,原來這個Crash卻被掩蓋在一個并不常見的系統(tǒng)設置下面,同時深藏于Flutter復雜的引擎深部。好在有熱心用戶愿意協(xié)助我們排查問題為我們提供精確的問題現(xiàn)場,才得以最終成功將其確認并解決。


原文鏈接

本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI