您好,登錄后才能下訂單哦!
前言
相較于正常的崩潰問題,啟動crash造成的損失要遠遠大得多。正常來說,如果有足夠強健的構建發布系統,大多數時候能在版本上線之前及時發現問題并且修復,但是仍然存在小概率的線上意外。啟動crash一般同時具備損害嚴重以及難以捕獲兩大特點
啟動過程
從應用圖標被用戶點擊開始,直到應用可以開始響應發生了很多事情。正常來說,盡管我們希望crash監控工具啟動的盡可能早,但接入方往往總是等到launch事件之后才能啟動工具,而在這個時間之前發生的崩潰就是啟動crash,下面列出了在應用直到launch時,存在的可能發生啟動crash的階段:
其中initialize的順序可能在更早,但總是會在load和launch之間。從圖中來說,如果我們想要監控啟動crash,那么開始監控的時間點必須要放到load階段,才能保證最好的監控效果
如何監控
最簡單的方式是不管接入方愿不愿意啟動crash監控,我們在load方法中直接啟動監控功能。但是這樣的做法會讓應用面臨四個風險點:
綜合這些風險點,啟動crash監控的方案應該滿足這些條件:
最終得出監控的流程圖:
不依賴類
不依賴類意味著監控工具需要使用C接口來實現功能,雖然比較麻煩,但由于runtime的機制決定了所有方法調用最終要以objc_msgSend函數作為入口,因此如果能夠hook掉這個函數并且實現一個調用棧結構,將所有調用入棧記錄,那么追蹤方法調用就不是難事。fishhook提供了hook掉函數的能力:
__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); }
實現msgSend
__naked__修飾的函數告訴編譯器在函數調用的時候不使用棧保存參數信息,同時函數返回地址會被保存到LR寄存器上。由于msgSend本身就是用這個修飾符的,因此在記錄函數調用的出入棧操作中,必須保證能夠保存以及還原寄存器數據。msgSend利用x0 - x9的寄存器存儲參數信息,可以手動使用sp寄存器來存儲和還原這些參數信息:
/// 保存寄存器參數信息 #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"); /// 還原寄存器參數信息 #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" ); /// 函數調用,value傳入函數地址 #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必須使用匯編實現 __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"); }
日志記錄
常規的I/O處理不能保證crash發生的數據安全,因此mmap是最適合用于此場景的方案。mmap能保證即便是應用發生了不可抗拒的崩潰時,也能完成將文件寫入IO的工作。另外我們只需記錄class和selector的調用棧信息,在不存在遞歸算法的情況下,只需要很小的內存使用就能記錄這些數據:
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就是我們寫入數據的緩沖區,為了保證調用棧的信息準確,每次調用函數信息出入棧的時候,都需要更新緩沖區的數據。一個可行的方式是每個調用記錄添加一個@符號前綴,總是保存最后一個調用記錄的此符號下標,出棧時清除該下標之后的所有數據即可
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的調用非常頻繁,這種監控方案并不適合長時間啟動,因此需要在某個時機關閉監控。由于正常的崩潰監控啟動時也可能會存在crash,監聽becomeActive通知來關閉功能是最合適的選擇,因為此時已經過了launch啟動崩潰監控工具的階段,可以保證該工具本身是正常使用的:
[[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]; }
回滾
當需要回滾時,說明已經發生了啟動crash,此時根據日志內容,也有不同的處理方式:
日志文件是空文件
這種情況是最危險的情況,如果日志文件為空,說明文件已經建立,但是還沒有產生任何方法調用。很有可能在fishhook的處理過程中存在crash,此時應該直接關閉監控方案,即便不是它的原因,并且快速增發版本
日志文件不為空
如果日志文件不為空,說明成功的監控到了crash,此時應該同步上傳日志文件,快速反饋到業務方及時止損。首先止損手段都應該采用同步的方式,保證應用能夠繼續運行,根據情況不同,止損的回滾方式包括以下:
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。