Runtime 消息发送的相关知识

508 阅读11分钟

isa 指针

isa 里面的数据结构

我们知道,当对一个对象发送消息的时候,对象会使用自己的 isa 指针找到所属的类,然后在类里面查找方法实现。所以 isa 指针应该是存储了和类地址相关信息的,但是如果查看 objc 源代码(我这里使用的是 objc4-781)会发现对象数据结构里面的 isa 指针是这样的:

objc-private.h 文件里面:

struct objc_object {
private:
    isa_t isa; // isa 指针,是一个 union 类型

public:

// more ...
// more ...
// more ...

isa_t 的定义:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;          // 类信息
    uintptr_t bits;     // 存放具体的内容
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

isa.h 文件里面查看 isa 具体的 ISA_BITFIELD 解释信息(目前拷贝出来的是 arm64 架构的类型)

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   define RC_ONE   (1ULL<<45)
#   define RC_HALF  (1ULL<<18)

我们知道在arm64架构上,isa 指针的大小是 64 位,也就是8字节。但是我们在 isa_t 的结构中又看到里面 结构体struct { ISA_BITFIELD; // defined in isa.h }; 似乎存放了很多的内容,它哪放的下呢?这是因为 isa 使用的是 union 数据结构,

简单的来说就是它有共有内存的意思,然后在64位的内存中,里面的存储以位为单位,比如 nonpointer 在isa指针的64位内存中占用1位,而且 结构体 struct { ISA_BITFIELD; // defined in isa.h }; 里面它实际是不存内容的,它的目的主要是告诉你里面的内存布局是怎样的。 具体的 isa 信息存放在 uintptr_t bits 这个家伙里面。当对具体的数据进行存储或者取出的时候使用类似 &(与),|(或),!(非)的操作,再取出在64位内存里面某个范围里面的信息。

isa 里面存储的内容

  • nonpointer 如果为 0,那么表示 isa 指针此时直接指向类或者元类;1表示 isa 指针经过优化,里面使用位域存放更多信息

  • has_assoc 表示是否有设置过关联对象,如果没有,内存释放的时候会更快(主要是对象内存释放的时候会判断一下,这个如果要看在原代码里面也是能看到的)

  • has_cxx_dtor 释放有c++析构函数

  • shiftcls 存储这 Class 或者 Meta_Class的内存地址

  • magic 调试的时候分辨对象是否未完成初始化

  • weakly_referenced 是否有设置过弱引用

  • deallocating 对象是否正在释放中

  • extra_rc 存放引用计数(目前分配的是 19 位,所以其实还是能存储很多的)

  • has_sidetable_rc 引用计数的值释放已经在isa指针里面存满了,如果为1,那么引用计数会存放在一个SideTable类的属性里面

所以在 isa 这个 union 共用体里面其实存放了很多的信息。

方法的查找

在 Objective-C 里面,消息的发送大致有3个阶段:

  • 方法查找

  • 消息动态解析

  • 消息转发

一个经典的方法查找图:

类里面相关数据结构

在文件中objc-runtime-new.h

可以看到方法底部是一个这样的结构体:

struct method_t {
    SEL name;            // 函数名
    const char *types;   // 编码(返回值类型,参数类型)
    MethodListIMP imp;   // 函数实现地址
};

方法的编码类型可以在这查看详情:Type Encodings

Class 类里面存储的信息:

struct objc_class : objc_object {
    Class ISA;                 // isa 指针
    Class superclass;          // 父类指针
    cache_t cache;             // 存放缓存方法等
    class_data_bits_t bits;    // 具体类信息
    // ... more
}

class_data_bits_t 结构体:

   class_rw_t // 可读可写信息
   class_ro_t // 只读信息

class_rw_t:

   method_array_t  // 方法列表相关(是一个二维数组,包含分类的方法和基本类的方法)
   property_array_t // 属性列表相关
   protocol_array_t // 协议列表相关
   
   // ... more

class_ro_t 存放基本类信息:

    const uint8_t * ivarLayout;
    
    const char * name; 
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars; // 成员变量列表(可以看出成员变量是在一个只读的数据结构中,这也是为什么类被注册到内存中以后就不能添加成员变量了)

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    
    // ... more

方法查找流程

在 Objective-C 里面,对一个对象发送消息的流程是这样的:

方法缓存 cache_t 的数据结构

在上面我们看到,对象的方法查找有一个 cache_t 的东西,它缓存了对象调用过的相关方法,它使用散列表提高查找效率

cache_t 大致的数据结构:

    <struct bucket_t *> _buckets   // 散列表 [bucket_t, bucket_t, bucket_t...]
    <mask_t> _mask                 // 散列表长度-1
    uint16_t _occupied             // 已经缓存方法的数量

bucket_t:

   _sel // 方法名为key
   _imp // 方法实现地址为value

在objc源代码中,可以找到缓存方法的插入:

如果缓存容器满了,会直接把之前的缓存方法清空,缓存容量内存加倍。

动态方法解析

我们知道,对一个对象发送消息,它的方法查找流程是这样的 object -> superClass - > superClass ... -> NSObject 。但是如果直到 NSObject 都没有找到的话,那么它会进入方法动态解析

在动态方法解析阶段,开发者可以动态为方法添加实现。

动态方法解析包括:

  • + (BOOL)resolveInstanceMethod:(SEL)sel 对象方法的动态解析

  • + (BOOL)resolveClassMethod:(SEL)sel 类方法的动态解析

实现动态解析一个对象方法的例子:

@implementation Cat

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(eat)) {
        Method method = class_getInstanceMethod(self, @selector(lotOfFish));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method)); // 动态指定 eat 方法的实现地址
        return true;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)lotOfFish{
    NSLog(@"%s", __func__);
}

