作者:lijie250
連結:https://github.com/jezzmemo/JJException
Unrecognized Selector Sent to Instance
由於Objective-c是Message機制,而且物件在轉換的時候,會有拿到的物件和預期不一致,所以會有方法找不到的情況,在找不到方法時,查詢方法將會進入方法Forward流程,系統給了三次補救的機會,所以我們要解決這個問題,在這三次均可以解決這個問題。
-
resolveInstanceMethod:(SEL)sel 這是實體化方法沒有找到方法,最先執行的函式,首先會流轉到這裡來,傳回值是BOOL,沒有找到就是NO,找到就傳回YES,如果要解決就需要再當前的實體中加入不存在的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沒有處理,將進行到forwardingTargetForSelector這步來,這時候你可以傳回nil,你也可以用一個Stub物件來接住,把訊息流程流轉到了你的Stub那邊了,然後在你的Stub裡新增不存在的Selector,這樣就不會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
這兩個方法一起說,因為他們之間有關聯,
1、當methodSignatureForSelector傳回nil時,會Crash
2、如果methodSignatureForSelector傳回一個定義好的NSMethodSignature,但是沒有實現forwardInvocation,也會閃退,如果實現了forwardInvocation,會先傳回到resolveInstanceMethod然後再才會到forwardInvocation
3、當流轉到forwardInvocation,透過以下方法:
[anInvocation invokeWithTarget:xxxtarget1];
[anInvocation invokeWithTarget:xxxtarget2];
還可以流轉到多個物件,[anInvocation invokeWithTarget:xxxtarget2]是為了讓不存在的方法有著陸點
-
doesNotRecognizeSelector:(SEL)aSelector 執行到這裡的時候,兩種情況:
1、當methodSignatureForSelector傳回一種任意的方法簽名的時候,也會進入doesNotRecognizeSelector,但是不會閃退
2、當methodSignatureForSelector傳回nil時,進入doesNotRecognizeSelector就會閃退
根據以上流程,最終還是選擇流程2,原因如下:
1、resolveInstanceMethod雖然可以解決問題,給不存在的方法增加到示例中去,會汙染當前示例
2、forwardInvocation在三步中式最後一步,會導致流轉的週期變長,而且會產生NSInvocation,效能不是最好的選擇
如何監聽實體化物件什麼時候釋放
先說下這個知識點,因為在接下來的好幾個地方都會用到,會有一些異常的情況,所以需要一種知道當前建立者啥時候釋放,首先會想到dealloc,這樣會Hook的NSObject,在一定程度會影響效能,後面發現一種比較優雅的方法,原理來自於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會釋放所有的用AssociatedObject資料。
objc_setAssociatedObject給當前物件新增一個中間物件,當前物件釋放時,會清理AssociatedObject資料,AssociatedObject的中間物件將被清理釋放,中間物件的dealloc方法將被執行。
最終清理被遺漏的監聽者,會用在KVO和NSNotification清理沒用的監聽者,不過這種方式有以下問題需要註意:
-
清理的時候執行緒安全問題
-
清理時機偏晚,是否適合你當前的情況
NSArray,NSMutableArray,NSDictonary,NSMutableDictionary
-
類族(Class Cluster)
NSDictonary,NSArray,NSString等,都使用了類族,這種樣式最大的好處就是,可以隱藏抽象基類背後的複雜細節,使用者只需呼叫基類簡單的方法就可以傳回不同的子類實體
-
Swizzle Hook
這裡就不贅述Swizzle概念了,Google到處都是講解的,這裡給一個典型的例子:
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,也參考了網易和美團的做法,主要是以下步驟:
1、Hook住dealloc方法
2、如果當前示例在黑名單裡,就把當年前示例加入集合,並把當前物件objc_destructInstance清理取用關係,並未真正釋放記憶體,並將object_setClass設定成自己的中間物件
3、Hook中間物件的方法,收到的訊息都由中間物件來處理
4、維護的野指標集合,要麼根據個數來維護,要麼根據總大小來維護,當滿了,就需要真正釋放物件記憶體free(obj)
存在的問題:
1、需要單獨的記憶體那些問題物件
2、最後釋放記憶體後,再訪問時會閃退,這個方法只是一定程度延遲了閃退時間
3、需要後臺維護黑名單機制,來指定那些問題物件
KVO
KVO在以下情況會導致閃退:
-
新增監聽後沒有清除會導致閃退
-
清除不存在的key也會閃退
-
新增重覆的key導致閃退
需要Hook以下方法:
-
addObserver:forKeyPath:options:context:
-
removeObserver:forKeyPath:
主要解決以下問題:
-
在註冊監聽後,中間物件需要維護註冊的資料集合,當宿主釋放時,清除還在集合中的監聽者
-
保護key不存在的情況
-
保護重覆新增的情況
NSTimer
NSTimer存在以下問題:
-
Target是強取用,記憶體洩漏
-
在宿主不存在的時候,清理NSTimer
Hook以下方法:
-
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats
解決方法:1.當repeats為NO時,走原始方法 2.當repeats為YES時,新建一個物件,宣告一個target屬性為weak型別,指向引數的target,當中間物件的target為空時,清理NSTimer
NSNotification
NSNotification的主要問題是:
-
新增通知後,沒有移除導致Crash的問題(不過在iOS9以後沒有這個問題,我在真機8.3測試也沒有這個問題,不知道iOS8是否有這個問題)
Hook以下方法:
-
addObserver:selector:name:object:
原因和解決辦法:問題就在在於和assign和weak問題,野指標問題,要麼置空指標或者刪除空指標的集合
MRC
這裡單獨說下,為什麼工程選擇了MRC,因為在Hook集合型別的時候,啟動的時候就閃退了,Crash的地方在系統類裡,Stack裡顯示在CF這層,這裡只能猜測系統底層對ARC的支援不好導致的,後續改成MRC就沒有問題,所以這個需要繼續研究和追蹤,如果有知道的同學記得告知我下.
效能
本來是沒有打算註意效能這個問題的,因為從Hook原理的角度來說,只是交換IMP的指向,時間複雜度來說,只是在系統級別上增加了幾條邏輯判斷指令,所以這個影響是極小的,基本可以忽略,我經過測試,迴圈1000000次,沒有HOOK和HOOK相差0.0x秒的,所以減少Crash,來增加這麼點時間複雜度來說,是值得的。
不過最後說一點,就是dealloc確實需要註意,因為這裡存在集合的操作,所以要註意時間複雜度,dealloc執行的很頻繁的,而且主執行緒和子執行緒都會涉及到,尤其是主執行緒一定註意,否則會影響到UI的體驗。
參考資料
https://github.com/opensource-apple/objc4/blob/master/runtime/objc-runtime-new.m
大白健康系統
最後給出開原始碼:https://github.com/jezzmemo/JJException
●編號299,輸入編號直達本文
●輸入m獲取文章目錄
Web開發
更多推薦《18個技術類微信公眾號》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。