大偵探福老師——幽靈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是否存在類似的慢慢放量的功能需求。然而事實證明,這個方向沒有任何收穫。
無法復現的問題
不斷有使用者向我們反饋容易遇到閃退,但是我們自己的裝置經過大量嘗試卻沒有復現這個問題。這是最為頭疼的,從使用者的操作路徑來看並無特殊的地方。無論是測試還是開發同學都無法在自己裝置上面復現出來,無法復現的野指標問題非常難以定位。
線上監控技術
從變更和問題特徵排除沒有實質性的進展,我們開始嘗試線上的一些監控方法來協助排查。希望可以拿到更加詳細的相關資訊。
執行緒跟蹤技術
從Crash Log我們可以到這應該是一個autorelease物件野指標導致的問題,本來應該autorelease進行釋放的物件,在其被AutoReleasePool釋放前就因為某種原因提前釋放。我們懷疑是否存在多執行緒導致的問題,所以我們採用執行緒跟蹤技術進行監控。
這個技術的基本原理是hook住的dispatch方法,將block的返回地址透過
__builtin_return_address
函式拿到,然後編碼寫入到當前的執行緒名中,的時候,從執行緒名字中解碼得出dispather的返回地址即可定位到是誰dispatch的這個block,然後隨同Crash Log的擴充套件欄位將其上傳到後臺。
是一套C介面,所以我們採用fishhook去hook,此類底層hook對效能會有一定影響,所以我們只在專門的技術驗證灰度中採用此項技術。fishhook的大致原理是重新繫結一些C的符號,因為很多共享的庫的符號比如在iOS中是動態繫結到App的可執行檔案中的。而目前這部分符號表所在的記憶體沒有簽名,所以可以透過MachO提供的介面去進行重新繫結。感興趣的同學可以參考Facebook fishhook專案。
我們準備了一個技術灰度版本來監控這個問題。可能由於樣本比較小,我們收集到的返回地址數量非常有限。透過符號解析,得出來的都是一些NSFoundation物件,沒有太多有價值的東西。之前懷疑這問題可能發生在執行的block中,只是收集的時候上一次呼叫的返回地址本身也缺乏針對性。
期望是美好的,現實是骨感覺,最終我們沒有拿到有用的資訊。
線上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複雜的引擎深部。好在有熱心使用者願意協助我們排查問題為我們提供精確的問題現場,才得以最終成功將其確認並解決。
本文為雲棲社群原創內容,未經允許不得轉載。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69922229/viewspace-2643930/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 解開女兒失蹤真相 偵探解謎冒險遊戲《蛛絲詭跡》正式發售遊戲
- 《迷霧偵探》國產賽博朋克偵探遊戲遊戲
- 幽靈選單介紹;
- 011 Rust死靈書之幽靈資料Rust
- 什麼是幽靈依賴
- [MtOI2019] 幽靈樂團
- 密室、殺人、謎團……綜藝帶火的解密推理風潮,引全民偵探?解密
- 硬核技術宅偵探和他的007黑貓——《迷霧偵探》評測
- 黑暗幽靈(DCM)木馬詳細分析
- 《橘貓偵探社》手遊今日全平臺公測 與萌貓偵探一起推理破案
- 避坑!一個幽靈字元!U200b字元
- 育碧Ubisoft新作《幽靈行動:斷點》首度亮相斷點
- 《大偵探皮卡丘》全球票房躋身遊改電影第二
- Vue render深入窺探之謎Vue
- Win10系統怎樣解除安裝幽靈熔斷補丁_win10解除安裝幽靈補丁的方法Win10
- 主機偵探:Centos7系統使用chmod修改檔案許可權方法CentOS
- 4399《胡偵探傳說》系列背後的故事
- 主機偵探:ResellerClub多重措施改善主機效能
- idou老師教你學Istio 22 : 如何用istio實現呼叫鏈跟蹤
- 到《幽靈行動:斷點》,歐美大廠們終於把“開放世界”玩死了斷點
- 育碧的幽靈行動:轉型主流的不適感
- 如何解決分散式系統中的“幽靈復現”?分散式
- Nuxt.js 錯誤偵探:useError 組合函式UXJSError函式
- 《Lacuna》:偵探的煙,和他的存在主義危機
- 【吳軍老師推薦】大學書單
- 幽靈詭計:遊戲敘事的意義、方法與技巧遊戲
- 《幽靈行者》:近期最酷炫的賽博朋克遊戲之一遊戲
- [App探索]JSBox中幽靈觸發器的實現原理探索APPJS觸發器
- 洛谷P1039 [NOIP2003 提高組] 偵探推理
- 【趙渝強老師】PostgreSQL的資料檔案SQL
- 【趙渝強老師】MySQL的引數檔案MySql
- 【TRACE】如何設定或動態跟蹤Oracle net偵聽器Oracle
- 懸疑解謎再出發,美術細節全升級,《迷霧偵探》團隊新作給我們帶來了哪些驚喜?
- G1《狙擊手幽靈戰士:契約2》總結
- 涅槃團隊:Xcode幽靈病毒存在惡意下發木馬行為XCode
- SAP UI的載入動畫效果和幽靈設計(Ghost Design)UI動畫
- 科幻偵探冒險遊戲《地平線之間》正式發售遊戲
- Three.js 實現3D全景偵探小遊戲?️JS3D遊戲