消息转发

2018-05-09  本文已影响16人  4ab5c73f365f

1. 消息查找

Objective-C 具有很强的动态性,它将静态语言在编译和链接时期做的工作,放置到运行时来处理. Runtime就是这门语言的基础.

在Objective-C中, 某个对象进行方法调用, 多被称作向对象发送消息。我们如果需要了解这个问题就需要向底层探究.我们平时编写的Objective-C代码,底层实现其实都是C\C++代码
如图:


OC-C++-.png

所以我们通过向下探究来研究一下,示例如下:

@interface Person : NSObject
- (void)run;
@end

@implementation Person
- (void)run {
    NSLog(@"person is running");
}
@end

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
    [p run];
}

那么如何将Objective-C代码转换为C\C++代码呢? 苹果为我们提供了工具:

规范: 
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

操作: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.mm

[person run] 经过编译后如下调用的是objc_msgSend方法

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
    Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
}

通过转变为C++代码我们发现

((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));

objc_msgSend()函数被调用, 通过查看Runtime 找到对应的函数定义. OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);self:消息的接收者 , op: 消息的方法名,C 字符串 ... :参数列表

那么对象调用方法, 是怎么进行转发的呢, 我们特地准备了一张图来描述:

消息转发机制.png

我们可以看到消息的处理分为两个阶段, "动态方法决议" 和 "消息转发", 只有在动态方法决议期间找不到方法时候才会进行下一步. 那我们先探究一下第一个阶段是怎么进行的. 在研究第一个阶段之前, 我们再来看一张经典图:


类继承.jpg oc类图.png oc继承图.png

在这两张图中,京D巴拉巴拉(京DBLABLA)就是实例, 小汽车类就是类对象, 小汽车类指向小汽车元类.小汽车元类指向 根元类, 根元类指向自己形成闭环, 通过图中的关系, 我们知道了实例对象通过isa指针,找到类对象, 类对象通过isa指针找到元类, 元类通过isa指针找到根元类对象. 根元类isa指向自己形成闭环, 但是superclass 指向NSObject, 所以可以说NSObject 就是消息机制的核心.

OK, 在我们了解了OC基本概念之后回归到函数本身

((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));

通过cmd+shift+o快捷键输入函数名 找到objc/runtime.h objc_msgSend函数函数定义如下, 但是函数中参数的sel_registerName("person_run")是什么, 我们继续查找, 同样在objc/runtime.h 中找到定义. 该函数在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器.


OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);


OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)

我们既然了解了函数, 我们从新回顾一下上边的"OC RUNTIME的基本概念"这张图. 回顾后,我们继续探究Class是什么? id 关键字是什么, 老方法,通过cmd+shift+o快捷键输入函数名,我们发现如下

typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

Class 和 id 都是 objc_object 结构体, 继续查看objc_object是什么 我们看到

struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class OBJC2_UNAVAILABLE;
    const char * _Nonnull name OBJC2_UNAVAILABLE;
    long version OBJC2_UNAVAILABLE;
    long info OBJC2_UNAVAILABLE;
    long instance_size OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

// 最新版本objc4-208 显示如下
struct objc_class {
    struct objc_class *isa;
    struct objc_class *super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;

#if defined(Release3CompatibilityBuild)
    struct objc_method_list *methods;
#else
    struct objc_method_list **methodLists;
#endif

struct objc_cache *cache;
struct objc_protocol_list *protocols;
};

在上边结构体中我们需要重点关注几个关键字

  1. isa 指向父类的指针.

  2. methodLists 一个类的方法分发表.

当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。


iOS-类熟悉继承.png

如上图所示的继承关系, 就意味着我们寻找一个实例方法, 在student找不到, 通过isa指针指向的Person, 在Person中继续寻找,直到NSObject. 例如 alloc 函数, 会通过isa 一层一层直到NSObject为止.

objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果 没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依 此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。

通过阅读神经病院 Objective-C Runtime 住院第二天:消息发送与转发 文章找到一段

#include <objc/objc-runtime.h>

id c_objc_msgSend( struct objc_class /* ahem */ *self, SEL _cmd, ...)
{
    struct objc_class *cls;
    struct objc_cache *cache;
    unsigned int hash;
    struct objc_method *method;
    unsigned int index;

if( self)
{
    cls = self->isa;
    cache = cls->cache;
    hash = cache->mask;
    index = (unsigned int) _cmd & hash;
do
{
    method = cache->buckets[ index];
    if( ! method)
    goto recache;
    index = (index + 1) & cache->mask;
}
while( method->method_name != _cmd);
    return( (*method->method_imp)( (id) self, _cmd));
}
    return( (id) self);

recache:
    /* ... */
    return( 0);
}

