Crash 防護(hù)方案(三):Container (NSArray、NSDictionary、NSNumber etc.)

原文 : 與佳期的個(gè)人博客(gonghonglou.com)

數(shù)組越界這類的 Crash 是最簡(jiǎn)單的也是最容易出現(xiàn),業(yè)務(wù)開(kāi)發(fā)過(guò)程中很可能操作某個(gè) NSArray 類型的對(duì)象時(shí)忘記判空或者忘記長(zhǎng)度判斷而造成數(shù)組越界崩潰。所以最好是在線上環(huán)境接入這類的 Crash 防護(hù)。當(dāng)然,在開(kāi)發(fā)環(huán)境下最好不要接入,避免縱容開(kāi)發(fā)者出現(xiàn)這類遺忘判斷的錯(cuò)誤。

這類崩潰的防護(hù)方案無(wú)非就是 Hook 可能產(chǎn)生 Crash 的類的相關(guān)方法。之前有過(guò)一篇文章是講這類防護(hù)的:從 SafeKit 看異常保護(hù)及 Method Swizzling 使用分析
SafeKit 并未 Hook 全可能出現(xiàn) Crash 的類及其方法,尤其是 NSArray 類簇。

關(guān)于類簇這里是蘋(píng)果官網(wǎng)文檔:Class Clusters
以及 sunnyxx 在 從NSArray看類簇 文章里的說(shuō)法:

Class Clusters(類簇)是抽象工廠模式在 iOS 下的一種實(shí)現(xiàn),眾多常用類,如 NSString,NSArray,NSDictionary,NSNumber 都運(yùn)作在這一模式下,它是接口簡(jiǎn)單性和擴(kuò)展性的權(quán)衡體現(xiàn),在我們完全不知情的情況下,偷偷隱藏了很多具體的實(shí)現(xiàn)類,只暴露出簡(jiǎn)單的接口。

我們來(lái)仔細(xì)打印下看看:

// 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 的中間對(duì)象,然后在 init 階段給這個(gè)中間對(duì)象發(fā)消息,由它做工廠,生成真正的對(duì)象。其中 NSMutableArray 生成的都是 __NSArrayM 類型,M 代表的就是 Mutable。NSArray 則區(qū)分了數(shù)組里:包含 0 個(gè)對(duì)象時(shí)生成的是 __NSArray0 類型,包含 1 個(gè)對(duì)象生成的是 __NSSingleObjectArrayI 類型,包含多個(gè)對(duì)象時(shí)生成的是 __NSArrayI 類型。

NSDictionary 同樣是類似的。那我們的防護(hù)方案里則是 Hook 全這些類型,比如 NSArray 的 Category:

+ (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]) {
        // 收集堆棧,上報(bào) Crash
        return nil;
    }
    return [self guard_objectAtIndex:index];
}

- (NSArray *)guard_arrayByAddingObject:(id)anObject {
    if (!anObject) {
        // 收集堆棧,上報(bào) Crash
        return self;
    }
    return [self guard_arrayByAddingObject:anObject];
}

當(dāng)然 NSArray、NSMutableArray、NSDictionary、NSMutableDictionary、NSString、NSMutableString、NSNumber 這些類都提供了跟多的方法,只要細(xì)心仔細(xì)的將他們?nèi)?Hook 掉就好了。當(dāng)然實(shí)際開(kāi)發(fā)中可能常用的就那么幾個(gè)方法,Hook 那些就已經(jīng)足夠了。

線上接入了這類的防護(hù)之后要比前邊的文章講的 Unrecognized Selector Crash 和 EXC_BAD_ACCESS Crash 更容易造成業(yè)務(wù)邏輯的錯(cuò)亂,畢竟業(yè)務(wù)邏輯中不可避免的要用到大量的 NSArray、NSDictionary 類,可能在接入這類防護(hù)后會(huì)操成點(diǎn)擊無(wú)響應(yīng)或者頁(yè)面卡死,有時(shí)候這種情況甚至比程序崩潰還讓用戶崩潰,所以也要看實(shí)際開(kāi)發(fā)需要的取舍。在接入防護(hù)后尤其要做好堆棧收集,上報(bào) Crash 的工作,及時(shí)解決掉問(wèn)題。

Demo 地址:GHLCrashGuard:GHLCrashGuard/Classes/Container

后記

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容