从RunTime源码回看消息发送及其基础知识

关于我的仓库

  • 这篇文章是我为面试准备的iOS基础知识学习中的一篇
  • 我将准备面试中找到的所有学习资料,写的Demo,写的博客都放在了这个仓库里iOS-Engineer-Interview
  • 欢迎star👏👏
  • 其中的博客在简书,CSDN都有发布
  • 博客中提到的相关的代码Demo可以在仓库里相应的文件夹里找到

前言

  • 本文为消息转发的先导文章
  • 涉及sel,imp的作用,消息发送大概机制

准备工作

  • 请准备好750.1版本的objc4源码一份【目前最新的版本】,打开它,找到文章中提到的方法,类型,对象
  • 一切请以手中源码为准,不要轻信任何人,任何文章,包括本篇博客
  • 文章中的源码都请过了我的删改,建议还是先看看源码
  • 源码建议从Apple官方开源网站获取obj4
  • 官网上下载下来需要自己配置才能编译运行,如果不想配置,可以在RuntimeSourceCode中clone

method_t

  • 我们知道方法是存储在类中的bits指向的class_rw_t中的methods里,而methods是一个二维数组,里面真正存储方法信息的是method_t结构体
///class_rw_t
// 方法信息
method_array_t methods;

///method_array_t
list_array_tt<method_t, method_list_t>
  
///method_t
struct method_t {
    SEL name;
    const char *types;  //方法标签
    MethodListIMP imp;
};
  • 也就是说每一个方法在method_t里要存放三个东西,sel方法签名,方法类型,IMP函数指针【这三个宝贝不同的文章有不同的叫法,本文统一为上面👆三个叫法】

SEL name

  • Objective-C 在编译的时候, 会根据方法的名字(包括参数序列),生成一个用 来区分这个方法的唯一的一个 ID,这个 ID 就是 SEL 类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么它们的 ID 都是相同的。就是 说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同 那么 ID 就是一样的
///Person.m
#import "Person.h"
@implementation Person

- (void)eat {
    NSLog(@"Person EAT");
}

- (void)eat:(NSString *)str {
    NSLog(@"Person EATSTR");
}

- (void)dayin {
    NSLog(@"dayin");
    SEL sell1 = @selector(eat:);
    NSLog(@"sell1:%p", sell1);
    SEL sell2 = @selector(eat);
    NSLog(@"sell2:%p", sell2);
}

@end

///main.m
#import <Foundation/Foundation.h>
#include "Student.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *newPerson = [[Person alloc] init];
        [newPerson dayin];
    }
    return 0;
}

///结果
//dayin
//sell1:0x100000f63
//sell2:0x100000f68
  • 这里要注意,@selector等同于是把方法名翻译成sel方法签名,它只关心方法名和参数个数,在dayin方法里,可以在eat后加上任意个:都有效果
  • 同时,这个生成sel的过程是固定的,因为它只是一个表明方法的ID,不管是在任何类里去写这个dayin方法,还是运行好几次,这个sel都是固定这个【eat就是0x100000f68,eat:就是0x100000f63】
  • 经过测试,在不同的项目里sel的值是不一样的,但在同一个项目里是绝对一样的
  • 当然,这里我们可以直观的看出,sel不关心返回值与参数的数据类型,你在生成sel的时候都没有把这些东西放上去,它们当然不会生成不同的sel
  • 这也是为什么我们不能在一个类里面写两个同名方法【哪怕参数的类型不一样,返回值不一样】,因为存放的时候,会把两个方法看作一个
  • 不同的类可以拥有相同的方法,因为sel的存储是存在这个类的methods里的
  • 本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
  • 虽然 SEL 定义成 char *, 我们可 以把它想象成 int. 每个方法的名字对应一个唯一的 int 值
28653839_1361282379yvAk

const char *types

  • 为了协助运行时系统,编译器用字符串为每个方法的返回值和参数类型和方法选择器编码。使用的编码方案在其他情况下也很有用,所以它是public 的,可用于@encode() 编译器指令。当给定一个类型参数,返回一个编码类型字符串。类型可以是一个基本类型如int,指针,结构或联合标记,或任何类型的类名,事实上,都可以作为C sizeof() 运算符的参数。这个机制也是为了提高Runtime的效率.
  • 编码翻译表:
QQ20190729-194442
  • 可以使用@encode进行验证
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
  • 也就是说,为了提高调取方法的速度,runtime会把方法的参数转换成字符串形式的方法类型进行存储

