[iOS]Objective-C 消息转发知识点梳理

1.1 什么是Objective-C消息转发

Objective-C的对象消息传递性根据接收到的消息,找到并执行对象中的方法。当对象收到与其方法集不匹配的消息时,通过消息转发机制可以使对象执行用户预先定义的处理流程。消息转发使对象能够在收到无法识别的消息时执行各种逻辑,如将消息发给能做出回应的接收器等。


1.2 消息转发流程

方法在调用时,系统会查找对象是否能接收消息(查找这个类有没有这个方法,或者有没有实现这个方法。),如果不能接收,系统会给出三种补救措施来避免因找不到方法而crash。下面是iOS系统给我们提供的三种补救措施所采用的实现方案,我们正是通过这三种方案来实现消息转发的。

1.3 动态解析

动态解析顾名思义就是动态的为类添加方法。所谓的动态添加是指:在程序运行时给某个类增加方法的实现,此方法可以是实例方法也可以是类方法。

动态解析主要重写系统提供给我们的两种API:

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

1.4 快速转发

快速转发是指将消息转发给能接收此消息的对象。主要是重写下面的方法:
- (id)forwardingTargetForSelector:(SEL)aSelector 
从方法的签名我们知道,此方法返回的是id类型,通常它返回的是能接收消息的对象。

1.5 标准转发

NSObject子类可以重写NSObject类的forwardInvocation:方法,实现标准转发。该技巧可以使用消息的全部内容(目标

方法和参数名)。在重写上面方法之前,需要手动实现方法签名功能:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector


1.6 demo

下面通过一个demo来演示消息转发的三种方案。

首先,新建一个Atom类,定义属性及方法如下:

#import <Foundation/Foundation.h>

@interface Atom : NSObject
{
    @protected NSUInteger _protons;
    @protected NSUInteger _neutrons;
    @protected NSUInteger _electrons;
    @protected NSString * _chemicalElements;
    @protected NSString *_atomicSymbol;
}
@property (readonly) NSUInteger protons;
@property (readonly) NSUInteger neutrons;
@property (readonly) NSUInteger electrons;
@property (readonly) NSString *chemicalElements;
@property (readonly) NSString *atomicSymbol;

- (NSUInteger)massNumber;

- (NSString *)logInfo;
@end

实现.m文件如下:

#import "Atom.h"

@implementation Atom

- (id)init{
    if (self = [super init]) {
        _chemicalElements = @"None";
    }
    
    return self;
}

- (NSUInteger)massNumber {
    return self.protons + self.neutrons;
}

- (NSUInteger)atomicNeruns {
   return self.neutrons;
}

- (NSString *)logInfo {
    NSString *result = [NSString stringWithFormat:@"%@:\n symbols =  %@,neutrons = %ld",_chemicalElements,_atomicSymbol,_neutrons];
    NSLog(@"%@",result);
    return result;
}

@end

然后,我们再新建一个Hydrogen类,继承自上面Atom类。

Hydrogen.h:

#import "Atom.h"

@interface Hydrogen : Atom

- (id)initWithNeutrons:(NSUInteger)neutrons;

+ (id)hydrogenWithNeutrons:(NSUInteger)neutrons;

@end

在Hydrogen.m中,我们要实现消息转发的功能:

#import "Hydrogen.h"
#import "HydrogenHelper.h"
#import "TestHelper1.h"
#import <objc/runtime.h>

@implementation Hydrogen {
    @private HydrogenHelper *helper;
    @private TestHelper1 *helper1;
}

- (id)initWithNeutrons:(NSUInteger)neutrons {
    if (self = [super init]) {
        _chemicalElements = @"Hydrogen";
        _atomicSymbol = @"H";
        _protons = 1;
        _neutrons = neutrons;
        
        // 实现消息转发机制创建辅助对象
        helper = [[HydrogenHelper alloc] init];
        helper1 = [[TestHelper1 alloc] init];
        
    }
    
    return self;
}

+ (id)hydrogenWithNeutrons:(NSUInteger)neutrons {
    return [[self alloc] initWithNeutrons:neutrons];
}

/* 动态解析 */

// 1、动态添加
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(testMethod)) {
        class_addMethod(self,sel,(IMP)testMethod,"v@:");
        return YES;
    }
    return [super resolveInstanceMethod: sel];
}

// 2、方法实现
void testMethod(id self,SEL _cmd) {
    NSLog(@"%@ %s",self,sel_getName(_cmd));
}


