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 ---

你可能感兴趣的:(OC-Runtime-Class结构和OC消息机制)