@end

测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        [cat performSelector:@selector(eat)]; // cat 没有 eat 方法,或者没有实现 
    }
    return 0;
}

输出:

2021-01-17 11:18:07.157671+0800 Runtime[66738:1460934] -[Cat lotOfFish]

可以看到,由于 cat 对象没有 eat 方法,所以执行 eat 方法的时候方法查找没找到,执行对象的动态方法解析 + (BOOL)resolveInstanceMethod:(SEL)sel,给对象添加了对于方法的实现。

类方法的动态解析流程和对象的是一样的,添加自定义的方法实现就好了。

需要注意的是,添加了动态方法解析以后,对象会再走一遍消息查找

大致的流程图如下所示:

消息转发

那如果动态方法解析没有实现呢?此时就会来到消息转发阶段了。在消息转发阶段,开发者可以:

  • 把方法转发给其它的对象来处理;

  • 捕获方法,添加方法签名,之后还可以再方法转发(或者什么都不做)。

把方法转发给其它的对象来处理消息

添加一个 Dog 类:

@interface Dog : NSObject

- (void)eat;

@end

@implementation Dog

// Dog 包含 eat 方法
- (void)eat {
    NSLog(@"Eat dog food");
}

@end

Cat 类里面实现消息转发:


@implementation Cat

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(eat)) {
       // 消息转发给 dog 对象处理 
        return [[Dog alloc] init];
    }
    // 其它的逻辑不变
    return [super forwardingTargetForSelector:aSelector];
}

@end

测试代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        // Cat 没有 eat 方法
        [cat performSelector:@selector(eat)];
    }
    return 0;
}

输出:

2021-01-17 11:49:20.044035+0800 Runtime[67857:1479538] Eat dog food

可以看到,cat 对象执行 eat 方法的时候,由于 cat 对象没有该方法,而且它的动态方法解析阶段也没过添加自己的方法实现,所以此时会来到消息转发阶段的 - (id)forwardingTargetForSelector:(SEL)aSelector。在这个方法里面可以把捕捉的方法转发给其它对象来执行。

添加方法签名 & 处理

如果在第一步的消息转发阶段没有实现 - (id)forwardingTargetForSelector:(SEL)aSelector方法,那么之后会调用方法:

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

  2. - (void)forwardInvocation:(NSInvocation *)anInvocation

Cat 测试类添加实现代码:

@implementation Cat

// 方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(eat)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 参数类型可以查看苹果的官网介绍
    }
    return [super methodSignatureForSelector:aSelector];
}

// 签名后的查看
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 这里可以拦截所有这个类的找不到的方法,这样就可以消息找不到的时候也不崩溃了
    NSLog(@"没有实现方法: %s", anInvocation.selector);
}

@end

测试:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        [cat performSelector:@selector(eat)];
    }
    return 0;
}

输出:

2021-01-17 11:57:40.963118+0800 Runtime[68203:1486085] 没有实现方法: eat