/* 快速转发 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([helper respondsToSelector:aSelector]) {
        return helper;
    }
    
    return nil;
}


/* 正常转发 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig ;
    sig = [helper1 methodSignatureForSelector:aSelector];
    if (sig) {
        return sig;
    }
    
    return nil; 
}


- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector  == @selector(testMethod)) {
        [anInvocation invokeWithTarget:helper1];
    }
}

@end

我们来看看源代码:

首先定义了HydrogenHelper类型的实例变量:helper。 这个helper实例变量是后面为快速转发方法:

- (id)forwardingTargetForSelector:(SEL)aSelector

提供的消息接收的对象。关于HydrogenHelper类的定义和实现参考下面:

HydrogenHelper.h:

#import <Foundation/Foundation.h>

@interface HydrogenHelper : NSObject

- (NSString *)factoid;

@end

HydrogenHelper.m:

#import "HydrogenHelper.h"

@implementation HydrogenHelper

- (NSString *)factoid {
    return @"The light elements and most abundant chemicl substance.";
}

@end

这个类只有一个方法:factoid。

接着看源码:在Hydrogen.m中还定义了一个TestHelper1的类,这个类定义了一个testMethod方法,为标准消息转发提供实现。

在初始化方法中,我们分别初始化上面提到的2个实例变量,已备后用。

我们再来看看测试代码,在ViewDidLoad中加入以下代码:

#import "ViewController.h"
#import "Atom.h"
#import "Atom+Nuclear.h"
#import "Atom+Ext.h"
#import "Hydrogen.h"
#import "Atom+Helper.h"

@interface ViewController ()


@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    Atom *atom = [[Hydrogen alloc] initWithNeutrons:0];
//    NSLog(@"Atom chemical element name: %@",atom.chemicalElements);
    [atom logInfo];
    
    
    id atom1 = [[Hydrogen alloc] initWithNeutrons:1];
//    NSLog(@"Atom chemical element name: %@",((Hydrogen *)atom1).chemicalElements);
    [atom1 logInfo];
    
    
    // 通过消息转发,获取Hydrogen的事实
    Hydrogen *atom2 = [Hydrogen hydrogenWithNeutrons:2];
//    NSLog(@"atom2 chemical element name: %@",atom2.chemicalElements);
    

    [atom2 testMethod];
    [atom2 logInfo];
    
    
}

@end

我们看到这句:[atom2 testMethod]; 当我们向对象atom2发送testMethod消息时,因为atom2是Hydrogen的实例,Hydrogen并没有testMethod这个方法,所以,如果我们不实现这个方法程序会导致crash。那么前面时候的动态解析就可以派上用场了。

我们看看动态解析都做了什么。首先,我们给Hydrogen类实现了下面的方法

+ (BOOL)resolveInstanceMethod:(SEL)sel

在这个方法中,我们动态的给Hydrogen类添加了testMethod方法。并且实现了此方法。不过此方法是通过c函数来实现的,而并不是OC的方法。

void testMethod(id self,SEL _cmd) {
    NSLog(@"%@ %s",self,sel_getName(_cmd));
}

通过动态添加新方法来实现消息转发就这么简单!

接着,我们来看看快速消息转发和正常转发。正如代码所示,快速转发是通过实现下面的方法来完成的:

/* 快速转发 */
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([helper respondsToSelector:aSelector]) {
        return helper;
    }
    
    return nil;
}

它的本质是检查helper这个对象是否可以接受aSelector所代表的方法,我们前面提到helper是属于HydrogenHelper类,此类并没有实现testMethod方法,因此上面方法返回nil。因为我们实现了标准消息转发,程序接着会执行到:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig ;
    sig = [helper1 methodSignatureForSelector:aSelector];
    if (sig) {
        return sig;
    }
    
    return nil; 
}

在这个方法里面首先检查help1对象是否有testMethod的方法签名,由于help1是TestHelp1类型,而且实现了testMethod方法,从而程序会返回方法签名,接着会执行到:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector  == @selector(testMethod)) {
        [anInvocation invokeWithTarget:helper1];
    }
}

这里,helper1作为anInvocation对象的调用参数,程序转而执行helper1的testMethod方法,从而达到了标准消息转发的目的。

下面是TestHelper1类的定义和实现:

#import <Foundation/Foundation.h>

@interface TestHelper1 : NSObject

- (void)testMethod;

@end


#import "TestHelper1.h"

@implementation TestHelper1

- (void)testMethod {
    NSLog(@"i am TestHelper1");
}

@end

到这里,我们实现了三种消息转发的实现。(完)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值