MethodListIMP imp

  • imp是函数的地址,我们知道runtime实现了OC的底层实现,实际上就是OC在编译运行的时候,底层都是通过C++运行的,包括方法的调用

方法调用的大致流程【消息发送】

  • 消息转发基本是最底层的一块的部分,其中的具体实现涉及到的都是汇编语言,已经超出笔者的作用域了,因此我们只能大概分析下流程
- (void)test:(NSInteger)num {
    NSLog(@"You know nothing about power!");
}

[person test:12];

//这短短一句话传递了,三个信息
//调用者:person
//方法名:test:
//参数:12
//这样一句话就会被转换为objc_msgSend(self, @selector(test:), 12);
  • 我们无法看到objc_msgSend的具体实现,但接下来发生的事情就是根据sel去self的isa指向的类对象里去找该方法【查找过程可以看我前面讲isa的文章】
  • 匹配到相应的sel后,我们集齐了sel,imp,obj【参数】,receiver【调用者】,就可以去执行该方法了

imp具体作用

  • imp就是函数的地址,这里可以直接用C语言的方式来理解,这就相当于某个函数的入口位置,我们调用方法【执行对应函数】其实就是按部就班的执行该C++函数
  • 我们取得了函数指针之后,也就意味着我们取得了执行的时候的这段方法的代码的入口,这样我 们就可以像普通的 C 语言函数调用一样使用这个函数指针。当然我们可以把函数指针作为参数 传递到其他的方法,或者实例变量里面,从而获得极大的动态性。我们获得了动态性,但是付出 的代价就是编译器不知道我们要执行哪一个方法所以在编译的时候不会替我们找出错误,我们只 有执行的时候才知道,我们写的函数指针是否是正确的。所以,在使用函数指针的时候要非常准 确地把握能够出现的所有可能,并且做出预防。尤其是当你在写一个供他人调用的接口 API 的 时候,这一点非常重要。
  • 我们同样进行一些打印试验
- (void)dayin {
    NSLog(@"dayin");
    SEL sell1 = @selector(eat);
    NSLog(@"sell1:%p", sell1);
    IMP imp1 = [self methodForSelector:sell1];  //根据sel获取imp
    NSLog(@"imp1:%p", imp1);
    SEL sell2 = @selector(eat:);
    NSLog(@"sell2:%p", sell2);
    IMP imp2 = [self methodForSelector:sell2];
    NSLog(@"imp2:%p", imp2);
    SEL sell3 = @selector(drink:);
    NSLog(@"sell3:%p", sell3);
    IMP imp3 = [self methodForSelector:sell3];
    NSLog(@"imp3:%p", imp3);
  
    imp1();//可以像这样直接调用imp函数,如果要加参数的话,直接在methodForSelector中加
}

//sell1:0x100000f51
//imp1:0x100000be0
//sell2:0x100000f68
//imp2:0x7fff7c89c300
//sell3:0x100000f6d
//imp3:0x7fff7c89c300
  • 其中eat,eat:,drink:只有eat方法是我有写实现,也只有这个方法的imp是确实存在的【imp1:0x100000be0】,而剩下两个的imp都指向同一个地址【0x7fff7c89c300】,这些数据和sel一样,不管运行几次都是一样的。但在不同的电脑上结果还是不一样,也就是说,虽然0x7fff7c89c300显然是一个特殊的地址,但也不是写死规定好的
  • 因此与sel这样只是生成一个特殊ID不一样,imp是必须要该方法存在【函数存在】才能生成的【也就是方法必须有实现】

在cache以及rw结构体中方法存储的区别

  • 方法缓存在cache中的bucket_t结构体里
  • 方法存储在class_rw_t中的method_array_t二维数组里
//bucket_t
struct bucket_t {
    cache_key_t _key;
    //sel
    MethodCacheIMP _imp;
    //函数内存地址
}

//method_t
struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;
};
  • 这里就有让人非常迷惑的一件事,在方法列表里我们要存储sel,types,imp;而缓存的时候只要sel,imp
  • 这里我也查了很多资料,没能找到解释
  • 其实这也关乎类型编码types到底是干什么的,查来查去只找到可以提高运行速度
  • 所以这里猜测types只是辅助提高运行速度,而Apple缓存方法时,存储这个内容的消耗已经超过了其带来的加速【我知道很扯。。。】
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339