通过C版本的objc_msgSend的源码版本,画了一张大致示意图:

此图描述仅供参考, 实际查找方式没进行深入研究, 请勿严重拍砖

iOS-实现消息转发.png iOS-isa-方法类方法.png iOS-消息转发-superclass.png

通过遍历类对象方法, meta-class, 找不到匹配的方法, 通过superclass 继续在父类中继续同样的操作,找到后加入实例类对象的cache, 如果遍历到NSObject还是找不到就开始了 消息转发.

2.消息转发

进入消息转发就不得再展示一次该图.


消息转发机制.png

对象在收到无法响应的消息后,会调用其所属类的下列方法

/**
* 如果尚未实现的方法是实例方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增实例方法用以处理selector
*/
+ (BOOL)resolveInstanceMethod:(SEL)selector;
/**
* 如果尚未实现的方法是类方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增类方法用以处理selector
*/
+ (BOOL)resolveClassMethod:(SEL)selector;

备援接收者 或者 快转发
如果无法动态解析方法,运行期系统就会询问是否能将消息转给其他接收者来处理,对应的方法为

/**
* 此方法询问是否能将消息转给其他接收者来处理
*
* @param aSelector 未处理的方法
*
* @return 如果当前接收者能找到备援对象,就将其返回;否则返回nil;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector;

完整的消息转发机制
如果前面两步都无法处理消息,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把尚未处理的那条消息有关的全部细节装在里面,在触发 NSInvocation 对象时,消息派发系统(message-dispatch system)将会把消息指派给目标对象。对应的方法为

/**
* 获取指定selector的方法签名
*
* @param aSelector aSelector 未处理的方法
*
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

/**
* 消息派发系统通过此方法,将消息派发给目标对象
*
* @param anInvocation 之前创建的NSInvocation实例对象,用于装载有关消息的所有内容
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation;

这个方法可以实现的很简单,通过改变调用的目标对象,使得消息在新目标对象上得以调用即可。然而这样实现的效果与 备援接收者 差不多,所以很少人会这么做。更加有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数、修改 selector 等等。

#import <Foundation/Foundation.h>

@interface Person : NSObject
- (void)run;
+ (void)eat;
@end


#import "Person.h"
#import <objc/runtime.h>
#import "Teacher.h"

@implementation Person

// 并没有真正实现run 方法, 和 eat 方法

void eatClass(id self, SEL cmd) {
    NSLog(@"类方法eat的实现");
}

void runClass(id self, SEL cmd) {
    NSLog(@"方法run的实现");
}

// first
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(eat))
{
    NSLog(@"eat static method 调用resolveClassMethod");
    BOOL success = class_addMethod(object_getClass([self class]), sel, (IMP)eatClass, "v@:");
    return success;
}
    return [super resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(run)) {
    NSLog(@"run method , 调用resolveInstanceMethod");
    BOOL success = class_addMethod([self class], sel, (IMP)runClass, "v@:");
    return success;
    // return NO; // 返回NO 会进行快转发.
}
    return [super resolveInstanceMethod:sel];
}

// second
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
 if (aSelector == @selector(run)) {
    // return nil; // 返回nil 会进行完整转发.
    return [[Teacher alloc] init];
} else {
    return [super forwardingTargetForSelector:aSelector];
  }
}


// third
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if (sel_isEqual(aSelector, @selector(run))) {
        signature = [[[Teacher alloc] init] methodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSString *sel = NSStringFromSelector(anInvocation.selector);
    if ([sel isEqualToString:@"run"]) {
        [anInvocation invokeWithTarget:[[Teacher alloc] init]];
    } else {
        [super forwardInvocation:anInvocation];
   }
}

demo 地址

Luckycity

实际应用

防止 Unrecogized-selector

参考

Objective-C Runtime 运行时之三:方法与消息
神经病院 Objective-C Runtime 住院第二天:消息发送与转发
Effective Objective-C Notes:理解消息传递机制
美团-技术专家-臧成威
李明杰-讲师

上一篇下一篇

猜你喜欢

热点阅读