開發(fā)過程中膝但,即使我們很注意的去寫代碼,但是還是不能百分百的保證避免程序的Crash汗销;iOS應(yīng)用Crash保護(hù)系統(tǒng) 的設(shè)計初衷,就是降低APP的崩潰率抵窒。利用Objective-C語言的動態(tài)特性弛针,采用面向切面編程的設(shè)計思想,做到無痕植入李皇。能夠自動在APP運行時實時捕獲導(dǎo)致APP崩潰的原因削茁,然后通過特定的技術(shù)手段去解決這些問題,使APP免于崩潰掉房,繼續(xù)運行茧跋,為APP的持續(xù)運轉(zhuǎn)保駕護(hù)航。
功能簡介
iOS應(yīng)用Crash保護(hù)系統(tǒng) 計劃解決程序運行過程中的大部分崩潰卓囚,但也有一些比較難發(fā)生的崩潰沒有找到具體原因和解決方案瘾杭,該方案主要從以下幾個方面進(jìn)行處理:
-
unrecognized selector
引起的崩潰 - 容器類數(shù)據(jù)類型操作引起的崩潰
- 字符串操作引起的崩潰
-
KVO
引起的崩潰 -
NSTimer
引起的崩潰 - 非主線程刷新UI
- 野指針
-
NSNotification
引起的崩潰
實現(xiàn)原理
unrecognized selector
引起的崩潰的防護(hù)
unrecognized selector
的崩潰在APP中占了很大比例,具體造成原因通常是:一個對象調(diào)用一個自己沒有實現(xiàn)的方法造成的哪亿;
例如:
NSObject *obj = [NSObject new];
[obj methodNoRealize];
具體錯誤原因如下:
-[obj methodNoRealize]: unrecognized selector sent to instance 0x60000087xxxxx
要解決該類問題粥烁,我們可以從OC消息轉(zhuǎn)發(fā)的過程中找到答案;首先看一下方法調(diào)用和消息轉(zhuǎn)發(fā)流程:
當(dāng)對象obj
調(diào)用方法methodNoRealize
時蝇棉,會執(zhí)行以下步驟
1.首先讨阻,在obj
對應(yīng)類的緩存方法列表中找methodNoRealize
,如果找到篡殷,轉(zhuǎn)向相應(yīng)實現(xiàn)并執(zhí)行变勇;
2.如果沒找到,在obj
的方法列表中找methodNoRealize
,如果找到搀绣,轉(zhuǎn)向相應(yīng)實現(xiàn)執(zhí)行;
3.如果沒找到戳气,去父類指針?biāo)赶虻膶ο笾袌?zhí)行1链患,2;
4.以此類推瓶您,如果一直到根類還沒找到麻捻,轉(zhuǎn)向攔截調(diào)用,走消息轉(zhuǎn)發(fā)機制呀袱;
消息轉(zhuǎn)發(fā)機制如下圖:
在消息轉(zhuǎn)發(fā)流程中贸毕,有三次機會可以“拯救”沒有實現(xiàn)的方法引起的崩潰,分別再消息轉(zhuǎn)發(fā)的三步流程中夜赵,我們可以通過HOOK這三步的方法實現(xiàn)對該類崩潰的保護(hù)明棍;
1.+ (BOOL)resolveInstanceMethod:(SEL)sel
:obj
找不到methodNoRealize
之后,最先執(zhí)行該方法寇僧,此方法返回值是BOOL,沒有找到就是NO,找到就返回YES,
在此方法中解決的方法:在obj
的類中加入methodNoRealize
,并綁定方法實現(xiàn)摊腋,具體操作如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSMethodSignature* sign = [self methodSignatureForSelector:sel];
if (!sign) {
class_addMethod([self class], sel, (IMP)unrecognizedSelector, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)unrecognizedSelector
{
// do something
}
此步操作可以解決該問題,但是會在類中添加一個方法嘁傀,在開發(fā)過程中兴蒸,往往不是因為自己本類方法沒有實現(xiàn)引起這種崩潰,而是對象類型判斷錯誤導(dǎo)致细办,這樣就會給未知的類中添加一個方法橙凳,對類造成污染;故笑撞,在這里解決可行岛啸,但不是最佳的方式;
2.- (id)forwardingTargetForSelector:(SEL)aSelector
:當(dāng)?shù)谝徊椒祷亟Y(jié)果為NO
時娃殖,就會走到該方法中值戳,在這個方法中,可以將obj
查找不到的方法轉(zhuǎn)發(fā)到另外一個對象中去炉爆,在另外對象中進(jìn)行處理堕虹;具體操作如下:
- (id)safeForwardingTargetForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [self methodSignatureForSelector:aSelector];
if (!methodSignature) {
id obj = [[HYUnrecognizedSelectorHandle alloc] init];
IMP imp = class_getMethodImplementation([HYUnrecognizedSelectorHandle class], @selector(unrecognizedSelector));
class_addMethod([obj class], aSelector, imp, "v@:");
return obj;
}
return [self safeForwardingTargetForSelector:aSelector];
}
在此步中,可以實例化一個預(yù)先寫好的類的對象HYUnrecognizedSelectorHandle
,然后獲取該類的unrecognizedSelector
方法的實現(xiàn)芬首,將該實現(xiàn)赴捞,綁定給該類的名稱為aSelector
的方法,然后將該對象返回郁稍,這樣我們就可以再unrecognizedSelector
統(tǒng)一處理該種錯誤赦政;
這種方式是最多的實現(xiàn)方式,因為既不污染對象對應(yīng)的類,又比下一步處理的時候消耗要小恢着,但是在這種方式中存在一個問題桐愉,在經(jīng)過測試發(fā)現(xiàn),在調(diào)用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法過程中掰派,某些類遵循了一些協(xié)議从诲,但是沒有實現(xiàn)協(xié)議方法的時候,該方法也會返回為方法簽名靡羡,這樣就會跳過將未實現(xiàn)方法轉(zhuǎn)嫁給另外一個類的步驟系洛,就不能實現(xiàn)保護(hù)功能;也就是說:某些類遵循了一些協(xié)議略步,但是沒有實現(xiàn)協(xié)議方法的時候描扯,在此步的解決方案不能生效
3.- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
和
- (void)forwardInvocation:(NSInvocation *)anInvocation
:在此步中,有三種情況需要處理趟薄,詳細(xì)如下:
- (NSMethodSignature *)safeMethodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [self safeMethodSignatureForSelector:aSelector];
if (methodSignature) return methodSignature;
IMP originIMP = class_getMethodImplementation([NSObject class], @selector(methodSignatureForSelector:));
IMP currentClassIMP = class_getMethodImplementation([self class], @selector(methodSignatureForSelector:));
// 如果子類重載了該方法绽诚,則返回nil
if (originIMP != currentClassIMP) return nil;
// - (void)xxxx
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)safeForwardInvocation:(NSInvocation *)invocation
{
NSString *reason = [NSString stringWithFormat:@"class:[%@] not found selector:(%@)",NSStringFromClass(self.class),NSStringFromSelector(invocation.selector)];
NSException *exception = [NSException exceptionWithName:@"Unrecognized Selector"
reason:reason
userInfo:nil];
// 收集錯誤信息
hy_handleErrorWithException(exception);
}
在以上代碼中,- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
處理了三種情況:
1.如果有方法簽名竟趾,則正常流程憔购;
2.如果沒有方法簽名,則返回一個默認(rèn)的方法簽名岔帽,然后在- (void)forwardInvocation:(NSInvocation *)anInvocation
中處理玫鸟;
3.如果子類重載了該方法,則返回nil犀勒,具體處理交給子類屎飘;
綜上所述,unrecognized selector
引起的崩潰贾费,在第三步中處理為最佳實踐方案钦购,此方案雖然相對比第二步中處理會效率略低,但可以解決第二步中解決不了的問題褂萧;
容器類數(shù)據(jù)類型操作引起的崩潰
數(shù)組押桃、字典、集合等是我們開發(fā)中經(jīng)常使用的數(shù)據(jù)類型导犹,在使用中經(jīng)常會出現(xiàn)數(shù)組越界唱凯、字典插入空值、集合越界等錯誤引起的崩潰谎痢;在開發(fā)中這種崩潰可以及時提醒我們改正錯誤磕昼,但是如果在線上也因為這種錯誤引起崩潰,對用戶來說节猿,體驗是很不友好的票从;
對于這種崩潰的保護(hù),采用的方法是:對容易出現(xiàn)異常的方法進(jìn)行HOOK,然后再自定義實現(xiàn)中峰鄙,對異常進(jìn)行處理浸间,并對異常進(jìn)行收集、上報吟榴;
對這三種數(shù)據(jù)類型发框,目前對常用的方法進(jìn)行了HOOK,做了異常保護(hù)煤墙,后續(xù)可以對所有有可能發(fā)生異常的方法進(jìn)行HOOK;
數(shù)組:
+ (instancetype)arrayWithObject:(id)anObject;
- (id)objectAtIndex:(NSUInteger)index;
- (id)objectAtIndexedSubscript:(NSInteger)index;
- (NSArray *)subarrayWithRange:(NSRange)range;
+ (instancetype)arrayWithObjects:(const id [])objects count:(NSUInteger)cnt;
- (void)addObject:(id)anObject;
- (id)objectAtIndex:(NSUInteger)index;
- (id)objectAtIndexedSubscript:(NSInteger)index;
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
- (void)removeObjectAtIndex:(NSUInteger)index;
- (void)replaceObjectAtIndex:(NSUInteger)index withObject:(id)anObject;
- (void)removeObjectsInRange:(NSRange)range;
- (NSArray *)subarrayWithRange:(NSRange)range;
字典:
+ (instancetype)dictionaryWithObject:(ObjectType)object forKey:(KeyType <NSCopying>)key;
+ (instancetype)dictionaryWithObjects:(const ObjectType _Nonnull [_Nullable])objects forKeys:(const KeyType <NSCopying> _Nonnull [_Nullable])keys count:(NSUInteger)cnt;
- (instancetype)initWithObjectsAndKeys:(id)firstObject, ... ;
- (instancetype)initWithObjects:(NSArray<ObjectType> *)objects forKeys:(NSArray<KeyType <NSCopying>> *)keys;
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
- (void)setObject:(nullable ObjectType)obj forKeyedSubscript:(KeyType <NSCopying>)key ;
- (void)removeObjectForKey:(KeyType)aKey;
集合:
+ (instancetype)setWithObject:(ObjectType)object;
- (void)addObject:(ObjectType)object;
- (void)removeObject:(ObjectType)object;
字符串操作引起的崩潰
字符串是我們開發(fā)中使用場景最多的數(shù)據(jù)類型宪拥,對字符串進(jìn)行操作也是很容易引起崩潰仿野;例如:字符串截取、字符串拼接她君、刪除指定范圍內(nèi)子串脚作、判斷是否包含子串等,如果開發(fā)中不注意 很容易引起程序崩潰缔刹;
對于這種崩潰的保護(hù)球涛,我們采用和容器類數(shù)據(jù)類型相似的方法:對容易出現(xiàn)異常的方法進(jìn)行HOOK,然后再自定義實現(xiàn)中校镐,對異常進(jìn)行處理亿扁,并對異常進(jìn)行收集、上報鸟廓;
KVO
引起的崩潰
KVO:即Key-Value Observing从祝,它提供一種機制,當(dāng)指定的對象的屬性被修改后引谜,則對象的監(jiān)聽者就會接受收到通知牍陌。簡單的說就是每次指定的被觀察的對象的屬性被修改后,KVO就會自動通知相應(yīng)的觀察者了员咽。
KVO機制在iOS的很多開發(fā)場景中都會被使用到毒涧。不過如果一不小心使用不當(dāng)?shù)脑挘瑫?dǎo)致Crash問題贝室。KVO引起的Crash主要包含以下兩個方面:
- KVO的被觀察者dealloc時仍然注冊著KVO導(dǎo)致的Crash契讲;
- KVO重復(fù)添加觀察者或重復(fù)移除;
針對以上問題档玻,采用以下解決方案:
由于絕大部分的問題都是因為KVO監(jiān)聽對象屬性過多造成的混亂怀泊,導(dǎo)致在開發(fā)過程中不能很好的手動管理,那么就可以給被觀察對象綁定一個Map误趴,這個Map的作用是存儲管理該對象被觀察的屬性霹琼,用它來維護(hù)對象的被觀察屬性的移除和添加;這樣做的好處有以下兩點:
- 如果是非正常的添加或者刪除觀察者,就可以通過Map的存儲判斷出異常枣申,從而避免這種操作售葡;
- 被觀察者在dealloc之前都會銷毀關(guān)聯(lián)的對象,這時該Map也被自動銷毀(系統(tǒng)特性忠藤,對象銷毀的時候挟伙,會檢查和該對象關(guān)聯(lián)的對象,然后銷毀)模孩,避免對象銷毀時尖阔,還注冊者觀察者;
NSTimer
引起的崩潰
開發(fā)時榨咐,我們不免使用定時器介却,在使用以下方法時:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
定時器會對target
施加一個強引用,如果不在適當(dāng)時機對timer進(jìn)行invalidate块茁,則會出現(xiàn)內(nèi)存泄露齿坷;假如當(dāng)對象銷毀之后,還沒有對定時器進(jìn)行invalidate数焊,則在某種情況下永淌,也會引起崩潰;具體情況和selector
內(nèi)部實現(xiàn)有關(guān)佩耳;
在這里遂蛀,對NSTimer
的處理為,引入中間代理對象TimerTagetAgent
蚕愤,解除timer
和target
之間的循環(huán)引用答恶;結(jié)構(gòu)如下:
這樣處理之后,解除掉了timer
對target
的強引用萍诱,并且可以在TimerTagetAgent
中對timer
進(jìn)行適時的invalidate掉悬嗓,這樣就解決了內(nèi)存泄露和不確定性閃退問題,并且可以上報錯誤裕坊,督促開發(fā)人員改正包竹;
非主線程刷新UI
在非主線程刷新UI操作會導(dǎo)致界面不能按照想要的結(jié)果展示,而且很有可能造成崩潰籍凝;在這里處理方法為:HOOK以下下三個系統(tǒng)方法周瞎,在debug
和release
模式下做不同操作;
- (void)setNeedsLayout;
- (void)setNeedsDisplay;
- (void)setNeedsDisplayInRect:(CGRect)rect;
debug
模式下:使用斷言機制饵蒂,是程序進(jìn)行崩潰声诸,并輸出錯誤信息,促使開發(fā)人員修改問題退盯;
release
模式下:異步到主線程刷新UI
這里和之前類型的崩潰采用的方法不同彼乌,是在開發(fā)環(huán)境下是程序主動崩潰泻肯,促使開發(fā)人員解決問題,在生產(chǎn)環(huán)境下采用崩潰保護(hù)慰照,但是沒有上報異常灶挟,原因為:這三個方法刷新UI的操作時在RunLoop的每個時鐘周期都會操作,如果上報異常毒租,服務(wù)器將會收到大量不必要信息稚铣;
野指針
NSNotification
引起的崩潰
對于iOS 9以下需要做操作,但由于9以下系統(tǒng)較少墅垮,此模塊后續(xù)完善惕医;