我們開發(fā)APP,雖然在極力避免出現(xiàn)線上crash,但是某些情況還是沒法把控,比如和后端約定好的數(shù)據(jù)格式,突然哪天給你換了,很容易導(dǎo)致crash。但是如果我們在任何地方都做防御性判斷,代碼會寫得特別難受。
之前看到有人開源了防止crash的代碼,所以分析了下。這些方案主要利用runtime的方法交換和消息轉(zhuǎn)發(fā)來實現(xiàn),對那些容易引起crash的方法,添加判斷,或者在crash之后走消息轉(zhuǎn)發(fā)。
之前項目用到這個,NSObjectSafe就是這么一個開源庫,只需要拖進(jìn)工程就可以起作用。
NSObjectSafe代碼地址:github鏈接
比如,向NSArray插入一個nil、獲取NSArray長度之外的元素等等,從而導(dǎo)致APP奔潰。NSObjectSafe能有效避免這些奔潰。這些方案,基本都在load中交換這些容易引起crash的方法,并且添加判斷,以此來避免異常。簡化版就是下面這樣:
@implementation NSArray (Safe)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/* 沒內(nèi)容類型是__NSArray0 */
swizzleInstanceMethod(NSClassFromString(@"__NSArray0"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));
/* 有內(nèi)容obj類型才是__NSArrayI */
swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));
.
.//還有很多
.
});
}
- (id) hookObjectAtIndex:(NSUInteger)index {
@synchronized (self) {
if (index < self.count) {
return [self hookObjectAtIndex:index];
}
SFAssert(NO, @"NSArray invalid index:[%@]", @(index));
return nil;
}
}
可以看到,這里面用了個+load 方法、runtime的方法交換。
那么 +load 到底是如何被調(diào)用的呢?
在 iOS 開發(fā)中,我們經(jīng)常會使用 +load 方法來做一些在 main 函數(shù)之前的操作,比如方法交換(Method Swizzle)等。
+load()方法的調(diào)用時機是這樣的:
1、當(dāng)類或分類被添加到 Obj-C 運行時的時候被調(diào)用;可以實現(xiàn)該方法用來在加載時刻執(zhí)行特定類的操作。
2、動態(tài)加載和靜態(tài)鏈接都能將 load 消息發(fā)送到類和分類,但前提是新加載類或分類實現(xiàn)了要響應(yīng)的方法。
3、初始化的順序如下:
鏈接的所有框架(Framework)中全部的構(gòu)造器。
鏡像(Image)中所有的 +load 方法。
鏡像中所有的 C++ 靜態(tài)構(gòu)造器,以及 C/C++ 的 attribute(constructor) 函數(shù)。
框架中鏈接的所有構(gòu)造器。
4、類的 +load 方法在其所有父類的 +load 方法調(diào)用之后調(diào)用。
5、分類的 +load 方法在其主類的 +load 方法調(diào)用之后調(diào)用。
了解到 +load 在運行時初始化加載鏡像時就會被調(diào)用,使得可以有機會預(yù)先做很多事情。但正是因為其加載的時機非??壳埃绻?+load 方法中做比較復(fù)雜且在主線程的操作,將會影響 App 啟動時間,降低用戶體驗。所以APP啟動優(yōu)化可以盡量把放到這個方法的任務(wù)往后放,比如+(void)initialize中,他會在每個類初始化的時候調(diào)用一次。
runtime會在運行時調(diào)用工程中的類的+load方法,并且不需要類在代碼中被顯示import,所以有些第三方可拖進(jìn)工程就會起作用,而不需要顯示import。比如:IQKeyboad、NSObjectSafe...
從這里可以解開我之前的一個誤解:一直以為垃圾代碼(尤其是第三方庫)留在工程里面不管他,以為它不會起作用,如果他里面實現(xiàn)了 +load 方法,還是可能會起作用的,一旦出bug了,那叫一個難找。
接著,看看runtime的方法交換:
+ (void)load{
NSLog(@"UIViewController Hook load");
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method oriMethod = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc"));
Method repMethod = class_getInstanceMethod([self class], @selector(xx_dealloc));
method_exchangeImplementations(oriMethod, repMethod);
});
}
- (void)xx_dealloc{
NSLog(@"%@ dealloc",NSStringFromClass([self class]));
}
或者像這樣(根據(jù)系統(tǒng)版本修改字號):
@implementation UIFont (hook)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleClassMethod:@selector(systemFontOfSize:) withMethod:@selector(xx_systemFontOfSize:)];
});
}
+ (UIFont *)xx_systemFontOfSize:(CGFloat)fontSize{
NSString *version = [UIDevice currentDevice].systemVersion;
if (version.doubleValue >= 11.0) {
return [UIFont xx_systemFontOfSize:25.0f];
} else {
return [UIFont xx_systemFontOfSize:14.0f];
}
}
@end
為什么xx_systemFontOfSize里面又調(diào)用了xx_systemFontOfSize?不會死循環(huán)么?
嗯,這里不會,因為交換了方法的IMP,這里不細(xì)說。
看NSObjectSafe源碼,hook那么多不常見的類,為什么呢?比如__NSArray0、__NSArrayI、__NSSingleObjectArrayI等等。
這些都是NSArray在某些情況下的實際類,NSArray在這里使用了類簇的模式。
類簇是一種設(shè)計模式,它包含了一組私有的具體的類,這些類繼承一個公開的抽象類,也即是基類,基類負(fù)責(zé)提供對外接口供調(diào)用者使用,具體類負(fù)責(zé)方法的真正實現(xiàn), 我們只需要調(diào)用基類提供的接口來實現(xiàn)相關(guān)功能,而無需關(guān)心背后的具體實現(xiàn)細(xì)節(jié)。
在Cocoa中,許多類實際上是以類簇的方式實現(xiàn)的,即它們是一群隱藏在通用接口之下的與實現(xiàn)相關(guān)的類。例如創(chuàng)建NSString對象時,實際上獲得的可能是NSLiteralString、NSCFString、NSSimpleCString、NSBallOfString或者其他未寫入文檔的與實現(xiàn)相關(guān)的對象。
NSNumber就是最常用的類簇實現(xiàn)的類之一。

這種模式的好處就是,可以隱藏私有類,調(diào)用者只能使用基類暴露的API,而無需關(guān)心里面是什么類,以及背后的具體實現(xiàn)細(xì)節(jié)。即使將來添加更多的私有類,對外的基類API也沒什么變化。
技術(shù)分享記錄~