Runtime 消息发送

1、isa 详解

  • isa 在 arm64 架构之前就是一个普通的指针,存储着 Class、Meta-Class 对象的内存地址
  • 从 arm64 架构开始,对 isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息
arm64 后:
instance 对象中 isa & ISA_MASK -----> Class 对象地址
class 对象中 isa & ISA_MASK -----> Meta_Class 对象地址

2、对象结构(isa解释)

struct objc_object {
        isa_t isa;
    }
    
    // 共用体:使用位域
    union isa_t {
        uintptr_t bits; // bits 的二进制一共 64 位。每一位都代表一个信息,详情看下面的 struct 解释
        #define ISA_MASK 0x0000000ffffffff8ULL
        // arm64下,bits 中结构如下
        struct {
            uintptr_t nonpointer            : 1 // 第1位:0 代表普通指针,存储着 Class、Meta-Class 对象的内存地址,1 代表优化过,使用位域存储着更多信息
            uintptr_t has_assoc             : 1 // 第2位:是否设置过关联属性,如果没哟,释放会更快
            uintptr_t has_cxx_dtor          : 1 // 第3位:是否有 C++ 的析构函数,如果没有,释放会更快
            uintptr_t shiftcls              : 33 // 第4~36位:类对象\元类对象的地址
            uintptr_t magic                 : 6 // 用于在调试是分辨对象是否为完成初始化
            uintptr_t weakly_referenced     : 1 // 是否被若引用指向过
            uintptr_t deallocating          : 1 // 正在释放
            uintptr_t has_sidetable_rc      : 1 // 计数器首付过大无法存储在 isa 中,如果为 1,那么引用计数会存储在一个叫 SideTable的类的属性中
            uintptr_t extra_rc              : 19 // 存储引用计数器,具体指为引用计数器-1
        }
    }

3、Class 结构

类对象中存放着: isa 指针、superclass 指针、属性、对象方法、协议、成员变量
元类对象中存放着: isa 指针、superclass 指针、类方法
元类对象 是一种特殊的 “类对象”

struct objc_class: objc_object {
        Class isa // 这里的 isa == isa_t isa
        Class superclass;
        cache_t chches; // 方法缓存
        class_data_bits_t bits; // 用于获取具体的类信息
    }

bits & FAST_DATA_MASK 可以得到 class_rw_t 结构体(具体的类信息)
class_rw_t 中保存了方法列表,属性类表,协议类表等

// 运行时会动态创建,包含了类,分类中的信息
struct class_rw_t {
    ...
    const class_to_t *ro;            // 编译时确定的中的成员变量方法,属性,协议等
    method_list_t *methods;          // 在运行时将类中,分类中的方法放到二维数组中
    propocity_list_t *propercies;    // 属性类表
    protocol_list_t *protocols;      // 协议列表
    ...
}
    
// 编译时就确定的类中的信息,这里的信息在运行时是不可变的
struct class_to_t {
    ...
    const char * name;  // 类名
    method_list_t *basemMethodList; // 类中的方法列表
    propocity_list_t *basePropercies;
    protocol_list_t *baseProtocols;
    const ivar_list_t *ivars;  // 成员变量列表
    ...
}

类中的 methods 是一个二维数组,查找方法比较耗时,所以 OC 准备了 caches 这个变量,用来存储一些曾经使用过的方法,以便再次调用方法时提高时间效率。

3.1、method

struct method_t {
    SEL name;               // 方法的名字 
    const char *types;      // 方法的返回值类型,参数类型 
    IMP imp;                // 方法实现的指针,地址
}
  • IMP代表函数的具体实现
    typedef id _Nullable (*IMP) (id _Nonnull, SEL _Nonnull, ...)

  • SEL 代表方法\函数名,一般叫选择器,底层结构类似char *

    • 可以通过 @selector() 和 sel_registerName() 获得
    • 可以通过 sel_getName() 和 NSSringFromSelector() 转成字符串
    • 不同类中相同名字的方法,所对应的方法选择器是相同的
      typedef struct objc_selector *SEL
  • types 包含了函数返回值,参数类型的字符串
    [返回值][参数 1]...[参数 n]

3.2、cache

cache 使用了‘散列表’的数据结构存储来存储曾经调用过的方法(cache 中存储的是[method_t, method_t]),可以提高调用速度

struct cache_t {
     struct bucket_t *_buckets;// 散列表:数组实现
     mask_t _mask;               // 三类表长度 - 1
     mask_t _occupied;           // 已缓存的方法数量
}

struct bucket_t {
    cache_key_t _key;       // SEL 作为 key
    IMP _imp;               // 函数的内存地址
}

OC 中散列表的规则:

  1. 初始化默认容量 _mask, 比如:_mask = 4
  2. 根据 sel 查找方法
  3. 结果查到了,判断方法名是否一致
    是:取 IMP,调用方法
    否:index - 1 且 != 初始 index,再次查找
  4. 结果没有查到,返回 NULL,这时需要从 objc_rw_t 结构体的 methods 中遍历取方法,调用成功后在添加到 cache 中

计算 index 方法:

 index = 方法名 & 容量 // 这里取到 index < 容量

4、objc_msgSend 消息发送

