OC-Runtime-Class结构和OC消息机制

OC - Runtime - Class 结构 和 OC 消息机制

Runtime 源码中 Class 结构如下:

// Class 其实就是一个 struct objc_class *
typedef struct objc_class *Class;

// struct objc_class 继承 objc_object
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

// objc_object 结构如下
struct objc_object {
    isa_t isa;
}

所以Class本身结构如下:

struct objc_class {
    isa_t isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

Class 内部 (bits & FAST_DATA_MASK) 可以得到 (class_rw_t *) 类型,
其内部结构如下

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

其中结构 class_ro_t 结构如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    // strong修饰的ivars
    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    
    // weak修饰的ivars
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

这几个类型关系如下图

Class

class_rw_t 和 class_ro_t

class_rw_t 里面的 methods、properties、protocols 是二维数组、是可读可写的,包含了类的初始内容,分类的内容等


class_rw_t

class_ro_t 里面同样也包含了 baseMethodList、baseProtocols、baseProperties 信息,他们是一维数组,是不可读写的,他们包含的是 Class 最原始的方法列表信息


class_ro_t

method_t

在 class_rw_t 和 class_ro_t 中都包含 class 的各种方法协议信息,以方法为例,其类型为 method_t.

  • method_t 是对方法/函数的封装,结构如下
struct method_t {
    SEL name;               // 函数名
    const char *types;      // 编码(返回值类型,参数类型)
    IMP imp;                // 指向函数的指针(函数地址)
}
  • IMP 是指向函数具体实现的指针。定义id (*IMP)(id, SEL, ...)

在 OC 底层的方法调用,都会转化为 C 语言的函数调用,所有的函数都有两个默认的参数 self、SEL 其他参数才是用户定义方法设置的参数,这也是在对象方法内我们能直接使用 self 的原因。

  • SEL 代表方法、函数名,定义typedef struct objc_selector *SEL;,通常叫做选择器,底层结构和 char* 类似。可以通过 @selector()sel_registerName 获得SEL

type encode

runtime 会对函数的返回值参数进行编码。具体来说,OC 对象的方法,在Runtime底层会转化为id (*IMP)(id, SEL, ...)类型指针。

以get方法为例- (NSString *)name; 其编码为@16@0:8
以无返回值方法为例- (void)name;其编码为v16@0:8

以上两个方法对应的转换为IMP函数:NSString* name(id self, SEL _cmd)void name(id self, SEL _cmd) 其编码都是一样的。

编码的规则示例如下:

type encode

官方说明: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html

方法缓存 cache_t

Class 内部结构有个方法缓存cache_t, 用散列表来缓存曾经调用过的方法,可以提高查找速度。

cache_t

散列表有一个起始长度值,会使用使用到的@selector(sel) & _mask来缓存具体的方法地址。

当列表的容量不够的时候会扩容,直接扩大为原来的2倍。

objc_msgSend执行流程

消息发送机制的执行流程主要分为3大阶段

  1. 消息发送阶段 -> 对象方法在Class中找,类方法在 MetaClass 中找,一层层往上找
  2. 动态方法解析 -> 系统找不到方法,会给一个机会添加动态方法
  3. 消息转发,如果此步再没有操作,就会报错 unrecognized selector sent to instance

objc_msgSend 函数在 Runtime 的代码中是直接使用混编语言实现的,可以从源码中查到其查找方法流程: 缓存 -> 自己的方法列表 -> 遍历父类的缓存 & 方法列表。整个流程如果都没有,就进入查找动态方法阶段。

impLookupOrForword

如果都没找到,进入动态方法解析,动态方法解析流程如下。动态方法会根据 + (BOOL)resolveInstanceMethod:(SEL)sel; 查看内部有没有给对应的 selector 动态添加方法实现,如果有就重新走消息发送流程。否则进入下一阶段:消息转发。

动态解析

如果动态解析还是什么都没有操作,就会进入消息转发阶段。消息转发阶段流程如下,这里是闭源的无法从源码上查看,但是可以从代码角度验证流程。


消息转发
#pragma mark -  消息转发阶段 - 1
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSObject objectSpecifier];
    }
    return [super forwardingTargetForSelector:aSelector];
}


#pragma mark -  消息转发阶段 - 2
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 这里如果没有操作就会走找不到方法。
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 对 invocation 做需要做的事情
}

面试题

  • 简述 OC 对象的消息机制。
1. OC 中的方法调用其实都是转化成了 objc_msgSend 函数的调用,给receiver发送一条消息
2. objc_msgSend 函数内部有3大阶段。
    1. 消息查找阶段
    2. 动态消息解析阶段
    3. 消息转发阶段
  • 消息转发机制流程
1. 调用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,如果有实现,且返回转发的对象,就将消息转发给该对象。反之,进入第二步骤
2. 调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法,如过有针对 aSelector 的方法签名就继续调用 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法去实现任何自己想实现的逻辑。反之进入第三步
3. 调用 doesNotRecognizeedSelector 方法,向外抛出异常,经典的 unrecognized selector sent to instance 错误
  • Runtime 是什么,项目中用到过吗?
是什么:
OC 是一门动态性比较强的语言,允许很多操作推迟到程序运行时候再进行。
OC 的动态性就是由 Runtime 来支撑和实现的,Runtime 是一套C语言API,封装了很多动态性相关的函数。
平时编写的OC代码,底层都是转换成了 Runtime API 进行调用。

具体应用:
1. 利用关联对象 (AssociatedObject) 给分类添加属性,创建便利分类,如按钮直接通过block处理事件
2. 遍历类的成员变量 (修改textfield的站位文字颜色、字典转模型、自动归档等)
3. 交换方法实现、例如交换系统方法,eg:自己写过的一个监听工具,hook系统实现,添加自己逻辑
4. 利用消息转发机制解决方法找不到的问题

下面代码输出什么?

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

// 答案: 都是 Son
// 原因: super 是编译器特性,会被转成 objc_msgSendSuper 函数,其真实接收者仍是 self,class 方法是在 NSObject 中实现的,最终在消息查找会走到基类的 class 方法,返回的是消息接收者的类型,即 self 的类型。
  • super 关键字详解
一个Person 类,其 init 方法如下:
- (instancetype)init
{
    self = [super init];
    return self;
}

// 使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp 转化为 C++ 代码如下

static instancetype _I_Person_init(Person * self, SEL _cmd) {
    self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"));
    return self;
}

// [super init]; 简化如下
struct __rw_objc_super { 
    struct objc_object *object;        // 真正的消息接受者 
    struct objc_object *superClass;    // 接收者父类,作为第一个查找的类。
};

// __rw_objc_super 结构体
__rw_objc_super objc_super = (__rw_objc_super){
    (id)self,
    (id)class_getSuperclass(objc_getClass("Person"))
};
    
// 实际上转化为此类型函数指针 (Person *(*)(__rw_objc_super *, SEL))
objc_msgSendSuper(objc_super, sel_registerName("init"));

super 关键字总结

  1. super 关键字会转化为 objc_msgSendSuper(),函数.
  2. 其中会指定,消息接受者,从哪个类开始查找,和要发送的消息。
  3. super 就是要在当前对象的父类开始查找消息的实现。

--- end ---

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

推荐阅读更多精彩内容