RunTime中消息转发机制及其底层实现逻辑


一、RunTime概念

Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体。


RunTime作用

  • RunTime可以遍历对象的属性。
  • RunTime可以动态添加/修改属性,动态添加/修改/替换方法,动态添加/修改/替换协议。
  • RunTime可以动态创建类/对象/协议等。
  • RunTime可以方法拦截调用。
  • ......

如遍历对象属性:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end
    Person *person = [Person new];
    id personClass = object_getClass(person);
    unsigned int outCount;
    
    objc_property_t *properties = class_copyPropertyList(personClass, &outCount);
    for (int i = 0; i < outCount; i++) {
        objc_property_t property = properties[i];
        NSLog(@"%s:%s\n", property_getName(property), property_getAttributes(property));
    }
    free(properties);

输出:

2017-05-22 20:50:59.040087 TestRunTime[13427:12747880] name:T@"NSString",C,N,V_name
2017-05-22 20:50:59.040154 TestRunTime[13427:12747880] age:Tq,N,V_age

二、RunTime中的函数调用

1、OC中的函数调用

C语言中,仅申明一个函数不去实现,其他地方调用此函数,编译时就会报错(C语言编译时查找要执行的函数,找不到所以报错)。在OC中并不会报错,只有在运行时候才会报错(OC运行时才查找要执行的函数)。

RunTime把对象的方法调用转化成消息发送的代码:

OC: [obj doSth];
runtime:objc_msgSend(obj, @selector(doSth);
  • objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为完成此操作,该方法需要在接收者所属的类中寻找其“方法列表”(下文会提到),如果能找到与选择子名称相符的方法,就跳转至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到合适的方法再跳转。如最终还是找不到相符的方法,那就执行“消息转发”操作。
  • 同时,objc_msgSend会将匹配结果缓存到“快速映射表”(下文会提到)中,每个类都有这样一块缓存。如稍后还向该类发送与选择子相同消息,执行起来快很多。

2、objc_msgSend的消息转发流程

objc_msgSend的流程
objc_msgSend的流程

消息转发包括两个步骤:

  1. 先征询接收者所属的类,看其能否动态添加方法,以处理当前这个“未知的选择子”,该过程叫——“动态方法解析”。
  2. 如步骤1执行完,接收者无法以动态新增方法来响应。执行如下:首先,接受者看是否有其他对象能处理这条消息,若有则运行期系统把消息转给那个对象(即备援接收者),消息转发结束。若没有“备援接收者”,则启动完整消息转发机制:会把与消息相关的细节封装到NSInvocation中,再给接收者最后一次机会,令其设法解决当前未处理的这条消息。
  • 动态方法解析

RunTime调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法允许开发者对当前收到的消息func做出响应。此方案常用来实现@dynamic属性。

// 给Person类加一个体重weight属性
@property (nonatomic, assign) NSInteger weight;

/**
 重写resolveInstanceMethod方法:动态方法解析

 @param sel <#sel description#>
 @return <#return value description#>
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(setWeight:)) {
        class_addMethod([self class], sel, (IMP)setPropertyDynamic, "v@:");
        
        return YES;
    }
    
    return [super resolveInstanceMethod:sel];
}

void setPropertyDynamic(id self, SEL _cmd) {
    NSLog(@"Dynamic setWeight");
}

// 调用Person的setWeight方法
Person *lision = [[Person alloc] init];
lision.weight = 75;

// 如果不重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法本应异常,但打印出信息:
2017-05-23 08:53:24.189509 TestRunTime[13457:12851395] Dynamic setWeight
  • 重定向

如果没有重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,那就就会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,把这个消息让另一个对象来处理,这叫做重定向。

现在Person类中添加一个weight属性。新建一个People类来等待重定向。

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) NSInteger weight;
@end

@interface People : NSObject
@end

给新写的People类加一个weight方法,注意:People没有weight属性。

@implementation People

- (NSInteger)weight {
    return 70;
}

- (void)setWeight:(NSInteger)weight {
    NSLog(@"%s", __func__);
}

@end

在Person类中重写- (id)forwardingTargetForSelector:(SEL)aSelector方法:

@implementation Person

@dynamic weight;

///**
// 重写resolveInstanceMethod方法:动态方法解析
//
// @param sel <#sel description#>
// @return <#return value description#>
// */
//+ (BOOL)resolveInstanceMethod:(SEL)sel {
//    if (sel == @selector(setWeight:)) {
//        class_addMethod([self class], sel, (IMP)setPropertyDynamic, "v@:");
//        
//        return YES;
//    }
//    
//    return [super resolveInstanceMethod:sel];
//}
//
//void setPropertyDynamic(id self, SEL _cmd) {
//    NSLog(@"Dynamic setWeight");
//}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(setWeight:) || aSelector == @selector(weight)) {
        People *people = [[People alloc] init];
        return people;
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

@end

Person *lision = [[Person alloc] init];
lision.weight = 75;
NSLog(@"weight = %ld", lision.weight);
    
// 输出
2017-05-23 10:38:45.377410 TestRunTime[13547:12879235] weight = 70

发现虽然你给weight属性赋值明明是75,可是打印结果是:weight = 70。这就是Person类- (id)forwardingTargetForSelector:(SEL)aSelector方法中把这条信息抛给了people对象,调用了People类的weight方法。

  • 消息转发

如果上面的两个方法都没有重写,并且消息依然是当前对象没有实现的方法,RunTime才会启用消息转发调用– (void)forwardInvocation:(NSInvocation *)anInvocation,需要注意的是这个方法花费代价较大,如果要实现把消息转发类似的功能建议最好使用重定向,而且再调用这个方法前RunTime会先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法。

继续给Person类加入属性:

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) NSInteger weight;
@property (nonatomic, copy) NSString *ID;

