大侦探福老师——幽灵Crash谜踪案
閑魚Flutter技術(shù)的基礎(chǔ)設(shè)施已基本趨于穩(wěn)定,就在我們準(zhǔn)備松口氣的時(shí)候,一個(gè)Crash卻異軍突起沖擊著我們的穩(wěn)定性防線!閑魚技術(shù)火速成立偵探小組執(zhí)行嫌犯偵查行動,經(jīng)理重重磨難終于在一個(gè)隱蔽的角落將其繩之以法!
幽靈Crash
問題要從閑魚Flutter基礎(chǔ)設(shè)施上一次大規(guī)模升級說起。2018年我們對閑魚的Flutter基建作了比較大的重構(gòu),目標(biāo)在于提高基建的穩(wěn)定性和可擴(kuò)展性。這個(gè)過程當(dāng)然是挑戰(zhàn)重重,在上一次大規(guī)模的重構(gòu)集成發(fā)版后,我們雖然沒有發(fā)現(xiàn)非常明顯的異常問題,但是Crash率卻出現(xiàn)了一個(gè)比較明顯的增長。雖然總體數(shù)值還在可控范圍之內(nèi),但這一個(gè)Crash卻占據(jù)了幾乎一大半。這個(gè)問題引起了我們警覺,我們立刻成立專項(xiàng)小組重點(diǎn)進(jìn)行排查。
一般Crash Log能夠?yàn)槲覀兌ㄎ籆rash提供主要信息,我們一起看看這個(gè)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)這是一個(gè)很典型的野指針Crash Log,是其中一種俗稱的Over released問題。但是具體是哪個(gè)對象和方法,很難直接從Log上面得知,況且ARC下面的野指針更令人費(fèi)解。
一些推測
Crash理因由變更引入的,我們直覺地從最近發(fā)版引入的主要變更去推測。考慮到我們開始出現(xiàn)問題的版本有幾個(gè)比較大的改造,我們讓相關(guān)的同學(xué)重新review了一下自己的代碼,主要關(guān)注內(nèi)存方面的問題。雖然沒有找到非常確切的問題,我們還是進(jìn)行了一次可疑代碼優(yōu)化,進(jìn)行技術(shù)灰度卻沒有任何效果。在龐大的代碼庫數(shù)不清的提交中去找尋毫無頭緒的野指針問題看起來不是一件容易的事情,
機(jī)型 iOS版本 閑魚版本
我們詳細(xì)的分析了Crash的數(shù)據(jù)以及用戶操作日志,然后得出結(jié)論這個(gè)Crash與機(jī)型,系統(tǒng)版本都沒明顯聯(lián)系。但是我們可以發(fā)現(xiàn)用戶基本上都是在Flutter容器的詳情頁容易崩潰。Flutter不可避免成為了被懷疑對象,包括我們自己實(shí)現(xiàn)的基礎(chǔ)設(shè)施,以及Flutter底層的庫。
但是Flutter已經(jīng)在閑魚應(yīng)用比較長的一段時(shí)間,Flutter底層我們幾乎確定是穩(wěn)定的,不然早就出問題了。這個(gè)時(shí)候主要懷疑點(diǎn)轉(zhuǎn)移到了我們自己實(shí)現(xiàn)的組件,主要包括混合棧組件以及一些監(jiān)控埋點(diǎn)設(shè)施。但是我們隨后將這些懷疑對象通過技術(shù)灰度手段一一排除了嫌疑。
版本走勢
從版本的Crash率的走勢看,我們還發(fā)現(xiàn)這個(gè)問題有一個(gè)緩慢增長放量的過程,這不免讓我們開始懷疑App是否存在類似的慢慢放量的功能需求。然而事實(shí)證明,這個(gè)方向沒有任何收獲。
無法復(fù)現(xiàn)的問題
不斷有用戶向我們反饋容易遇到閃退,但是我們自己的設(shè)備經(jīng)過大量嘗試卻沒有復(fù)現(xiàn)這個(gè)問題。這是最為頭疼的,從用戶的操作路徑來看并無特殊的地方。無論是測試還是開發(fā)同學(xué)都無法在自己設(shè)備上面復(fù)現(xiàn)出來,無法復(fù)現(xiàn)的野指針問題非常難以定位。
線上監(jiān)控技術(shù)
從變更和問題特征排除沒有實(shí)質(zhì)性的進(jìn)展,我們開始嘗試線上的一些監(jiān)控方法來協(xié)助排查。希望可以拿到更加詳細(xì)的相關(guān)信息。
GCD線程跟蹤技術(shù)
從Crash Log我們可以到這應(yīng)該是一個(gè)autorelease對象野指針導(dǎo)致的問題,本來應(yīng)該autorelease進(jìn)行釋放的對象,在其被AutoReleasePool釋放前就因?yàn)槟撤N原因提前釋放。我們懷疑是否存在多線程導(dǎo)致的問題,所以我們采用GCD線程跟蹤技術(shù)進(jìn)行監(jiān)控。
這個(gè)技術(shù)的基本原理是hook住GCD的dispatch方法,將block的返回地址通過?__builtin_return_address函數(shù)拿到,然后編碼寫入到當(dāng)前的線程名中,崩潰的時(shí)候,從線程名字中解碼得出dispather的返回地址即可定位到是誰dispatch的這個(gè)block,然后隨同Crash Log的擴(kuò)展字段將其上傳到后臺。
GCD是一套C接口,所以我們采用fishhook去hook,此類底層hook對性能會有一定影響,所以我們只在專門的技術(shù)驗(yàn)證灰度中采用此項(xiàng)技術(shù)。fishhook的大致原理是重新綁定一些C的符號,因?yàn)楹芏喙蚕淼膸斓姆柋热鏕CD在iOS中是動態(tài)綁定到App的可執(zhí)行文件中的。而目前這部分符號表所在的內(nèi)存沒有簽名,所以可以通過MachO提供的接口去進(jìn)行重新綁定。感興趣的同學(xué)可以參考Facebook fishhook項(xiàng)目。
我們準(zhǔn)備了一個(gè)技術(shù)灰度版本來監(jiān)控這個(gè)問題。可能由于樣本比較小,我們收集到的返回地址數(shù)量非常有限。通過符號解析,得出來的都是一些NSFoundation對象,沒有太多有價(jià)值的東西。之前懷疑這問題可能發(fā)生在GCD執(zhí)行的block中,只是收集崩潰的時(shí)候GCD上一次調(diào)用的返回地址本身也缺乏針對性。
期望是美好的,現(xiàn)實(shí)是骨感覺,最終我們沒有拿到有用的信息。
線上Zombie的野指針監(jiān)控
在Debug模式下,Xcode有用強(qiáng)大的工具去幫助你定位野指針。最為通用的野指針監(jiān)控工具莫過于NSZombie,如果我們能在線上開啟Zombie應(yīng)該能夠很容易的抓到野指針對象。淘系基礎(chǔ)設(shè)施里面有線上Zombie的實(shí)現(xiàn)。
線上的Zombie實(shí)現(xiàn)主要原理hook對象的dealloc方法在dealloc的時(shí)候通過runtime的動態(tài)性將其轉(zhuǎn)變成一個(gè)Zombie類,當(dāng)有其它消息發(fā)給Zombie對象的時(shí)候我們就可以根據(jù)存儲下來的類型定位到Zombie的對象類型。詳細(xì)可以參考Mike Ash的Let's build NSZombie。不過需要注意的是,這里面的實(shí)現(xiàn)是基于MRC,ARC實(shí)現(xiàn)上可能會有差異,基本原理是大致相同的。
我們在閑魚App中根據(jù)基礎(chǔ)提供的文檔將線上Zombie打開進(jìn)行灰度監(jiān)控,所幸的是我們拿到了一些野指針對象。量也不是很多,只有個(gè)位數(shù)的類型。
可能是由于樣本不夠大,沒有覆蓋到典型的用戶。或許是我們的監(jiān)控組件無法抓到這個(gè)特定類型的Crash。最終在排查完所有收集到的野指針對象后,依然沒有解決這個(gè)Crash。
線上監(jiān)控似乎沒能為我們打開突破口。
UI自動化
我們還是期望與能夠?qū)栴}重現(xiàn)出來,這樣可以迅速通過Xcode定位到問題。從概率上確實(shí)不算太高,基于前面手動復(fù)現(xiàn)困難的問題,我們嘗試?yán)米詣踊ぞ呷プ鲎詣訌?fù)現(xiàn)嘗試。
SwiftMonkey + 引擎DEBUG
SwiftMonkey是一個(gè)比較好的UI自動化工具,集成簡單,而且可以在Debug模式下面進(jìn)行自動UI測試。也就是說我們可以在保持Xcode各種強(qiáng)大工具有效的前提下進(jìn)行自動化測試。
我們采用Local Debug Flutter引擎進(jìn)行測試以便拿到相關(guān)的符號,經(jīng)過一段時(shí)間的自動化測試我們在模擬器上面抓到了一摸一樣的Crash Log!
這不得不說是一個(gè)令人振奮的消息,Xcode抓到的Zombie對象是一個(gè)NSMutableArray,這是一個(gè)通用對象,似乎也沒有特別的地方。這個(gè)時(shí)候我們需要用到Xcode提供的malloc log和Address sanitizer去跟蹤是誰創(chuàng)建的這個(gè)對象。
我們在模擬器上面打開malloc log以及Address sanitizer復(fù)現(xiàn)問題導(dǎo)出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;這個(gè)問題把我們帶到了Flutter的Accessibility(通用->輔助功能)支持模塊,我們跟用戶經(jīng)過了交流,并沒有發(fā)現(xiàn)用戶有打開相關(guān)的輔助功能。
雖然Log是一摸一樣的,我們有點(diǎn)不相信我們追尋的Crash是由于這個(gè)原因?qū)е碌摹_@的確是Flutter在Accessibility的一個(gè)坑,但是跟我們用戶交流的情形不一致。而且模擬器上面容易出現(xiàn),我們將測試包裝到手機(jī)上卻無法在復(fù)現(xiàn)這問題。很顯然,用戶都是真機(jī),模擬器或許不能說明問題。此時(shí)我們還沒有信心確認(rèn)這個(gè)問題,開輔助功能的人應(yīng)該是不多的。
這感覺好像在黑暗中看到光亮,一瞬間又被黑暗淹沒了,我們似乎又來到了一個(gè)死胡同。到底是哪里出問題了?
用戶面對面
線上交流
在問題排查的過程中我們一直跟用戶保持良好的交流。工程師們主動聯(lián)系用戶,很多用戶也熱心響應(yīng)我們的訪問,給我們錄制了不少崩潰現(xiàn)場的視頻。我們可以看到那些反饋問題的用戶很容易出現(xiàn),但是不出現(xiàn)的用戶基本上沒有這個(gè)問題。我們開始懷疑跟賬號的關(guān)系,可能有一些ABTest的參數(shù)所有影響。線上的交流雖然給了我們不少有用的信息,但是依然沒有實(shí)質(zhì)性突破。
線下面對面
我們開始尋找愿意協(xié)助我們現(xiàn)場排查問題用戶,我們重點(diǎn)找了幾個(gè)非常容易出現(xiàn)問題的杭州用戶打算上門現(xiàn)場Debug。在和用戶進(jìn)行了深入交流以后,其中一個(gè)用戶愿意已訪問園區(qū)方式來現(xiàn)場協(xié)助工程師排查問題。
我們選了用戶有時(shí)間的一個(gè)周末然后拿到用戶的手機(jī)進(jìn)行了調(diào)試,果然在用戶的手機(jī)上非常容易復(fù)現(xiàn)。而且就是我們前面提到的accessibility_bridge.mm處的崩潰,為什么之前再模擬器上那么容易出現(xiàn)呢?
原來在引擎的代碼中如果是模擬器的話是默認(rèn)打開Accessibility的,而真機(jī)是取決于系統(tǒng)的設(shè)置。
#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, 這導(dǎo)致Flutter輔助支持模塊被打開。我們馬上聯(lián)系其它用戶確認(rèn),基本上用戶都打開了“閱讀屏幕”功能。至此,我們基本確認(rèn)就是這個(gè)問題所致。我們隨后進(jìn)行了一個(gè)小范圍禁用Accessibility的灰度實(shí)驗(yàn)確認(rèn)就是這問題導(dǎo)致的Crash。
在經(jīng)過止血修復(fù)以后,我們繼續(xù)尋找野指針的源頭。問題出在這個(gè)autorelease的NSMutableArray對象,這個(gè)代碼看起來也沒什么明顯問題。FLutter引擎的iOS使用MRC進(jìn)行內(nèi)存管理。我們繼續(xù)review相關(guān)的代碼, 終于在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];,這里不應(yīng)該直接調(diào)用dealloc,而只需要release,這或許就是MRC難以避免的誤寫吧。問題定位到,修復(fù)也就是分分鐘鐘的事情。
后來我們發(fā)現(xiàn)其實(shí)這個(gè)問題最近已經(jīng)在Flutter官方master分支上修復(fù)了,只是我們自己維護(hù)的引擎尚未同步對應(yīng)的代碼。
至此,問題得到圓滿解決,Crash率恢復(fù)到正常水平。
總結(jié)
為了排查這個(gè)問題,我們從多個(gè)方向同時(shí)進(jìn)行了不同的嘗試。具體來說從代碼變更跟蹤,線上監(jiān)控技術(shù),UI自動化以及深入閱讀相關(guān)源碼等方式同時(shí)去推進(jìn)問題的解決。需要特別強(qiáng)調(diào)的是,跟用戶的緊密交流也是解決問題的關(guān)鍵,俗話說知彼知己方能百戰(zhàn)不殆,只有充分理解需要解決的問題才能更有效的將其解決。
問題的復(fù)現(xiàn)與否通常對于解決方案至關(guān)重要,一個(gè)能夠復(fù)現(xiàn)的問題基本能夠在現(xiàn)代的IDE提供的強(qiáng)大工具的幫助下方便定位到。一開始我們也是苦于沒能找到復(fù)現(xiàn)的路徑,原來這個(gè)Crash卻被掩蓋在一個(gè)并不常見的系統(tǒng)設(shè)置下面,同時(shí)深藏于Flutter復(fù)雜的引擎深部。好在有熱心用戶愿意協(xié)助我們排查問題為我們提供精確的問題現(xiàn)場,才得以最終成功將其確認(rèn)并解決。
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的大侦探福老师——幽灵Crash谜踪案的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Grab的实验平台进行混沌实验编排
- 下一篇: 能用机器完成的,千万别堆工作量|持续集成