OC 方法调用:消息机制,给方法调用者发送消息

都是转换成了 objc_msgSend 消息发送

objc_msgSend 的执行流程可以分为 3 大阶段:

  • 消息发送
  • 动态方法解析
  • 消息转发

如果三个阶段都没有成功,则报错:unrecognized selector sent to instance

4.1、消息发送

1、判断接受者receiver 是否为nil, 如果为 nil,直接退出 (所以nil也可以调用方法),结束

2、如果 receiver 不为nil,则从 receiverClass 的cache中进行查找,如果找到了该方法,则返回方法地址,直接调用,结束。

3、如果从 cache 中没有找到该方法,则从 receiverClass 的class_rw_t中进行查找,如果找到了该方法,返回方法地址,直接调用,并将method_t缓存到cache中,结束

  • 如果已排序的:二分查找
  • 如果没有排序:遍历查找

4、如果仍然没有找到该方法,则从superClasscache中进行查找,如果找到了该方法,返回方法地址,直接调用,并将method_t缓存到receivercache中,结束

5、如果仍然没有找到该方法,则从superClassclass_rw_t中进行查找,如果找到了该方法,返回方法地址,直接调用,并将method_t缓存到receivercache中,结束

6、如果仍然没有找到该方法,则一直向上查找父类,循环第4、5步,直到root

7、如果一直找到了root,仍然没有找到该方法,则进行<<动态方法解析>>

4.2、动态方法解析

  1. 是否已经有过动态解析,如果有,则直接进行<<消息转发>>

  2. 如果没有,则进行动态解析。如果业务员实现了resolveInstanceMethod:resolveClassMethod的方法,并在该方法中动态添加方法,这些方法会添加到class_rw_t的 methods 中,并标记为已解析
    可以使用class_addMethod(...)方法动态添加,

  3. 动态解析后,获取动态方法解析方法的返回值(用于打印),会再次回到<<消息发送>>阶段,再次查找方法

4.3、消息转发

  1. 调用forwardingTargetForSelector:方法进行转发,这个方法可以返回能够处理该方法的对象,如果返回了其他对象,则会交给返回对象进行处理,即给返回的对象发送消息objc_msgSend(forwardingTarget, aSelector)

  2. 如果没有返回forwardingTarget,则调用methodSignatureForSelector:方法进行方法签名,如果返回签名为nil,则调用doesNotRecognizeSelector:方法,抛出异常

  3. 如果返回了响应的方法签名,则会将签名封装到一个NSInvocation的对象aInvocation中,并将aInvocation传递到forwordInvocation:方法中,进行最后处理

  4. forwordInvocation:方法中可以为aInvocation设置方法调用者,方法名,参数等,并执行指定的方法

     - (void)forwordInvocation:(NSInvocation *)aInvocation {
         // NSInvocation 封装了一个方法调用,包括:方法签名、方法名、方法参数
         aInvocation.target = [Cat new]; // 方法调用者
         // aInvocation.selector // 方法名
         // 方法参数,index从2开始, 0:receiver, 1:selector
         // [aInvocation setArgument:NULL atIndex:]      
         // 方法的调用执行
         [aInvocation invoke];
     }
    

super

[super message]的底层实现:

  • 消息接受者仍然是子类对象

  • 从父类开始查找方法的实现

     [super message]
     
     struct aaa = {
         self, // 消息接收者
         SuperClass; // 从哪个类开始搜索
     }
     
     /// aaa 就是 self,调用的是会找到[Student class].superclass
     objc_msgSendSuper(aaa, sel_registerName("message"));
     
     真是用的是objc_msgSendSuper2,
     在 objc_msgSendSuper2 中要求结构体是:
     struct aaa = {
         self, // 消息接收者
        CurrentClass; // 本身类
     }
     在 在objc_msgSendSuper2 中会对第二个变量取 superclass, 结果是一样的
    

面试题

1. 讲一下 OC 的消息机制

OC 中的方法调用其实都是转成了`objc_msgSend`函数的调用,给`receiver`方法调用者)发送了一条消息(selector方法名)
`objc_msgSend`底层有三大阶段:消息发送,动态方法解析,消息转发
    (这里可以详细说一下三大流程)

2. 什么是 Runtime?平时项目中有用过吗?

Runtime:
OC 是一种动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC 的动态性就是由 Runtime 来支撑和实现的,Runtime 是一套 C 语言的 api,封装了很多动态性相关的函数
平时编写的 OC 代码,底层都是转换成了 Runtime API 进行调用
    
具体应用:
利用关联对象给分类添加属性
遍历类的所有成员变量(修改 textfield 的 placeholder 颜色,自动归档解档)
交换方法实现(交换一些系统的方法)
利用消息转发机制,解决方法找不到的异常问题
.....

3. 判断打印结果

/// 父类
@interface Person: NSObject @end
@implemention Person @end
    
/// 子类
@interface Student: Person @end
@implemention Student
    
- (instancetype)init {
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]); // Student
        NSLog(@"[self superclass] = %@", [self superclass]); // Person
        
        NSLog(@"[super class] = %@", [super class]); // Student
        NSLog(@"[super superclass] = %@", [super superclass]); // Person
    }
    
    return self;
}
    
@end
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342