原文 : 與佳期的個(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
后記
小白出手,請(qǐng)多指教。如言有誤,還望斧正!
轉(zhuǎn)載請(qǐng)保留原文地址:http://gonghonglou.com/2019/07/07/crash-guard-container/