iOS監(jiān)控筆記之啟動(dòng)crash
前言
相較于正常的崩潰問(wèn)題,啟動(dòng)crash造成的損失要遠(yuǎn)遠(yuǎn)大得多。正常來(lái)說(shuō),如果有足夠強(qiáng)健的構(gòu)建發(fā)布系統(tǒng),大多數(shù)時(shí)候能在版本上線之前及時(shí)發(fā)現(xiàn)問(wèn)題并且修復(fù),但是仍然存在小概率的線上意外。啟動(dòng)crash一般同時(shí)具備損害嚴(yán)重以及難以捕獲兩大特點(diǎn)
啟動(dòng)過(guò)程
從應(yīng)用圖標(biāo)被用戶(hù)點(diǎn)擊開(kāi)始,直到應(yīng)用可以開(kāi)始響應(yīng)發(fā)生了很多事情。正常來(lái)說(shuō),盡管我們希望crash監(jiān)控工具啟動(dòng)的盡可能早,但接入方往往總是等到launch事件之后才能啟動(dòng)工具,而在這個(gè)時(shí)間之前發(fā)生的崩潰就是啟動(dòng)crash,下面列出了在應(yīng)用直到launch時(shí),存在的可能發(fā)生啟動(dòng)crash的階段:
其中initialize的順序可能在更早,但總是會(huì)在load和launch之間。從圖中來(lái)說(shuō),如果我們想要監(jiān)控啟動(dòng)crash,那么開(kāi)始監(jiān)控的時(shí)間點(diǎn)必須要放到load階段,才能保證最好的監(jiān)控效果
如何監(jiān)控
最簡(jiǎn)單的方式是不管接入方愿不愿意啟動(dòng)crash監(jiān)控,我們?cè)趌oad方法中直接啟動(dòng)監(jiān)控功能。但是這樣的做法會(huì)讓?xiě)?yīng)用面臨四個(gè)風(fēng)險(xiǎn)點(diǎn):
- 類(lèi)似A/B的線上開(kāi)關(guān)方案失去了對(duì)監(jiān)控工具的控制能力
- crash監(jiān)控啟動(dòng)存在崩潰問(wèn)題,這將導(dǎo)致應(yīng)用完全癱瘓
- load階段類(lèi)未加載完畢,啟動(dòng)工具過(guò)程的遞歸加載引發(fā)的崩潰無(wú)法監(jiān)控
綜合這些風(fēng)險(xiǎn)點(diǎn),啟動(dòng)crash監(jiān)控的方案應(yīng)該滿(mǎn)足這些條件:
- 啟動(dòng)過(guò)程不依賴(lài)類(lèi),避免遞歸加載造成的crash
- 一旦過(guò)程發(fā)生crash,能夠保證日志記錄的安全性
最終得出監(jiān)控的流程圖:
不依賴(lài)類(lèi)
不依賴(lài)類(lèi)意味著監(jiān)控工具需要使用C接口來(lái)實(shí)現(xiàn)功能,雖然比較麻煩,但由于runtime的機(jī)制決定了所有方法調(diào)用最終要以objc_msgSend函數(shù)作為入口,因此如果能夠hook掉這個(gè)函數(shù)并且實(shí)現(xiàn)一個(gè)調(diào)用棧結(jié)構(gòu),將所有調(diào)用入棧記錄,那么追蹤方法調(diào)用就不是難事。fishhook提供了hook掉函數(shù)的能力:
__unused static id (*orig_objc_msgSend)(id, SEL, ...); __attribute__((__naked__)) static void hook_Objc_msgSend() { /// save stack data /// push msgSend /// resume stack data /// call origin msgSend /// save stack data /// pop msgSend /// resume stack data } void observe_Objc_msgSend() { struct rebinding msgSend_rebinding = { "objc_msgSend", hook_Objc_msgSend, (void *)&orig_objc_msgSend }; rebind_symbols((struct rebinding[1]){msgSend_rebinding}, 1); }
實(shí)現(xiàn)msgSend
__naked__修飾的函數(shù)告訴編譯器在函數(shù)調(diào)用的時(shí)候不使用棧保存參數(shù)信息,同時(shí)函數(shù)返回地址會(huì)被保存到LR寄存器上。由于msgSend本身就是用這個(gè)修飾符的,因此在記錄函數(shù)調(diào)用的出入棧操作中,必須保證能夠保存以及還原寄存器數(shù)據(jù)。msgSend利用x0 - x9的寄存器存儲(chǔ)參數(shù)信息,可以手動(dòng)使用sp寄存器來(lái)存儲(chǔ)和還原這些參數(shù)信息:
/// 保存寄存器參數(shù)信息 #define save() \ __asm volatile ( \ "stp x8, x9, [sp, #-16]!\n" \ "stp x6, x7, [sp, #-16]!\n" \ "stp x4, x5, [sp, #-16]!\n" \ "stp x2, x3, [sp, #-16]!\n" \ "stp x0, x1, [sp, #-16]!\n"); /// 還原寄存器參數(shù)信息 #define resume() \ __asm volatile ( \ "ldp x0, x1, [sp], #16\n" \ "ldp x2, x3, [sp], #16\n" \ "ldp x4, x5, [sp], #16\n" \ "ldp x6, x7, [sp], #16\n" \ "ldp x8, x9, [sp], #16\n" ); /// 函數(shù)調(diào)用,value傳入函數(shù)地址 #define call(b, value) \ __asm volatile ("stp x8, x9, [sp, #-16]!\n"); \ __asm volatile ("mov x12, %0\n" :: "r"(value)); \ __asm volatile ("ldp x8, x9, [sp], #16\n"); \ __asm volatile (#b " x12\n"); /// msgSend必須使用匯編實(shí)現(xiàn) __attribute__((__naked__)) static void hook_Objc_msgSend() { save() __asm volatile ("mov x2, lr\n"); __asm volatile ("mov x3, x4\n"); call(blr, &push_msgSend) resume() call(blr, orig_objc_msgSend) save() call(blr, &pop_msgSend) __asm volatile ("mov lr, x0\n"); resume() __asm volatile ("ret\n"); }
日志記錄
常規(guī)的I/O處理不能保證crash發(fā)生的數(shù)據(jù)安全,因此mmap是最適合用于此場(chǎng)景的方案。mmap能保證即便是應(yīng)用發(fā)生了不可抗拒的崩潰時(shí),也能完成將文件寫(xiě)入IO的工作。另外我們只需記錄class和selector的調(diào)用棧信息,在不存在遞歸算法的情況下,只需要很小的內(nèi)存使用就能記錄這些數(shù)據(jù):
time_t ts = time(NULL); const char *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject stringByAppendingString: [NSString stringWithFormat: @"%d", ts]].UTF8String; unsigned char *buffer = NULL; int fileDescriptor = open(filePath, O_RDWR, 0); buffer = (unsigned char *)mmap(NULL, MB * 4, PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, fileDescriptor, 0);
buffer就是我們寫(xiě)入數(shù)據(jù)的緩沖區(qū),為了保證調(diào)用棧的信息準(zhǔn)確,每次調(diào)用函數(shù)信息出入棧的時(shí)候,都需要更新緩沖區(qū)的數(shù)據(jù)。一個(gè)可行的方式是每個(gè)調(diào)用記錄添加一個(gè)@符號(hào)前綴,總是保存最后一個(gè)調(diào)用記錄的此符號(hào)下標(biāo),出棧時(shí)清除該下標(biāo)之后的所有數(shù)據(jù)即可
static inline void push_msgSend(id _self, Class _cls, SEL _cmd, uintptr_t lr) { _lastIdx = _length; buffer[_lastIdx] = '@'; ...... } static inline void pop_msgSend(id _self, SEL _cmd, uintptr_t lr) { ...... buffer[_lastIdx] = '\0'; _length = _lastIdx; size_t idx = _lastIdx - 1; while (idx >= 0) { if (buffer[idx] == '@') { _lastIdx = idx; break; } idx--; } }
清空日志
由于msgSend的調(diào)用非常頻繁,這種監(jiān)控方案并不適合長(zhǎng)時(shí)間啟動(dòng),因此需要在某個(gè)時(shí)機(jī)關(guān)閉監(jiān)控。由于正常的崩潰監(jiān)控啟動(dòng)時(shí)也可能會(huì)存在crash,監(jiān)聽(tīng)becomeActive通知來(lái)關(guān)閉功能是最合適的選擇,因?yàn)榇藭r(shí)已經(jīng)過(guò)了launch啟動(dòng)崩潰監(jiān)控工具的階段,可以保證該工具本身是正常使用的:
[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(closeMsgSendObserve) name: UIApplicationDidBecomeActiveNotification object: nil]; - (void)closeMsgSendObserve { close(fileDescriptor); munmap(buffer, MB * 4); [[NSFileManager defaultManager] removeItemAtPath: _logPath error: nil]; }
回滾
當(dāng)需要回滾時(shí),說(shuō)明已經(jīng)發(fā)生了啟動(dòng)crash,此時(shí)根據(jù)日志內(nèi)容,也有不同的處理方式:
日志文件是空文件
這種情況是最危險(xiǎn)的情況,如果日志文件為空,說(shuō)明文件已經(jīng)建立,但是還沒(méi)有產(chǎn)生任何方法調(diào)用。很有可能在fishhook的處理過(guò)程中存在crash,此時(shí)應(yīng)該直接關(guān)閉監(jiān)控方案,即便不是它的原因,并且快速增發(fā)版本
日志文件不為空
如果日志文件不為空,說(shuō)明成功的監(jiān)控到了crash,此時(shí)應(yīng)該同步上傳日志文件,快速反饋到業(yè)務(wù)方及時(shí)止損。首先止損手段都應(yīng)該采用同步的方式,保證應(yīng)用能夠繼續(xù)運(yùn)行,根據(jù)情況不同,止損的回滾方式包括以下:
- 如果crash發(fā)生在并不干擾正常業(yè)務(wù)執(zhí)行的功能組件中,可以通過(guò)A/B線上開(kāi)關(guān)關(guān)閉對(duì)應(yīng)的功能,前提是功能組件使用開(kāi)關(guān)控制
- 崩潰處代碼已經(jīng)干擾正常業(yè)務(wù)執(zhí)行,但是錯(cuò)誤代碼短,可以嘗試通過(guò)服務(wù)器下發(fā)patch包動(dòng)態(tài)修復(fù)錯(cuò)誤代碼,但是patch包要提防引入其他問(wèn)題
- 在A/B Test和patch包都無(wú)法解決問(wèn)題的情況下,假如項(xiàng)目采用了合理的組件化設(shè)計(jì),通過(guò)路由轉(zhuǎn)發(fā)來(lái)使用h5完成應(yīng)用的正常運(yùn)行
- 缺少動(dòng)態(tài)修復(fù)的手段且crash不干擾正常業(yè)務(wù)執(zhí)行,考慮停止一切插件、輔助組件運(yùn)行
- 缺少動(dòng)態(tài)修復(fù)的手段,包括1, 2, 3的方案??煽紤]通過(guò)第三方越獄市場(chǎng)提供逆向包,提示用戶(hù)下載安裝
- 缺少動(dòng)態(tài)修復(fù)的手段,包括1, 2, 3的方案。增發(fā)版本快速止損,使用Test Flight分批次快速讓用戶(hù)恢復(fù)使用
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
講解iOS開(kāi)發(fā)中基本的定位功能實(shí)現(xiàn)
這篇文章主要介紹了講解iOS開(kāi)發(fā)中基本的定位功能實(shí)現(xiàn),示例基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-10-10iOS應(yīng)用開(kāi)發(fā)中矢量圖的使用及修改矢量圖顏色的方法
這篇文章主要介紹了iOS應(yīng)用開(kāi)發(fā)中矢量圖的使用及修改矢量圖顏色的方法,文中的方法是在Adobe Illustrator中繪制矢量圖然后導(dǎo)入Xcode中使用,需要的朋友可以參考下2016-03-03iOS的UI開(kāi)發(fā)中Modal的使用與主流應(yīng)用UI結(jié)構(gòu)介紹
這篇文章主要介紹了iOS的UI開(kāi)發(fā)中Modal的使用與主流應(yīng)用UI結(jié)構(gòu),代碼基于傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-12-12淺談WKWebView 在64位設(shè)備上的白屏問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談WKWebView 在64位設(shè)備上的白屏問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04