@end

实现上面提到的两个方法:

@implementation Person

@dynamic ID;

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    People *people = [[People alloc] init];
    if ([people respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:people];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(setID:)) {
        // "v@:"代表的意思参见Objective-C Type Encodings,这里的意思是返回值为空
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return nil;
}

@end

在People类中添加对应的set方法:

@implementation People

- (void)setID:(NSString *)ID {
    NSLog(@"People setID:%@", ID);
}

@end

输出:

Person *lision = [[Person alloc] init];
lision.ID = @"xxxxx";

// 输出
2017-05-23 11:11:11.368598 TestRunTime[13598:12891137] People setID:xxxxx

三、OC中函数调用底层实现

将调用函数的对象obj和函数的方法名对应的选择子@selector(doSth)作为参数传入objc_msgSend()方法中,由objc_msgSend()方法实现了函数查找和匹配,该方法通过一下步骤来查找和调用:

  1. 根据对象obj找到对象类中存储的函数列表methodLists。
  2. 根据选择子@selector(doSth)在methodLists中查找对应的函数指针method_imp。
  3. 根据函数指针method_imp调用响应的函数。

objc_msgSend的底层原理

  • 任意一个NSObject对象,都有一个isa属性,指向对象对应的Class类
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
  • 对象对应的Class,是一个结构体指针,指向objc_class结构体
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

struct objc_class {
    // 指向metaclass
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    // 指向父类Class
    Class super_class                                        OBJC2_UNAVAILABLE;
    // 类名 
    const char *name                                         OBJC2_UNAVAILABLE;
    // 类的版本信息
    long version                                             OBJC2_UNAVAILABLE;
    // 一些标识信息,标明是普通的Class还是metaclass
    long info                                                OBJC2_UNAVAILABLE;
    // 该类的实例变量大小(包括从父类继承下来的实例变量)
    long instance_size                                       OBJC2_UNAVAILABLE;
    //类中成员变量的信息
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    // 类中方法列表
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    // 查找方法的缓存,用于提升效率
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    // 存储该类遵守的协议  
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
  • objc_class中有一个methodLists,是一个objc_method_list结构体
struct objc_method_list {
    // 废弃、过时的属性
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    // 方法的个数
    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    // 方法的首地址
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}  

为什么是method_list[1],数组的大小怎么会是1呢?由于数组的大小是不定的,不同的类对应的不同的方法个数,所以定义时只存储首地址,在实际使用过程中再扩展长度。

  • objc_method结构体
struct objc_method {
    // 函数的SEL
    SEL method_name                                          OBJC2_UNAVAILABLE;
    // 函数的类型
    char *method_types                                       OBJC2_UNAVAILABLE;
    // 函数指针
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
  • 流程:
  1. obj->isa(Class类型) :obj对象通过isa属性拿到对应的Class。
  2. Class->methodLists(objc_method_list类型): Class通过methodLists属性拿到存放所有方法的列表。
  3. objc_method_list->method_list: 在objc_method_list中通过SEL查找到对应的objc_method。
  4. objc_method->method_imp(IMP类型): objc_method通过method_imp属性拿到函数指针。
  5. method_imp->调用函数:通过函数指针调用函数。

函数调用中cache的使用

  • SEL是什么
/// An opaque type that represents a method selector.
/// 一种不透明的类型,它代表着一个方法选择器。
typedef struct objc_selector *SEL;

SEL本质是一个int类型的地址,指向存储的方法名。对于每一个类,都会分配一块特殊空空间,专门存储类中的方法名,SEL就是指向对应方法名的地址。由于方法名字符串是唯一的,所以SEL也是唯一的。

  • cache的使用

从上面的流程:obj->isa(Class类型)->methodLists(objc_method_list类型)->objc_method->method_imp(IMP类型)->调用函数,可以看出,函数调用的时间主要消耗在“objc_method_list->method_list”,即在objc_method_list中通过SEL查找到对应的objc_method。cache就是对该过程进行优化。

可以把cache简单当成一个哈希表,key是SEL,Value是objc_method。包括以下两个步骤:

  1. 通过SEL在cache中查找objc_method,若找到了直接返回,若未找到执行2 。
  2. 在methodLists中查找objc_method,找到之后先将objc_method插入cache中以方便下次查找,再返回objc_method。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,635评论 0 9
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,469评论 33 467
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,109评论 0 7
  • 转载:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麦子阅读 694评论 0 2
  • 蓝大说的太好了。 弱水三千,只取一瓢饮。 这个问题之前也提过。好问题。 1.投基础设施类项目,这...
    hbliuwb阅读 232评论 0 1