[NSMutableAttributedString initWithData options documentAttributes err

背景

項目中使用了 [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 個彙編指令, cmpb.lecmpcompare 的縮寫也就是比較的意思,而 b.leBranch if Less than or Equal 的縮寫,若是上一條指令 cmp 執行的結果是小於或等於則進行跳轉。 通常來講 arm64 上 x0x7 分別會存放方法的前 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> 是將函數的入參保存到寄存器 x0x1 中, <+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 的實例,那麼取一個實例地址某一個偏移值的內容,是否能夠猜想是讀取實例對象中的一個變量的值,咱們能夠直接在該指令位置打一個斷點,查看此時 x21x24 的值。結果以下圖:

x21和x24寄存器的值

發現此時的 x21 的確是 _WebSafeForwarder 的一個實例對象,而 x24 的值也很像一個變量的偏移值。因爲已經知道是 _WebSafeForwarder 的實例,因此咱們直接打印出其內部變量,以下圖:

_WebSafeForwarder內部變量

能夠發現 _WebSafeForwarder 對象有 4 個變量(忽略 isa),咱們一個一個查看變量的偏移值,看是否有何 x24 的值匹配的變量偏移值。

_WebSafeForwarder變量偏移值

能夠看出第一個變量 target 的偏移值就是 8,和 x24 寄存器相匹配,因此 objc_msgSend 中的第一個參數,實際上就是 _WebSafeForwardertarget 變量。而objc_msgSend的第二個變量是 SEL,咱們也能夠打印出來,以下圖:

objc_msgSend的第二個變量

綜上所訴,實際是在調用 [self.target respondsToSelector:] 時發生崩潰。

這裏咱們能夠有一個大膽的推測,因爲從彙編指令中看,對於 self.target 的取值是直接讀取偏移值,並且沒有調用 objc_loadWeakRetained 方法,因此 _WebSafeForwardertarget 應該不是 weak變量,而是一個 assign變量,因此可猜想是不是因爲 target 所指向的地址已經被釋放,致使訪問 target 時發生了野指針錯誤。

復現問題

從上面的分析中,咱們能夠猜想是因爲 _WebSafeForwardertarget 所指向的對象已經被釋放掉,而 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_WebSafeForwarderforwardInvocation:,在 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


複製代碼

參考文獻

相關文章
相關標籤/搜索