項目中使用了 [NSMutableAttributedString initWithData:options:documentAttributes:error]
來解析 HTML
字符串,可是在線上檢測到了不少該方法崩潰的記錄,以下圖html
因爲在開發環境一直沒辦法復現,因此只能直接分析線上的崩潰記錄。ios
通常來講,會先查看是不是某個機型或iOS 系統版本的問題,可是該問題幾乎涉及到全部的機型,版本號也是從 iOS 8 到 最新的 iOS 12,因此沒辦法從這 2 方面縮小範圍。從上報的崩潰記錄上看,崩潰的主要緣由是SEGV_ACCERR
,也就是常說的野指針訪問。通常來講,發生在訪問一個已經被釋放的內存地址時,會致使該問題。web
從堆棧上看,都是崩潰在算法
libobjc.A.dylib objc_msgSend + 16
複製代碼
這一個方法調用中,雖然 objc_msgSend
方法蘋果沒有開源,可是咱們能夠經過使用 symbolic breakpoint
來斷點該方法,查看其彙編的調用以下(這裏涉及到一些彙編知識):bash
libobjc.A.dylib`objc_msgSend:
-> 0x1c0eccd60 <+0>: cmp x0, #0x0 ; =0x0
0x1c0eccd64 <+4>: b.le 0x1c0eccdcc ; <+108>
0x1c0eccd68 <+8>: ldr x13, [x0]
0x1c0eccd6c <+12>: and x16, x13, #0xffffffff8
0x1c0eccd70 <+16>: ldp x10, x11, [x16, #0x10]
0x1c0eccd74 <+20>: and w12, w1, w11
0x1c0eccd78 <+24>: add x12, x10, x12, lsl #4
0x1c0eccd7c <+28>: ldp x17, x9, [x12]
0x1c0eccd80 <+32>: cmp x9, x1
0x1c0eccd84 <+36>: b.ne 0x1c0eccd8c ; <+44>
複製代碼
因爲崩潰發生在偏移值爲 <+16> 也就是第 5 行的位置,因此只須要分析前 5 行彙編究竟作了什麼就能夠了,下面咱們一行一行進行分析。函數
0x1c0eccd60 <+0>: cmp x0, #0x0
0x1c0eccd64 <+4>: b.le 0x1c0eccdcc ; <+108>
複製代碼
這裏涉及到 2 個彙編指令, cmp
和 b.le
, cmp
是 compare
的縮寫也就是比較的意思,而 b.le
是 Branch if Less than or Equal
的縮寫,若是上一條指令 cmp
執行的結果是小於或等於則進行跳轉。 通常來講 arm64 上 x0
– x7
分別會存放方法的前 8 個參數,若是參數個數超過了8個,多餘的參數會存在棧上,新方法會經過棧來讀取。而返回值通常都在 x0
中。post
因此上面 2 條彙編指令的大概意思是:將 objc_msgSend
的第一個參數和 0 進行比較,若是值小於或等於 0,則跳轉到地址 0x1c0eccdcc
,而 objc_msgSend
的第一個參數其實就是 self
,因此這裏是判斷 self
是不是 nil
,若是是 nil
就直接跳轉到 0x1c0eccdcc
, 0x1c0eccdcc
這裏沒有寫出來,能夠理解成 objc_msgSend
的結束位置(不是很恰當)。ui
0x1c0eccd68 <+8>: ldr x13, [x0]
複製代碼
ldr
是讀取指令,是指從存取器中讀取加載到寄存器中。因此上面的指令加載寄存器 x0
指向的內容到寄存器 x13
中。若是將 x0
理解成 c 語言中的指針,上面的指令能夠理解成 x13 = *x0
;atom
0x1c0eccd6c <+12>: and x16, x13, #0xffffffff8
複製代碼
and
指令,就是 x16 = x13 & #0xffffffff8
,很簡單的算法指令。spa
0x1c0eccd70 <+16>: ldp x10, x11, [x16, #0x10]
複製代碼
該指令也就是發生崩潰的地方,也是一條加載指令 ldp
,是指 從 x16 + 0x10
指向的地址裏面取出 2 個 64 位的數,分別存入 x10
, x11
。在執行該指令的時候,出現了野指針錯誤,雖然沒有具體的源碼,可是咱們反推 x16
地址的來源來縮小範圍,x16
來自於 x13
,而 x13
又來自於 x0
,因此也就是說,是在對 objc_msgSend
第一個參數進行操做時致使產生野指針崩潰。
到這裏,objc_msgSend
已經沒有什麼能夠分析的內容了,下一步是分析
1 WebKitLegacy -[_WebSafeForwarder forwardInvocation:] + 132
複製代碼
一樣的辦法,咱們在 [_WebSafeForwarder forwardInvocation:]
打一個斷點,直接跳到 <+ 132 >
位置進行分析,其內容以下:
WebKitLegacy`-[_WebSafeForwarder forwardInvocation:]:
-> 0x1cbed13fc <+0>: stp x24, x23, [sp, #-0x40]!
0x1cbed1400 <+4>: stp x22, x21, [sp, #0x10]
0x1cbed1404 <+8>: stp x20, x19, [sp, #0x20]
0x1cbed1408 <+12>: stp x29, x30, [sp, #0x30]
0x1cbed140c <+16>: add x29, sp, #0x30 ; =0x30
0x1cbed1410 <+20>: mov x19, x2
0x1cbed1414 <+24>: mov x21, x0
0x1cbed1418 <+28>: bl 0x1caadcd1c ; WebThreadIsCurrent
0x1cbed141c <+32>: cbz w0, 0x1cbed1448 ; <+76>
0x1cbed1420 <+36>: adrp x8, 145109
0x1cbed1424 <+40>: add x1, x8, #0xe2a ; =0xe2a
0x1cbed1428 <+44>: mov x0, x19
0x1cbed142c <+48>: bl 0x1c8d16378
0x1cbed1430 <+52>: mov x0, x19
0x1cbed1434 <+56>: ldp x29, x30, [sp, #0x30]
0x1cbed1438 <+60>: ldp x20, x19, [sp, #0x20]
0x1cbed143c <+64>: ldp x22, x21, [sp, #0x10]
0x1cbed1440 <+68>: ldp x24, x23, [sp], #0x40
0x1cbed1444 <+72>: b 0x1caadd294 ; WebThreadCallDelegate
0x1cbed1448 <+76>: adrp x8, 187655
0x1cbed144c <+80>: ldrsw x24, [x8, #0x428]
0x1cbed1450 <+84>: ldr x23, [x21, x24]
0x1cbed1454 <+88>: adrp x8, 145010
0x1cbed1458 <+92>: add x20, x8, #0x6e0 ; =0x6e0
0x1cbed145c <+96>: mov x0, x19
0x1cbed1460 <+100>: mov x1, x20
0x1cbed1464 <+104>: bl 0x1c8d16378
0x1cbed1468 <+108>: mov x2, x0
0x1cbed146c <+112>: adrp x8, 145009
0x1cbed1470 <+116>: add x22, x8, #0x5e0 ; =0x5e0
0x1cbed1474 <+120>: mov x0, x23
0x1cbed1478 <+124>: mov x1, x22
0x1cbed147c <+128>: bl 0x1c8d16378
0x1cbed1480 <+132>: cbz w0, 0x1cbed149c ; <+160>
複製代碼
此次咱們從後往前分析,因爲彙編語言的特性(這裏就不詳細講解),雖然崩潰是指向<+132>
,但實際上在調用上一行彙編指令致使的崩潰,因此咱們直接從 <+128>
開始分析。
0x1cbed1474 <+120>: mov x0, x23
0x1cbed1478 <+124>: mov x1, x22
0x1cbed147c <+128>: bl 0x1c8d16378
複製代碼
上面 3 行指令其實是一個函數的調用過程,<+120>
和 <+124>
是將函數的入參保存到寄存器 x0
和 x1
中, <+128>
跳轉到指定的地址,也就是調用函數。該函數就是咱們上面分析的 objc_msgSend
的函數,因爲咱們上面已經分析得出,是因爲第一個參數,也就是寄存器中的值 x0
出現問題,致使了野指針錯誤,因此咱們直接按照 objc_msgSend
分析思路,分析 x0
的來源,下面列出了 x0
相關的幾條指令
WebKitLegacy`-[_WebSafeForwarder forwardInvocation:]:
-> ...
0x1cbed1414 <+24>: mov x21, x0 // 將 x0 的賦值給 x21
.....
0x1cbed1448 <+76>: adrp x8, 187655 // 讀取 pc + 187655 地址的內容
0x1cbed144c <+80>: ldrsw x24, [x8, #0x428] // 加載 x8 + 0x428 地址的內容到 x24中
0x1cbed1450 <+84>: ldr x23, [x21, x24] // 加載 x21 + x24 地址的內容到 x23 中
....
0x1cbed1474 <+120>: mov x0, x23 // 將 x23 的值賦值給 x0
0x1cbed1478 <+124>: mov x1, x22
0x1cbed147c <+128>: bl 0x1c8d16378
0x1cbed1480 <+132>: cbz w0, 0x1cbed149c ; <+160>
複製代碼
從上面的精簡指令中,咱們能夠知道,objc_msgSend
的第一個參數來自 x23
, x23
來自 x21
,而 x21
又是來自 x0
, 也就是 forwardInvocation:
方法的第一個參數,實際上就是 _WebSafeForwarder
的實例對象。
這裏咱們重點分析下 <+84>
這一行指令
0x1cbed1450 <+84>: ldr x23, [x21, x24] // 加載 x21 + x24 地址的內容到 x23 中
複製代碼
該指令是取 x21 + x24
地址的內容,咱們已經知道 x21
是 _WebSafeForwarder
的實例,那麼取一個實例地址某一個偏移值的內容,是否能夠猜想是讀取實例對象中的一個變量的值,咱們能夠直接在該指令位置打一個斷點,查看此時 x21
和 x24
的值。結果以下圖:
發現此時的 x21
的確是 _WebSafeForwarder
的一個實例對象,而 x24
的值也很像一個變量的偏移值。因爲已經知道是 _WebSafeForwarder
的實例,因此咱們直接打印出其內部變量,以下圖:
能夠發現 _WebSafeForwarder
對象有 4 個變量(忽略 isa),咱們一個一個查看變量的偏移值,看是否有何 x24
的值匹配的變量偏移值。
能夠看出第一個變量 target
的偏移值就是 8,和 x24
寄存器相匹配,因此 objc_msgSend
中的第一個參數,實際上就是 _WebSafeForwarder
的 target
變量。而objc_msgSend
的第二個變量是 SEL
,咱們也能夠打印出來,以下圖:
綜上所訴,實際是在調用 [self.target respondsToSelector:]
時發生崩潰。
這裏咱們能夠有一個大膽的推測,因爲從彙編指令中看,對於 self.target
的取值是直接讀取偏移值,並且沒有調用 objc_loadWeakRetained
方法,因此 _WebSafeForwarder
中 target
應該不是 weak
變量,而是一個 assign
變量,因此可猜想是不是因爲 target
所指向的地址已經被釋放,致使訪問 target
時發生了野指針錯誤。
從上面的分析中,咱們能夠猜想是因爲 _WebSafeForwarder
的 target
所指向的對象已經被釋放掉,而 target
又沒有被設置爲 nil
致使程序奔潰,爲了驗證這個猜測,咱們人爲的製造一個 crash,將堆棧信息和線上崩潰的堆棧信息進行對比,若是是一致的,就能夠確認是該緣由致使的,復現代碼以下:
CG_INLINE void
SwizzleMethod(Class _originClass, SEL _originSelector, Class _newClass, SEL _newSelector) {
Method oriMethod = class_getInstanceMethod(_originClass, _originSelector);
Method newMethod = class_getInstanceMethod(_newClass, _newSelector);
class_addMethod(_originClass, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
BOOL isAddedMethod = class_addMethod(_originClass, _originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (isAddedMethod) {
class_replaceMethod(_originClass, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, newMethod);
}
}
@interface NSObject (EPWebSafe_Private)
@end
@implementation NSObject (EPWebSafe)
+ (void)load {
SwizzleMethod(NSClassFromString(@"_WebSafeForwarder"),NSSelectorFromString(@"forwardInvocation:"), self , @selector(safe_forwardInvocation:));
}
- (void)safe_forwardInvocation:(NSInvocation *)arg1 {
if ([NSStringFromSelector(arg1.selector) isEqualToString:@"webView:willCloseFrame:"] && [NSThread isMainThread]) {
@autoreleasepool {
Class delegateClass = NSClassFromString(@"NSHTMLWebDelegate");
id newDelegate = [delegateClass new];
object_setIvarValue(self, "target", newDelegate);
}
}
return [self safe_forwardInvocation:arg1];
}
@end
複製代碼
復現的思路是,hook
的 _WebSafeForwarder
的 forwardInvocation:
,在 safe_forwardInvocation:
中將 target
設置成一個臨時變量,採用 @autoreleasepool
是爲了模擬 target
指向的對象已經被釋放,可是 target
並無被設置爲 nil
的現象。if ([NSStringFromSelector(arg1.selector) isEqualToString:@"webView:willCloseFrame:"] && [NSThread isMainThread])
這個判斷條件是因爲 forwardInvocation:
會被用於不少種用處,添加上面的判斷條件是爲了保證 forwardInvocation:
簽名調用堆棧和線上的保持一致。運行結果以下圖:
經過對比線上的崩潰堆棧,以下圖
能夠發現崩潰的堆棧信息是如出一轍的,因此能夠基本能夠肯定線上的崩潰就是因爲該問題引發的。
肯定問題的根源後,就很好解決了,這裏的解決方法參考了同事以前實現的一個防止 iOS8 上面 UIScrollView
delegate 指向內容被釋放後,還被調用致使的崩潰。實現思路能夠查看連接 優雅解決 iOS 8 UIScrollView delegate EXC_BAD_ACCESS
具體代碼以下:
#import "NSObject+EPWebSafe.h"
#define object_getIvarValue(object, name) object_getIvar(object, class_getInstanceVariable([object class], name))
#define object_setIvarValue(object, name, value) object_setIvar(object, class_getInstanceVariable([object class], name), value)
CG_INLINE void
SwizzleMethod(Class _originClass, SEL _originSelector, Class _newClass, SEL _newSelector) {
Method oriMethod = class_getInstanceMethod(_originClass, _originSelector);
Method newMethod = class_getInstanceMethod(_newClass, _newSelector);
BOOL a = class_addMethod(_originClass, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
BOOL isAddedMethod = class_addMethod(_originClass, _originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
if (isAddedMethod) {
class_replaceMethod(_originClass, _newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, newMethod);
}
}
@interface HtmlReleaseDelegateCleaner : NSObject
@property (nonatomic, strong) NSPointerArray *htmlDelegates;
@end
@implementation HtmlReleaseDelegateCleaner
- (void)dealloc {
[self cleanHtmlDelegate];
}
- (void)recordHtmlDelegate:(id)htmlDelegate {
NSUInteger index = [self.htmlDelegates.allObjects indexOfObject:htmlDelegate];
if (index == NSNotFound) {
[self.htmlDelegates addPointer:(__bridge void *)(htmlDelegate)];
}
}
- (void)removeHtmlDelegate:(id )htmlDelegate {
NSUInteger index = [self.htmlDelegates.allObjects indexOfObject:htmlDelegate];
if (index != NSNotFound) {
[self.htmlDelegates removePointerAtIndex:index];
}
}
- (void)cleanHtmlDelegate {
[self.htmlDelegates.allObjects enumerateObjectsUsingBlock:^(id htmlDelegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([htmlDelegate isKindOfClass:NSClassFromString(@"_WebSafeForwarder")]) {
object_setIvarValue(htmlDelegate, "target", nil);
}
}];
}
- (void)setHtmlDelegates:(NSMutableSet *)htmlDelegates {
objc_setAssociatedObject(self, @selector(htmlDelegates), htmlDelegates, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSPointerArray *)htmlDelegates {
NSPointerArray *htmlDelegates = objc_getAssociatedObject(self, _cmd);
if (!htmlDelegates) {
htmlDelegates = [NSPointerArray weakObjectsPointerArray];
[self setHtmlDelegates:htmlDelegates];
}
return htmlDelegates;
}
@end
@interface NSObject (EPWebSafe_Private)
@property (nonatomic, readonly) HtmlReleaseDelegateCleaner *webDelegateCleaner;
@end
@implementation NSObject (EPWebSafe)
- (HtmlReleaseDelegateCleaner *)webDelegateCleaner {
HtmlReleaseDelegateCleaner *cleaner = objc_getAssociatedObject(self, _cmd);
if (!cleaner) {
cleaner = [HtmlReleaseDelegateCleaner new];
objc_setAssociatedObject(self, _cmd, cleaner, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return cleaner;
}
+ (void)load {
SwizzleMethod(NSClassFromString(@"_WebSafeForwarder"),NSSelectorFromString(@"initWithTarget:defaultTarget:"), self ,@selector(safe_initWithTarget:defaultTarget:));
}
- (id)safe_initWithTarget:(id)arg1 defaultTarget:(id)arg2 {
if ([NSStringFromClass([arg1 class]) isEqualToString:@"NSHTMLWebDelegate"]) {
[[arg1 webDelegateCleaner] recordHtmlDelegate: self];
}
return [self safe_initWithTarget:arg1 defaultTarget:arg2];
}
@end
複製代碼