上篇文章 介紹了一些runtime的基礎(chǔ)知識(shí), 這次分享一些runtime的各種黑科技玩法: 消息轉(zhuǎn)發(fā)截獲, isa-swizzling, method swizzling, associated object等等. 順便研究了野指針的問(wèn)題, 以及如何寫一個(gè)僵尸對(duì)象(Zombie).
Unrecognized Selector
消息轉(zhuǎn)發(fā)截獲
這個(gè)簡(jiǎn)單了, 首先來(lái)張圖:
當(dāng)向?qū)ο蟀l(fā)送消息, 沿著類的繼承鏈找不到響應(yīng)的方法時(shí), runtime的消息轉(zhuǎn)發(fā)機(jī)制會(huì)依次調(diào)用這幾個(gè)方法. 這里選擇第二個(gè)
forwardingTargetForSelector
來(lái)操作. 該方法返回一個(gè)對(duì)象, 該對(duì)象為消息新的接受者.
這里我們選擇了第二步
forwardingTargetForSelector
來(lái)做文章。原因如下:
-
resolveInstanceMethod
需要在類的本身上動(dòng)態(tài)添加它本身不存在的方法稳诚,這些方法對(duì)于該類本身來(lái)說(shuō)冗余的 -
forwardInvocation
可以通過(guò)NSInvocation的形式將消息轉(zhuǎn)發(fā)給多個(gè)對(duì)象舟陆,但是其開(kāi)銷較大理肺,需要?jiǎng)?chuàng)建新的NSInvocation對(duì)象胁后,并且- forwardInvocation
的函數(shù)經(jīng)常被使用者調(diào)用炉菲,來(lái)做多層消息轉(zhuǎn)發(fā)選擇機(jī)制疲陕,不適合多次重寫 -
forwardingTargetForSelecto
r可以將消息轉(zhuǎn)發(fā)給一個(gè)對(duì)象鞠值,開(kāi)銷較小,并且被重寫的概率較低竞膳,適合重寫
好了. 代碼:
id forwardingTargetForSelector(id self, SEL _cmd, SEL aSelector) {
NSLog(@"Unrecognized selector %@ sent to %@, ***forwarding to Stub", NSStringFromSelector(aSelector), self);
StubProxy *stub = [[StubProxy alloc] init];
if (![stub respondsToSelector:aSelector]) {
class_addMethod([stub class], aSelector, (IMP)someMethodIMP, "v@:");
}
return stub;
}
void someMethodIMP(id self, SEL _cmd) {
NSLog(@"*** someMethodIMP prevent the crash. *** ");
}
這里方法寫成了C語(yǔ)言的格式, 其實(shí)是一樣的. 所有實(shí)例方法都隱含了self
和_cmd
參數(shù), 最終OC形式的方法也會(huì)轉(zhuǎn)化成類似形式. 如寫成OC形式的方法, 可以調(diào)用runtime的class_getInstanceMethod
和method_getImplementation
轉(zhuǎn)化為IMP
供class_addMethod
使用. 一樣道理~
StubProxy
是一個(gè)樁類, 可認(rèn)為它僅是一個(gè)空的模板, 也可以不在代碼中定義類, 直接使用runtime的objc_allocateClassPair
和objc_registerClassPair
函數(shù)去動(dòng)態(tài)創(chuàng)建并注冊(cè)類, 只需把
StubProxy *stub = [[StubProxy alloc] init];
換成
Class StubProxy = objc_allocateClassPair([NSObject class], "StubProxy", 0);
objc_registerClassPair(StubProxy);
class_addMethod(StubProxy, aSelector, (IMP)someMethodIMP, "v@:");
id stub = [[StubProxy alloc] init];
然后再APP開(kāi)始運(yùn)行的地方(如AppDeleage的didFinishLaunchingWithOptions
回調(diào))加上代碼:
//get target class
id targetClass = objc_getClass("MyViewController");
//override the forwardingTargetForSelector method of NSObject
class_addMethod([targetClass class], @selector(forwardingTargetForSelector:), (IMP)forwardingTargetForSelector, "@@:@");
這里"MyViewController"是一個(gè)你需要加上unrecognized selector 崩潰防護(hù)的類, 這里使用class_addMethod
函數(shù)動(dòng)態(tài)為該類添加上forwardingTargetForSelector
方法, 該方法把無(wú)法識(shí)別的selector消息轉(zhuǎn)發(fā)至一個(gè)Stub類的對(duì)象, 該對(duì)象為這個(gè)selector動(dòng)態(tài)添加一個(gè)函數(shù)實(shí)現(xiàn), 這個(gè)函數(shù)怎么實(shí)現(xiàn)就自定義了, 可以為空, 返回0, 或者打印個(gè)日志, 隨你所好. 該函數(shù)對(duì)應(yīng)Demo里的void someMethodIMP()
.
由此, 當(dāng)出現(xiàn)Unrecognized selector時(shí), 原本的Crash
CrashCrusher[65488:28134311] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason:-[MyViewController someMethod]: unrecognized selector sent to instance 0x7fd81a50db20'
...(call stack)
libc++abi.dylib: terminating with uncaught exception of type NSException
將變成了友好的
CrashCrusher[65353:28112308] Unrecognized selector someMethod sent to <MyViewController: 0x7fd4ece0f390>, ***forwarding to Stub
CrashCrusher[65353:28112308] *** someMethodIMP prevent the crash. ***
順著這種思路, 可以封裝一下API, 接受一個(gè)NSString類型的類名, 對(duì)相應(yīng)的類進(jìn)行unrecognized selector的crash防護(hù). 這里僅作簡(jiǎn)單的Demo, 就不封裝了.
Zombie
當(dāng)遇到野指針訪問(wèn)不恰當(dāng)內(nèi)存時(shí)航瞭,系統(tǒng)發(fā)送SIGSEGV
信號(hào),出現(xiàn)EXC_BAD_ACCESS錯(cuò)誤而崩潰坦辟。
Xcode在Debug模式下可開(kāi)啟NSZombieEnabled刊侯,當(dāng)對(duì)象被釋放時(shí),runtime系統(tǒng)通過(guò)isa-swizzling把該對(duì)象替換成一個(gè)Zombie對(duì)象锉走,當(dāng)往該對(duì)象發(fā)送消息時(shí)滨彻,Zombie對(duì)象將輸出一個(gè)message sent to deallocated instance
的log,隨后發(fā)送SIGKILL
信號(hào)終止程序挪蹭。Log(Message from debugger: Terminated due to signal 9)
亭饵。
可以看出在開(kāi)啟Zombie情況下,比起令人頭大的EXC_BAD_ACCESS
野指針崩潰梁厉,Zombie給開(kāi)發(fā)者提供了更友好的“崩潰”方式辜羊,并且提供相關(guān)日志來(lái)追溯bug。
由于僵尸對(duì)象的存在導(dǎo)致內(nèi)存的過(guò)度消耗的問(wèn)題词顾,蘋果并不在Release模式下提供該功能八秃。
這并不能阻止我們自己去實(shí)現(xiàn)一個(gè)Zombie啊~lol
下面利用runtime寫一個(gè)自定義的zombie對(duì)象
首先,
isa-swizzling
什么是isa-swizzling? 先看下
typedef struct objc_object {
Class isa;
} *id;
每個(gè)OC對(duì)象結(jié)構(gòu)里的第一項(xiàng), 就是一個(gè)名為isa的Class類型變量, Class
為類對(duì)象結(jié)構(gòu)體的指針類型.
typedef struct objc_class {
Class isa ;
Class super_class ;
const char *name ;
long version ;
long info ;
long instance_size ;
struct objc_ivar_list *ivars ;
struct objc_method_list **methodLists ;
struct objc_cache *cache ;
struct objc_protocol_list *protocols ;
} *Class;
對(duì)象的isa指針指向它的類對(duì)象.
從代碼的定義可以看出, Class
類型也是id
類型的一個(gè)特例. (認(rèn)識(shí)到這點(diǎn)很重要, 不要理所當(dāng)然得認(rèn)為Class就只是類類型, id就只是對(duì)象類型)
從Class
類型強(qiáng)制轉(zhuǎn)換為id
類型將損失"精度"(或者說(shuō),可見(jiàn)度? 明白我意思就行??).
id
類型里, 僅對(duì)變量isa
可見(jiàn).
所謂isa-swizzling, 就是把一個(gè)對(duì)象的isa改為指向另外一個(gè)類!
可供操作的runtime方法是:
Class object_setClass(id obj, Class cls);
obj
為被swizzled的對(duì)象, cls
為新的isa值.
method swizzling
我們要在對(duì)象被回收時(shí)把它置換成另一個(gè)對(duì)象,想到了method swizzling掉NSObject的dealloc
方法肉盹。
關(guān)于dealloc
當(dāng)對(duì)象的引用計(jì)數(shù)降為0時(shí), 系統(tǒng)向被釋放的對(duì)象發(fā)送-dealloc
消息.
dealloc
方法做了三件事:
- 調(diào)用
objc_destructInstance()
釋放對(duì)象的所有實(shí)例變量和關(guān)聯(lián)對(duì)象(該方法并未回收對(duì)象本身內(nèi)存).
- 調(diào)用
- isa-swizzling將該對(duì)象的類置為一個(gè)空的類對(duì)象.
- 調(diào)用
free()
回收該對(duì)象的內(nèi)存.
- 調(diào)用
它的最終代碼是這樣的
static id _object_dispose(id anObject)
{
if (anObject==nil) return nil;
objc_destructInstance(anObject);
anObject->initIsa(_objc_getFreedObjectClass ());
free(anObject);
return nil;
}
關(guān)于dealloc
更詳細(xì)的分析可看大神的這篇文章.
我們的目的是把原對(duì)象isa-swizzle成一個(gè)Zombie對(duì)象, 這個(gè)Zombie仍保留于內(nèi)存中, 以監(jiān)測(cè)野指針. 所以用來(lái)swizzle的dealloc方法是這樣的:
- (void)my_dealloc {
//after method swizzling, the `self` here refers to the Object to be dealloc-ed but not the CCZombie instance itself
//if the class of object-to-be-dealloced is not enabled to be a zombie, call the original dealloc
CCZombie *zombie = [CCZombie sharedZombie];
if (![zombie->_classesThatEnablesZombie containsObject:[self class]]) {
return [self my_dealloc];
}
//release all instance variables and associated objects the object references
objc_destructInstance(self);
//store the isa's original name
NSString *originClassName = NSStringFromClass([self class]);
objc_setAssociatedObject(self, OrigClassNameKey, originClassName, OBJC_ASSOCIATION_COPY_NONATOMIC);
//isa-swizzling
Class zombieClass = objc_getClass("CCZombie");
object_setClass(self, zombieClass);
//TODO: set a more customized class name, like CCZombie_<OrigClassName>?
//TODO: implement a cache mechanism for the zombies
//no free() called here
}
思路:
- 一開(kāi)始先判斷對(duì)象是否加入了Zombie防護(hù)機(jī)制, 如果未加入, 則調(diào)用原始的
dealloc
方法. 如果是, 下一步; - 調(diào)用
objc_destructInstance
析構(gòu)對(duì)象; - 使用associated object函數(shù)把原類名存儲(chǔ)于對(duì)象中;
- isa-swizzling把對(duì)象類設(shè)置為Zombie
可見(jiàn), 除了存儲(chǔ)類名以便后來(lái)的識(shí)別外, 這個(gè)自定義的dealloc
方法與原來(lái)的dealloc
不同之處則在于少了free()
回收內(nèi)存的一步. (畢竟只是想把被釋放的對(duì)象變成僵尸嘛)
注意這里被isa-swizzled的dealloc是[NSObject dealloc]
, 因?yàn)槿魏蔚?code>dealloc調(diào)用最終都會(huì)調(diào)用到根類(即NSObject
)的dealloc
.
回想在MRC情況下, 所有重寫的dealloc最終都得寫上
[super dealloc];
而ARC下, 編譯器自動(dòng)插入了這一步.
回到method swizzling來(lái).
可定義一個(gè)開(kāi)啟zombie的方法:
- (void)enableZombie {
if (!_isZombieEnabled) {
//add the swizzled method to NSObject before swizzling, since CCZombie is not a category of NSObject
Method myDeallocMethod = class_getInstanceMethod([self class], @selector(my_dealloc));
BOOL result = class_addMethod([NSObject class], @selector(my_dealloc), method_getImplementation(myDeallocMethod), method_getTypeEncoding(myDeallocMethod));
if (result) {
//method swizzling in NSObject
Method myDeallocMethod = class_getInstanceMethod([NSObject class], @selector(my_dealloc));
Method origDeallocMethod = class_getInstanceMethod([NSObject class], @selector(dealloc));
method_exchangeImplementations(origDeallocMethod, myDeallocMethod);
}
}
}
注意: 由于這里不是在method swizzling的常見(jiàn)場(chǎng)景Category中, 所以需要一開(kāi)始先把my_dealloc
方法加入到NSObject
類里, 然后再進(jìn)行swizzling. 否則, 被釋放的對(duì)象將會(huì)由于找不到my_dealloc
的SEL而報(bào)錯(cuò).
再調(diào)用- enableZombie
方法后開(kāi)啟Zombie機(jī)制后, 所有對(duì)象的dealloc
方法都會(huì)最終跳到這個(gè)my_dealloc
中來(lái); 在my_dealloc
中在判斷對(duì)象是走原dealloc
還是被置換后的dealloc
; 被置換的dealloc最終不會(huì)調(diào)用free()
釋放內(nèi)存; 由此實(shí)現(xiàn)Zombie.
CCZombie
CCZombie是一個(gè)自定義的僵尸類, 可設(shè)置一些開(kāi)啟僵死服務(wù)的接口:
@interface CCZombie : NSObject
+ (void)enableZombie;
+ (void)addClassToZombieService:(NSString *)className;
@end
static void* OrigClassNameKey = "OrigClassNameKey";
@implementation CCZombie {
BOOL _isZombieEnabled;
NSMutableArray<Class> *_classesThatEnablesZombie;
}
+ (instancetype)sharedZombie {
static CCZombie* sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (instancetype)init {
if (self = [super init]) {
_isZombieEnabled = NO;
_classesThatEnablesZombie = [[NSMutableArray alloc] init];
return self;
}
return nil;
}
+ (void)enableZombie {
[[self sharedZombie] enableZombie];
}
+ (void)addClassToZombieService:(NSString *)className {
Class cls = objc_getClass([className UTF8String]);
CCZombie *zombie = [self sharedZombie];
[zombie->_classesThatEnablesZombie addObject:cls];
}
...
@end
剛才的- enableZombie
和my_dealloc
方法也定義在該類中.
這樣對(duì)某個(gè)類開(kāi)啟zombie就變得很簡(jiǎn)便了, 例如:
[CCZombie enableZombie];
[CCZombie addClassToZombieService:@"Son"];
[CCZombie addClassToZombieService:@"UIView"];
這些代碼可寫在App啟動(dòng)時(shí), 如AppDelegate的didFinishLaunching
回調(diào)里.
向野指針發(fā)送消息示例
在VC里定義一個(gè)點(diǎn)擊事件:
- (IBAction)onBtnTestWildPointer:(id)sender {
Son *__strong strongSon = [[Son alloc] init];
Son *__unsafe_unretained son = strongSon;
NSLog(@"release %@", son);
strongSon = nil;
[son performSelector:@selector(isMarried)];
[son performSelector:@selector(someMethodThatExist)];
[son performSelector:@selector(someMethodThatDoesNotExist)];
UIView *__unsafe_unretained view = [[UIView alloc] init];
[view setNeedsDisplay];
}
在strongSon = nil;
后, Son
對(duì)象被釋放, 調(diào)用被swizzled的dealloc
方法, son被isa-swizzle成僵尸對(duì)象. 同理View
對(duì)象; 向其發(fā)送的所有消息, 都將發(fā)送CCZombie對(duì)象中.
因此, 在CCZombie類中又重寫forwardingTargetForSelector
方法, 截獲該消息, 并轉(zhuǎn)發(fā)給一個(gè)樁類:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"[%@ %@] message sent to deallocated instance %@", objc_getAssociatedObject(self, OrigClassNameKey), NSStringFromSelector(aSelector), self);
StubProxy *stub = [[[StubProxy alloc] init] autorelease];
if (![stub respondsToSelector:aSelector]) {
Method method = class_getInstanceMethod([stub class], sel_registerName("someMethodUsedToPreventCrash"));
class_addMethod([stub class], aSelector, method_getImplementation(method), method_getTypeEncoding(method));
}
return stub;
}
跟上面unrecognized selector處理是一樣道理.
例如向一個(gè)被釋放的UIView
對(duì)象發(fā)送setNeedsDisplay
方法, 由原來(lái)的EXC_BAD_ACCESS
Crash變成了友好的提示:
CrashCrusher[67769:28599054] [UIView setNeedsDisplay] message sent to deallocated instance <CCZombie: 0x7fbce7c083a0>
CrashCrusher[67769:28599054] *** <StubProxy: 0x60800000ef40> prevent the crash. ***
這就達(dá)到了野指針?lè)雷o(hù)的目的.
kvo
另外一中特殊的野指針情況, KVO.
如果observer先于被觀察對(duì)象釋放了的時(shí), 被觀察對(duì)象對(duì)Observer的不安全弱引用變成了野指針. 是的昔驱,EXC_BAD_ACCESS如果被發(fā)送了KVO消息.
這種情況也可用到剛才的Zombie機(jī)制來(lái)防護(hù).
[CCZombie addClassToZombieService:@"Observer"];
例如在VC里定義一個(gè)屬性, 并KVO它
- (void)viewDidLoad {
[super viewDidLoad];
//...
Observer *observer = [[Observer alloc] init];
self.someProperty = @"orignal value";
[self addObserver:observer forKeyPath:@"someProperty" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
-viewDidLoad
方法結(jié)束后, observer對(duì)象被釋放, 變成CCZombie對(duì)象,
這時(shí)如果發(fā)生一個(gè)點(diǎn)擊事件觸發(fā)了KVO
- (IBAction)triggerKVO:(id)sender {
self.someProperty = @"new value";
}
這時(shí), 一個(gè)kvo消息observeValueForKeyPath:ofObject:change:context:
將發(fā)送至CCZombie對(duì)象. 然后
CrashCrusher[68012:28642070] name:NSInternalInconsistencyException,
reson:<CCZombie: 0x60000001d6d0>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
居然 SIGABRT
Crash掉了!
這是為何, 為何該消息不像[UIView setNeedsDisplay]
之類的消息一樣被Zombie轉(zhuǎn)發(fā)并處理掉了呢?
原因很簡(jiǎn)單:
因?yàn)檫@里定義的CCZombie類是繼承于NSObject類的. 它自然也是擁有了observeValueForKeyPath:ofObject:change:context:
等NSObject的方法. 所以不會(huì)進(jìn)入到消息轉(zhuǎn)發(fā)流程.
所以, 只需要在Zombie里重寫該方法就搞定了:
// CCZombie.m
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"KVO message sent to deallocated instance %@(%@)", objc_getAssociatedObject(self, OrigClassNameKey), self);
NSLog(@"Observe keypath [%@] change in %@, old:%@, new:%@", keyPath, object, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}
這樣, 當(dāng)向已被釋放的觀察者發(fā)送KVO時(shí), 也會(huì)給出相當(dāng)?shù)?友情提示"了.
CrashCrusher[68119:28657439] KVO message sent to deallocated instance Observer(<CCZombie: 0x600000013260>)
CrashCrusher[68119:28657439] Observe keypath [someProperty] change in <ViewController: 0x7ff5fec0e250>, old:orignal value, new:new value
Demo
待上傳
寫在最后
這篇主要從runtime的角度探究Crash防護(hù)的問(wèn)題, 順便研究和學(xué)習(xí)了一些常見(jiàn)的runtime實(shí)踐. 還研究了一下僵尸對(duì)象的問(wèn)題.
隨著zombie的增長(zhǎng)必定消耗越來(lái)越多的內(nèi)存, 這里沒(méi)有說(shuō)到關(guān)于zombie緩存的問(wèn)題, 這個(gè)問(wèn)題回頭有空研究研究, 再封裝一下這個(gè)Zombie. 待更.
除了野指針之外, Crash還有很多其它原因.
參考:
ARC下dealloc過(guò)程及.cxx_destruct的探究
大白健康系統(tǒng)--iOS APP運(yùn)行時(shí)Crash自動(dòng)修復(fù)系統(tǒng)
Clang 5 documentation OBJECTIVE-C AUTOMATIC REFERENCE COUNTING (ARC)