保護(hù)App,一般常見(jiàn)的問(wèn)題不會(huì)導(dǎo)致閃退惋鸥,增強(qiáng)App的健壯性贺嫂,同時(shí)會(huì)將錯(cuò)誤拋出來(lái),根據(jù)每個(gè)App自身的日志渠道記錄捂龄,下次迭代修復(fù)那些問(wèn)題.
Unrecognized Selector Sent to Instance
NSArray,NSMutableArray,NSDictonary,NSMutableDictionary
KVO
Zombie Pointer
NSTimer
NSNotification
Unrecognized Selector Sent to Instance
由于Objective-c是Message機(jī)制释涛,而且對(duì)象在轉(zhuǎn)換的時(shí)候,會(huì)有拿到的對(duì)象和預(yù)期不一致倦沧,所以會(huì)有方法找不到的情況唇撬,在找不到方法時(shí),查找方法將會(huì)進(jìn)入方法Forward流程,系統(tǒng)給了三次補(bǔ)救的機(jī)會(huì)展融,所以我們要解決這個(gè)問(wèn)題窖认,在這三次均可以解決這個(gè)問(wèn)題
- resolveInstanceMethod:(SEL)sel
這是實(shí)例化方法沒(méi)有找到方法,最先執(zhí)行的函數(shù),首先會(huì)流轉(zhuǎn)到這里來(lái)扑浸,返回值是BOOL,沒(méi)有找到就是NO,找到就返回YES,如果要解決就需要再當(dāng)前的實(shí)例中加入不存在的Selector,并綁定IMP烧给,示例如下:
static void xxxInstanceName(id self, SEL cmd, id value) {
NSLog(@"resolveInstanceMethod %@", value);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
NSMethodSignature* sign = [self methodSignatureForSelector:selector];
if (!sign) {
class_addMethod([self class], sel, (IMP)xxxInstanceName, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- forwardingTargetForSelector:(SEL)aSelector
如果resolveInstanceMethod沒(méi)有處理,將進(jìn)行到forwardingTargetForSelector這步來(lái)喝噪,這時(shí)候你可以返回nil础嫡,你也可以用一個(gè)Stub對(duì)象來(lái)接住,把消息流程流轉(zhuǎn)到了你的Stub那邊了酝惧,然后在你的Stub里添加不存在的Selector榴鼎,這樣就不會(huì)crash了,示例如下:
- (id)forwardingTargetForSelectorSwizzled:(SEL)selector{
NSMethodSignature* sign = [self methodSignatureForSelector:selector];
if (!sign) {
id stub = [[UnrecognizedSelectorHandle new] autorelease];
class_addMethod([stub class], selector, (IMP)unrecognizedSelector, "v@:");
return stub;
}
return [self forwardingTargetForSelectorSwizzled:selector];
}
methodSignatureForSelector:(SEL)aSelector
forwardInvocation:(NSInvocation *)anInvocation
這兩個(gè)方法一起說(shuō)晚唇,因?yàn)樗麄冎g有關(guān)聯(lián)巫财,
- 當(dāng)methodSignatureForSelector返回nil時(shí),會(huì)Crash
- 如果methodSignatureForSelector返回一個(gè)定義好的NSMethodSignature哩陕,但是沒(méi)有實(shí)現(xiàn)forwardInvocation平项,也會(huì)閃退,如果實(shí)現(xiàn)了forwardInvocation悍及,會(huì)先返回到resolveInstanceMethod然后再才會(huì)到forwardInvocation
- 當(dāng)流轉(zhuǎn)到
forwardInvocation
,通過(guò)以下方法:
[anInvocation invokeWithTarget:xxxtarget1];
[anInvocation invokeWithTarget:xxxtarget2];
還可以流轉(zhuǎn)到多個(gè)對(duì)象,[anInvocation invokeWithTarget:xxxtarget2]是為了讓不存在的方法有著陸點(diǎn)
- doesNotRecognizeSelector:(SEL)aSelector
執(zhí)行到這里的時(shí)候闽瓢,兩種情況:
- 當(dāng)methodSignatureForSelector返回一種任意的方法簽名的時(shí)候,也會(huì)進(jìn)入doesNotRecognizeSelector并鸵,但是不會(huì)閃退
- 當(dāng)methodSignatureForSelector返回nil時(shí)鸳粉,進(jìn)入doesNotRecognizeSelector就會(huì)閃退
根據(jù)以上流程,最終還是選擇流程2,原因如下:
- resolveInstanceMethod雖然可以解決問(wèn)題园担,給不存在的方法增加到示例中去届谈,會(huì)污染當(dāng)前示例
- forwardInvocation在三步中式最后一步,會(huì)導(dǎo)致流轉(zhuǎn)的周期變長(zhǎng)弯汰,而且會(huì)產(chǎn)生NSInvocation,性能不是最好的選擇
NSArray,NSMutableArray,NSDictonary,NSMutableDictionary
- 類族(Class Cluster)
NSDictonary艰山,NSArray,NSString等,都使用了類族咏闪,這種模式最大的好處就是曙搬,可以隱藏抽象基類背后的復(fù)雜細(xì)節(jié),使用者只需調(diào)用基類簡(jiǎn)單的方法就可以返回不同的子類實(shí)例
- Swizzle Hook
這里就不贅述Swizzle概念了鸽嫂,Google到處都是講解的纵装,這里給一個(gè)典型的例子:
swizzleInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:), @selector(hookObjectAtIndex:));
- (id) hookObjectAtIndex:(NSUInteger)index {
if (index < self.count) {
return [self hookObjectAtIndex:index];
}
handleCrashException(@"HookObjectAtIndex invalid index");
return nil;
}
Zombie Pointer
讓野指針不閃退是模仿了XCode debug的Zombie Object,也參考了網(wǎng)易和美團(tuán)的做法,主要是以下步驟:
- Hook住dealloc方法
- 如果當(dāng)前示例在黑名單里据某,就把當(dāng)年前示例加入集合橡娄,并把當(dāng)前對(duì)象
objc_destructInstance
清理引用關(guān)系,并未真正釋放內(nèi)存癣籽,并將object_setClass
設(shè)置成自己的中間對(duì)象 - Hook中間對(duì)象的方法挽唉,收到的消息都由中間對(duì)象來(lái)處理
- 維護(hù)的野指針集合滤祖,要么根據(jù)個(gè)數(shù)來(lái)維護(hù),要么根據(jù)總大小來(lái)維護(hù)瓶籽,當(dāng)滿了匠童,就需要真正釋放對(duì)象內(nèi)存
free(obj)
存在的問(wèn)題:
- 需要單獨(dú)的內(nèi)存那些問(wèn)題對(duì)象
- 最后釋放內(nèi)存后,再訪問(wèn)時(shí)會(huì)閃退塑顺,這個(gè)方法只是一定程度延遲了閃退時(shí)間
- 需要后臺(tái)維護(hù)黑名單機(jī)制汤求,來(lái)指定那些問(wèn)題對(duì)象
KVO,NSTimer严拒,NSNotification
這三種放在一起首昔,是因?yàn)樗麄冎g有共同的特征,就是創(chuàng)建后糙俗,忘記銷毀會(huì)導(dǎo)致閃退,或者會(huì)有一些異常的情況预鬓,所以需要一種知道當(dāng)前創(chuàng)建者啥時(shí)候釋放巧骚,首先會(huì)想到dealloc,這樣會(huì)Hook的NSObject,在一定程度會(huì)影響性能,后面發(fā)現(xiàn)一種比較優(yōu)雅的方法,原理來(lái)自于Runtime源碼:
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARR ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
* Be warned that GC DOES NOT CALL THIS. If you edit this, also edit finalize.
* CoreFoundation and other clients do call this under GC.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}
return obj;
}
_object_remove_assocations
會(huì)釋放所有的用AssociatedObject格二,所以我們Hook以下方法劈彪,只是列舉有代表性的,根據(jù)自身情況補(bǔ)齊添加的地方
KVO(addObserver:forKeyPath)
NSNotification(addObserver:selector)
objc_setAssociatedObject
給當(dāng)前對(duì)象添加一個(gè)中間對(duì)象顶猜,當(dāng)前對(duì)象釋放時(shí)沧奴,會(huì)清理AssociatedObject數(shù)據(jù),AssociatedObject的中間對(duì)象將被清理釋放长窄,中間對(duì)象的dealloc方法將被執(zhí)行滔吠,最終清理被遺漏的監(jiān)聽(tīng)者。
- NSTimer(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats)
NSTimer的問(wèn)題在于挠日,target默認(rèn)是強(qiáng)引用疮绷,如果用戶不手動(dòng)關(guān)閉NSTimer和置空,會(huì)存在內(nèi)存泄漏和異常情況嚣潜,所以用中間層來(lái)持有冬骚,用KVO和NSNotification的方法來(lái)清理
MRC
這里單獨(dú)說(shuō)下,為什么工程選擇了MRC懂算,因?yàn)樵贖ook集合類型的時(shí)候只冻,啟動(dòng)的時(shí)候就閃退了,Crash的地方在系統(tǒng)類里计技,Stack里顯示在CF這層喜德,這里只能猜測(cè)系統(tǒng)底層對(duì)ARC的支持不好導(dǎo)致的,后續(xù)改成MRC就沒(méi)有問(wèn)題酸役,所以這個(gè)需要繼續(xù)研究和追蹤住诸,如果有知道的同學(xué)記得告知我下
性能
本來(lái)是沒(méi)有打算注意性能這個(gè)問(wèn)題的驾胆,因?yàn)閺腍ook原理的角度來(lái)說(shuō),只是交換IMP的指向贱呐,時(shí)間復(fù)雜度來(lái)說(shuō)丧诺,只是在系統(tǒng)級(jí)別上增加了幾條邏輯判斷指令,所以這個(gè)影響是極小的奄薇,基本可以忽略驳阎,我經(jīng)過(guò)測(cè)試,循環(huán)1000000次馁蒂,沒(méi)有HOOK和HOOK相差0.0x秒的呵晚,所以減少Crash,來(lái)增加這么點(diǎn)時(shí)間復(fù)雜度來(lái)說(shuō)沫屡,是值得的饵隙。
不過(guò)最后說(shuō)一點(diǎn),就是dealloc確實(shí)需要注意沮脖,因?yàn)檫@里存在集合的操作金矛,所以要注意時(shí)間復(fù)雜度,dealloc執(zhí)行的很頻繁的勺届,而且主線程和子線程都會(huì)涉及到驶俊,尤其是主線程一定注意,否則會(huì)影響到UI的體驗(yàn)免姿。
https://github.com/jezzmemo/JJException
參考資料
https://github.com/opensource-apple/objc4/blob/master/runtime/objc-runtime-new.mm