大侦探福老师——幽灵Crash谜踪案
閑魚Flutter技術的基礎設施已基本趨于穩定,就在我們準備松口氣的時候,一個Crash卻異軍突起沖擊著我們的穩定性防線!閑魚技術火速成立偵探小組執行嫌犯偵查行動,經理重重磨難終于在一個隱蔽的角落將其繩之以法!
幽靈Crash
問題要從閑魚Flutter基礎設施上一次大規模升級說起。2018年我們對閑魚的Flutter基建作了比較大的重構,目標在于提高基建的穩定性和可擴展性。這個過程當然是挑戰重重,在上一次大規模的重構集成發版后,我們雖然沒有發現非常明顯的異常問題,但是Crash率卻出現了一個比較明顯的增長。雖然總體數值還在可控范圍之內,但這一個Crash卻占據了幾乎一大半。這個問題引起了我們警覺,我們立刻成立專項小組重點進行排查。
一般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理因由變更引入的,我們直覺地從最近發版引入的主要變更去推測。考慮到我們開始出現問題的版本有幾個比較大的改造,我們讓相關的同學重新review了一下自己的代碼,主要關注內存方面的問題。雖然沒有找到非常確切的問題,我們還是進行了一次可疑代碼優化,進行技術灰度卻沒有任何效果。在龐大的代碼庫數不清的提交中去找尋毫無頭緒的野指針問題看起來不是一件容易的事情,
機型 iOS版本 閑魚版本
我們詳細的分析了Crash的數據以及用戶操作日志,然后得出結論這個Crash與機型,系統版本都沒明顯聯系。但是我們可以發現用戶基本上都是在Flutter容器的詳情頁容易崩潰。Flutter不可避免成為了被懷疑對象,包括我們自己實現的基礎設施,以及Flutter底層的庫。
但是Flutter已經在閑魚應用比較長的一段時間,Flutter底層我們幾乎確定是穩定的,不然早就出問題了。這個時候主要懷疑點轉移到了我們自己實現的組件,主要包括混合棧組件以及一些監控埋點設施。但是我們隨后將這些懷疑對象通過技術灰度手段一一排除了嫌疑。
版本走勢
從版本的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); #elsebool 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復雜的引擎深部。好在有熱心用戶愿意協助我們排查問題為我們提供精確的問題現場,才得以最終成功將其確認并解決。
原文鏈接
本文為云棲社區原創內容,未經允許不得轉載。
總結
以上是生活随笔為你收集整理的大侦探福老师——幽灵Crash谜踪案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Grab的实验平台进行混沌实验编排
- 下一篇: 能用机器完成的,千万别堆工作量|持续集成