消息转发的大致流程如下:

一些 objc API 的介绍

类相关

动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls) 

销毁一个类
void objc_disposeClassPair(Class cls)

获取isa指向的Class
Class object_getClass(id obj)

设置isa指向的Class
Class object_setClass(id obj, Class cls)

判断一个OC对象是否为Class
BOOL object_isClass(id obj)

判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

获取父类
Class class_getSuperclass(Class cls)

一个简单的测试:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class Cat = objc_allocateClassPair([NSObject class], "Cat", 0); // 注册一个类和它的元类
        class_addIvar(Cat, "_age", 4, 1, @encode(int)); // 添加成员变量(必须在类的内存注册之前)
        objc_registerClassPair(Cat); // 注册到内存
        
        id cat = [[Cat alloc] init];
        [cat setValue:@5 forKey:@"_age"];
        
        NSLog(@"cat age = %@", [cat valueForKey:@"_age"]); // 输出 cat age = 5
    }
    return 0;
}

成员变量

获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

属性

获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                  unsigned int attributeCount)

动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                      unsigned int attributeCount)

获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

方法

获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name) 
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2) 

拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

补充

isKindOfClass 和 isMemberOfClass 的区别

这个我们其实可以到源代码里面查看一下内部实现:

  • 对于类对象:

+ (BOOL)isMemberOfClass:(Class)cls 是找到自己的 isa 指针的指向类,也就是自己元类

+ (BOOL)isKindOfClass:(Class)cls 是找到自己的 isa 指针的指向类,也就是自己元类,然后如果找不到,再遍历自己的父类。所以它会一直到 NSObject

  • 对于实例对象:

- (BOOL)isMemberOfClass:(Class)cls 是找自己的 class 属性得到所属类,然后对比当前类

- (BOOL)isKindOfClass:(Class)cls 是找自己的 class 属性得到所属类,然后对比当前类,如果不是,再遍历父类,直到 NSObject

测试一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 类
        NSLog(@"NSObject isKindOfClass NSObject: %d", [NSObject isKindOfClass:[NSObject class]]); // 1
        NSLog(@"NSObject isMemberOfClass NSObject: %d", [NSObject isMemberOfClass:[NSObject class]]); // 0
        NSLog(@"Cat class isKindOfClass Cat: %d", [Cat isKindOfClass:[Cat class]]);  // 0
        NSLog(@"Cat class isMemberOfClass Cat: %d", [Cat isMemberOfClass:[Cat class]]); // 0

        NSLog(@"------------------");
        
        // 对象
        // 继承关系 Cat: NSObject
        Cat *cat = [[Cat alloc] init];
        NSLog(@"cat instance isKindOfClass NSObject: %d", [cat isKindOfClass:[NSObject class]]);  // 1
        NSLog(@"cat instance isMemberOfClass NSObject: %d", [cat isMemberOfClass:[NSObject class]]); // 0
        NSLog(@"cat instance isKindOfClass Cat: %d", [cat isKindOfClass:[Cat class]]);  // 1
        NSLog(@"cat instance isMemberOfClass Cat: %d", [cat isMemberOfClass:[Cat class]]); // 1
    }
    return 0;
}

输出:

2021-01-17 20:10:12.286409+0800 Runtime[76526:1658908] NSObject isKindOfClass NSObject: 1
2021-01-17 20:10:12.286671+0800 Runtime[76526:1658908] NSObject isMemberOfClass NSObject: 0
2021-01-17 20:10:12.286703+0800 Runtime[76526:1658908] Cat class isKindOfClass Cat: 0
2021-01-17 20:10:12.286723+0800 Runtime[76526:1658908] Cat class isMemberOfClass Cat: 0
2021-01-17 20:10:12.286741+0800 Runtime[76526:1658908] ------------------
2021-01-17 20:10:12.286760+0800 Runtime[76526:1658908] cat instance isKindOfClass NSObject: 1
2021-01-17 20:10:12.286780+0800 Runtime[76526:1658908] cat instance isMemberOfClass NSObject: 0
2021-01-17 20:10:12.286798+0800 Runtime[76526:1658908] cat instance isKindOfClass Cat: 1
2021-01-17 20:10:12.286815+0800 Runtime[76526:1658908] cat instance isMemberOfClass Cat: 1

super 关键字

