死磕Objective-C runtime運行時之二

實戰問題一(答案在最後):

如何吞掉整個應用的unrecognized selector sent to instance崩潰, 使程序正常運行?html

動態添加方法class_addMethod

第一種:

@interface ViewController()
- (void)hello:(NSString *)content;
@end

void hello(id self, SEL selector, NSString *content){
    NSLog(@"hello %@", content);
}

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    class_addMethod([self class], @selector(hello:), (IMP)hello, "v@:@");
    [self hello:@"world"];

}

運行時objc_msgSend說過:ios

Bbjc方法只不過是C語言方方法,加上兩個特殊的參數第一個是receiver(self),第二個參數是selector(_cmd)app

這裏咱們有個C函數void hello(id self, SEL selector, NSString *content),除了上述兩個必要參數外,咱們添加了個NSString *content, 而後用class_addMethod添加,最後一個參數是Objc運行時符號,具體參考這裏, 第一個V表明返回值void, @表明id,:表明SEL,@表明id(這裏是NSString *)ide

這裏因爲在調用[self hello:@"world"]之時, 運行時方法class_addMethod添加了hello:方法的,參考運行時objc_msgSend, 完成方法調用函數

第二種:

@interface ViewController ()
- (void)hello:(NSString *)content;
@end

void hello(id self, SEL selector, NSString *content){
    NSLog(@"hello %@", content);
}

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    [self hello:@"world"];

}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(hello:)) {
        class_addMethod([self class], sel, (IMP)hello, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

這回,咱們先調用方法,因爲經過運行時objc_msgSend,沒法找到hello:, 此時運行時會發消息給resolveInstanceMethod:resolveClassMethod:方法,本文由於成員方法調用[[self class] resolveInstanceMethod:@selector(hello:)],詢問該方法是否是有可能動態實現呢,根據第二種代碼實現,發現若是是@selector(hello:), 加入動態方法,而後return YES給運行時,告知能夠處理該方法。ui

過程當中,我猜想運行時會關注class_addMethod等相關功能代碼,而後快速派遣,即便return YES, 假如此類狀態沒有任何變化,直接調用doesNotRecognizeSelector:拋出異常spa

消息轉寄Forwarding

若是運行時objc_msgSend找不到該方法,在拋出異常以前,運行時給咱們一個機會轉寄(Forwarding)這個消息的機會:code

第一種:

@interface SomeFool: NSObject
- (void)hello:(NSString *)content;
@end

@implementation SomeFool
- (void)hello:(NSString *)content{
    NSLog(@"hello %@", content);
}
@end

@interface ViewController (){
    SomeFool *_surrogate;
}
- (void)hello:(NSString *)content;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self hello:@"world"];

}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (!_surrogate) {
        _surrogate = [SomeFool new];
    }
    return _surrogate;
}
@end

第一次機會是運行時詢問forwardingTargetForSelector:是否是這個方法其餘人可以處理呢?代碼forwardingTargetForSelector:返回SomeFool實例,運行時就不會抱怨,把消息傳給SomeFool實例啦htm

第二種:

@interface SomeFool: NSObject
- (void)hello:(NSString *)content;
@end

@implementation SomeFool
- (void)hello:(NSString *)content{
    NSLog(@"hello %@", content);
}
@end

@interface ViewController (){
    SomeFool *_surrogate;
}
- (void)hello:(NSString *)content;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _surrogate = [SomeFool new];
    [self hello:@"world"];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature = [_surrogate methodSignatureForSelector:aSelector];
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    if ([_surrogate respondsToSelector: [anInvocation selector]]){
        [anInvocation invokeWithTarget:_surrogate];
    }
    else{
        [super forwardInvocation:anInvocation];
    }
}

第二次機會是運行時詢問methodSignatureForSelector:是否是這個方法其餘人有具體實現呢?代碼中咱們把SomeFool將具體實現返回,而後運行時就會調用
forwardInvocation:。在其中,咱們用[anInvocation invokeWithTarget:_surrogate]調用方法。直接將消息從新跑給SomeFool, 首先之行運行時objc_msgSend對象

消息轉寄(Forwarding)總結:

這裏說的是運行時objc_msgSend不成功的時候:

  1. resolveClassMethod:resolveInstanceMethod, 若返回YES同時運行時狀態有新函數加入,則直接調用實現,完成消息發送

  2. 若否則, forwardingTargetForSelector: 若返回不是nil和self,則完成消息發送, 對返回對象進行運行時objc_msgSend

  3. 若否則, methodSignatureForSelector: 若返回不爲空,則發送消息給forwardInvocation:由Invocation完成

  4. 若否則, 調用doesNotRecognizeSelector:拋出異常

問題一答案:

PS: 本例爲Swizzle的正確打開方式,詳情

#import <objc/runtime.h>
@interface ViewController ()
- (void)hello:(NSString *)content;
- (void)whoareyou;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self hello:@"world"];
    [self whoareyou];
    
}
@end



IMP __original_forwardInvocation = NULL;
IMP __original_methodSignatureForSelector = NULL;

void __swizzle_forwardInvocation(id self, SEL _cmd, NSInvocation * anINvocation){
    //pretend nothing happen
}

NSMethodSignature * __swizzle_methodSignatureForSelector(id self, SEL _cmd, SEL aSelector){
    return ((NSMethodSignature *(*)(id, SEL, SEL))__original_methodSignatureForSelector)(self, _cmd, NSSelectorFromString(@"cacheAll"));
}


@interface NSObject(cacheAll)
@end

@implementation NSObject(cacheAll)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method oringal_method
        = class_getInstanceMethod([self class], @selector(forwardInvocation:));
        __original_forwardInvocation = method_setImplementation(oringal_method, (IMP)__swizzle_forwardInvocation);
        
        oringal_method = class_getInstanceMethod([self class], @selector(methodSignatureForSelector:));
        __original_methodSignatureForSelector = method_setImplementation(oringal_method, (IMP)__swizzle_methodSignatureForSelector);
    });
}

- (void)cacheAll{}
@end
相關文章
相關標籤/搜索