Crash 防禦方案(三):Container (NSArray、NSDictionary、NSNumber etc.

原文 : 與佳期的我的博客(gonghonglou.com)html

數組越界這類的 Crash 是最簡單的也是最容易出現,業務開發過程當中極可能操做某個 NSArray 類型的對象時忘記判空或者忘記長度判斷而形成數組越界崩潰。因此最好是在線上環境接入這類的 Crash 防禦。固然,在開發環境下最好不要接入,避免縱容開發者出現這類遺忘判斷的錯誤。git

這類崩潰的防禦方案無非就是 Hook 可能產生 Crash 的類的相關方法。以前有過一篇文章是講這類防禦的:從 SafeKit 看異常保護及 Method Swizzling 使用分析SafeKit 並未 Hook 全可能出現 Crash 的類及其方法,尤爲是 NSArray 類簇。github

關於類簇這裏是蘋果官網文檔:Class Clusters 以及 sunnyxx 在 從NSArray看類簇 文章裏的說法:數組

Class Clusters(類簇)是抽象工廠模式在 iOS 下的一種實現,衆多經常使用類,如 NSString,NSArray,NSDictionary,NSNumber 都運做在這一模式下,它是接口簡單性和擴展性的權衡體現,在咱們徹底不知情的狀況下,偷偷隱藏了不少具體的實現類,只暴露出簡單的接口。bash

咱們來仔細打印下看看:app

// NSArray
NSLog(@"arr alloc:%@", [NSArray alloc].class); // __NSPlaceholderArray
NSLog(@"arr init:%@", [[NSArray alloc] init].class); // __NSArray0

NSLog(@"arr:%@", [@[] class]); // __NSArray0
NSLog(@"arr:%@", [@[@1] class]); // __NSSingleObjectArrayI
NSLog(@"arr:%@", [@[@1, @2] class]); // __NSArrayI
    
// NSMutableArray
NSLog(@"mutA alloc:%@", [NSMutableArray alloc].class); // __NSPlaceholderArray
NSLog(@"mutA init:%@", [[NSMutableArray alloc] init].class); // __NSArrayM

NSLog(@"mutA:%@", [@[].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1].mutableCopy class]); // __NSArrayM
NSLog(@"mutA:%@", [@[@1, @2].mutableCopy class]); // __NSArrayM

// NSDictionary
NSLog(@"dict alloc:%@", [NSDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"dict init:%@", [[NSDictionary alloc] init].class); // __NSDictionary0

NSLog(@"dict:%@", [@{} class]); // __NSDictionary0
NSLog(@"dict:%@", [@{@1:@1} class]); // __NSSingleEntryDictionaryI
NSLog(@"dict:%@", [@{@1:@1, @2:@2} class]); // __NSDictionaryI

// NSMutableDictionary
NSLog(@"mutD alloc:%@", [NSMutableDictionary alloc].class); // __NSPlaceholderDictionary
NSLog(@"mutD init:%@", [[NSMutableDictionary alloc] init].class); // __NSDictionaryM

NSLog(@"mutD:%@", [@{}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1}.mutableCopy class]); // __NSDictionaryM
NSLog(@"mutD:%@", [@{@1:@1, @2:@2}.mutableCopy class]); // __NSDictionaryM

// NSString
NSLog(@"str:%@", [@"" class]); // __NSCFConstantString

// NSNumber
NSLog(@"num:%@", [@1 class]); // __NSCFNumber
複製代碼

以 NSArray 爲例,他在 alloc 階段生成的是 __NSPlaceholderArray 的中間對象,而後在 init 階段給這個中間對象發消息,由它作工廠,生成真正的對象。其中 NSMutableArray 生成的都是 __NSArrayM 類型,M 表明的就是 Mutable。NSArray 則區分了數組裏:包含 0 個對象時生成的是 __NSArray0 類型,包含 1 個對象生成的是 __NSSingleObjectArrayI 類型,包含多個對象時生成的是 __NSArrayI 類型。ui

NSDictionary 一樣是相似的。那咱們的防禦方案裏則是 Hook 全這些類型,好比 NSArray 的 Category:spa

+ (void)load {
    
    // [NSArray alloc]
    [NSClassFromString(@"__NSPlaceholderArray") jr_swizzleMethod:@selector(initWithObjects:count:) withMethod:@selector(initWithObjects_guard:count:) error:nil];
    // @[]
    [NSClassFromString(@"__NSArray0") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(guard_objectAtIndex:) error:nil];
    [NSClassFromString(@"__NSArray0") jr_swizzleMethod:@selector(arrayByAddingObject:) withMethod:@selector(guard_arrayByAddingObject:) error:nil];
    // @[@1]
    [NSClassFromString(@"__NSSingleObjectArrayI") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(guard_objectAtIndex:) error:nil];
    [NSClassFromString(@"__NSSingleObjectArrayI") jr_swizzleMethod:@selector(arrayByAddingObject:) withMethod:@selector(guard_arrayByAddingObject:) error:nil];
    // @[@1, @2]
    [NSClassFromString(@"__NSArrayI") jr_swizzleMethod:@selector(objectAtIndex:) withMethod:@selector(guard_objectAtIndex:) error:nil];
    [NSClassFromString(@"__NSArrayI") jr_swizzleMethod:@selector(arrayByAddingObject:) withMethod:@selector(guard_arrayByAddingObject:) error:nil];
}

- (instancetype)initWithObjects_guard:(id *)objects count:(NSUInteger)cnt {
    NSUInteger newCnt = 0;
    for (NSUInteger i = 0; i < cnt; i++) {
        if (!objects[i]) {
            break;
        }
        newCnt++;
    }
    self = [self initWithObjects_guard:objects count:newCnt];
    return self;
}

- (id)guard_objectAtIndex:(NSUInteger)index {
    if (index >= [self count]) {
        // 收集堆棧,上報 Crash
        return nil;
    }
    return [self guard_objectAtIndex:index];
}

- (NSArray *)guard_arrayByAddingObject:(id)anObject {
    if (!anObject) {
        // 收集堆棧,上報 Crash
        return self;
    }
    return [self guard_arrayByAddingObject:anObject];
}
複製代碼

固然 NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSString、NSMutableString、NSNumber 這些類都提供了跟多的方法,只要細心仔細的將他們全 Hook 掉就行了。固然實際開發中可能經常使用的就那麼幾個方法,Hook 那些就已經足夠了。code

線上接入了這類的防禦以後要比前邊的文章講的 Unrecognized Selector Crash 和 EXC_BAD_ACCESS Crash 更容易形成業務邏輯的錯亂,畢竟業務邏輯中不可避免的要用到大量的 NSArray、NSDictionary 類,可能在接入這類防禦後會操成點擊無響應或者頁面卡死,有時候這種狀況甚至比程序崩潰還讓用戶崩潰,因此也要看實際開發須要的取捨。在接入防禦後尤爲要作好堆棧收集,上報 Crash 的工做,及時解決掉問題。htm

Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/Container

後記

相關文章
相關標籤/搜索