添加如下测试:

// --------------- Animal ---------------

@interface Animal : NSObject

- (void)eatFood;

@end

@implementation Animal

- (void)eatFood {
    NSLog(@"Animal eat");
}

@end

// --------------- Cat ---------------

@interface Cat : Animal

@end

@implementation Cat

- (void)eatFood {
    [super eatFood];
    
    NSLog(@"Cat eat");
}

@end

测试运行:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        [cat eatFood];
    }
    return 0;
}

输出:

2021-01-17 14:10:42.039886+0800 Super[71153:1538568] Animal eat
2021-01-17 14:10:42.040163+0800 Super[71153:1538568] Cat eat

这只是一个很简单的继承和方法的调用,符合预期的执行效果。

再看下面情况:

@interface Cat : Animal

- (void)sleep;

@end

- (void)sleep {
    NSLog(@"[self class] = %@", [self class]);
    NSLog(@"[self superclass] = %@", [self superclass]);
    NSLog(@"[super class] = %@", [super class]);
    NSLog(@"[super superclass] = %@", [super superclass]);
}

测试:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Cat *cat = [[Cat alloc] init];
        [cat sleep];
    }
    return 0;
}

输出:

2021-01-17 14:14:14.480170+0800 Super[71285:1540874] [self class] = Cat
2021-01-17 14:14:14.480781+0800 Super[71285:1540874] [self superclass] = Animal
2021-01-17 14:14:14.480816+0800 Super[71285:1540874] [super class] = Cat
2021-01-17 14:14:14.480838+0800 Super[71285:1540874] [super superclass] = Animal

什么情况? 在 Cat 类中输出情况是:[super class] = Cat[super superclass] = Animal

我们知道,在 runtime 的消息发送走的是 objs_msgSend 这个家伙。 一个对象的消息发送流程是如果一个对象在所属类中找不到方法,那么会到父类里面查找,然后一直到 NSObject为止。那么为什么在cat对象的方法调用里面会是:[super class] = Cat 而不是:[super class] = Animal 呢?

简化代码,查看 [super eatFood] 干了什么:

- (void)eatFood {
    [super eatFood];
}

执行命令 clang -rewrite-objc Cat.m -o Cat.cpp, 转成 c++ 代码,查看底层实现(当然,具体的OC代码的底层肯定不是这样的,但是它基本的逻辑实现是差不多的)

c++ 的代码中,找到 eatFood 方法的实现,可以发现是这样一个东西:

static void _I_Cat_eatFood(Cat * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Cat"))},
    sel_registerName("eatFood"));
}

我们发现执行 super 方法调用执行的是 objc_msgSendSuper 方法,方法里面接收两个参数 __rw_objc_super 结构体eatFood 方法__rw_objc_super 结构体接收了两个参数:第一个是当前对象,在我们的例子中也就是 cat 对象,第二个参数是当前类的父类,也就是Cat类的父类,为Animal类。

__rw_objc_super 的数据额结构:

struct __rw_objc_super { 
	struct objc_object *object;         // 消息接收者
	struct objc_object *superClass;     // 消息接收者的父类
};

所以在Cat类的对象方法里面调用 [super class] 的消息接收者还是为 cat 对象,只不过它的消息查找从父类开始,跳过了Cat类。所以我们会发现:

NSLog(@"[super class] = %@", [super class]);           // 输出:[super class] = Cat
NSLog(@"[super superclass] = %@", [super superclass]); // 输出:[super superclass] = Animal

当然我们也可以在 objc 原代码,查看里面的 objc_msgSendSuper 方法的实现:

(我这边下载的是 objc4-781 源代码)

objc_msgSendSuper 方法的接口介绍:

/** 
 * Sends a message with a simple return value to the superclass of an instance of a class.
 * 
 * @param super A pointer to an \c objc_super data structure. Pass values identifying the
 *  context the message was sent to, including the instance of the class that is to receive the
 *  message and the superclass at which to start searching for the method implementation.
 * @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
 * @param ...
 *   A variable argument list containing the arguments to the method.
 * 
 * @return The return value of the method identified by \e op.
 * 
 * @see objc_msgSend
 */
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

objc_super 结构体:

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};

可以看出,里面的实现虽然和我们的c++代码有点差异,当时它具体的方法接收者还是当前的对象,然后方法的查找是从父类开始查找的。