Objective-C Runtime

本文整理了下 Objective-C Runtime 中的一些关键理论和一些应用实践,包括以下几部分:

一、简介

1、C + Runtime = Obj-C

Objective-C 是一门动态语言,它将很多可以在编译连接的工作推迟到了运行时才做,所以它不仅需要编译器来编译代码,还得需要一个运行时系统( runtime system )来执行编译后的代码。所以这个运行时系统对 Objective-C 来说就像是操作系统,有了它 Objective-C 才能正常运作。
Objective-C Runtime 是一个 Runtime 库,主要是用 C 和汇编写的,这个库让基于 C 的 Objective-C 有了面向对象的能力。
可以在 这里 下载到 Runtime 的源码。

2、与 Runtime 交互

我们在使用 Objective-C 的过程中都无时无刻不在通过以下三种方法与 Runtime 系统进行交互:

(1)使用 Objective-C 的源代码

我们在写 Objective-C 代码的时候,其实 Runtime 系统就自动地默默在幕后工作着。如在编译 Objective-C 中类和方法时,编译器都是将其转换为数据结构和函数。

(2)使用 NSObject 的方法

Cocoa 中大多数的类都继承自 NSObject 类,也就继承了它的方法。NSObject 的有些方法纯粹是用来获取 Runtime 系统的信息的,那些方法让对象拥有自省的能力。如: class 方法是返回某个对象所属的类;isKindOfClass:isMemberOfClass: 是检查对象是否在某个继承体系中;respondsToSelector: 是检查对象是否能接受并响应某个信息;conformToProtocol: 是检查对象是否遵守了某个协议;methodForSelector: 是返回某个方法的具体实现的地址。

(3)使用 Runtime 函数

Runtime 是一个由一系列函数和数据结构组成的动态共享库( dynamic shared library ),并提供了一些公开的接口。所以我们也可以用纯 C 的代码来实现编译器编译 OC 代码后的效果。虽然大多数函数我们一般都是用不上的,但有时在某些情景下使用还是很有帮助的。

二、相关术语

当在 OC 中使用方法 是这样的:

1
[self doSomething];

实际上会被转化为调用 objc_msgSend 函数,给某个对象发送消息:

1
objc_msgSend(self, @selector(doSomething));

它的声明是这样的:

1
// message.h
2
id objc_msgSend(id self, SEL op, ...);

那具体是怎样转换的?OC 中的类、方法、属性等在 C 语言中是怎样被表示的?下面先看下一些相关术语。

1、SEL

SEL 是转换后的函数中第二个参数的类型,它的对象 selector (方法选择器) ,顾名思义,是用来识别和选择要执行的 OC 方法的。它的定义如下:

1
// objc.h
2
typedef struct objc_selector *SEL;

其实它就是个映射到方法的 C 字符串,上面就是通过 @selector(doSomething) 来获取一个名字叫 doSomething 的 selector

2、id

id 是转换后的函数中第一个参数的类型,它在 OC 中被称为万能指针,可以指向任何类的实例。它的定义如下:

1
// objc.h
2
typedef struct objc_object *id;
3
struct objc_object {
4
    Class isa;
5
};

objc_object 结构体中的第一个元素是 isa 指针,根据它可以找到对象所属的类。(但要注意在 KVO 中 isa 指针指向的是一个中间类了)

3、Class

上面说的 isa 的类型是 Class,而它的定义如下:

1
// objc.h
2
typedef struct objc_class *Class;
3
4
// runtime.h
5
struct objc_class {
6
    Class isa;
7
8
    Class super_class;
9
    const char *name;
10
    long version;
11
    long info;
12
    long instance_size;
13
    struct objc_ivar_list *ivars;
14
    struct objc_method_list **methodLists;
15
    struct objc_cache *cache;
16
    struct objc_protocol_list *protocols;
17
};

可见在 Runtime 系统中,一个类还关联了它的父类指针、类名、成员变量、方法、缓存、协议。
注意到不仅表示对象的 objc_object 结构体中有个 isa 指针,表示类的 objc_class 结构体中也有个 isa 指针,这是因为在 OC 中,类本身也是一个对象(类对象)。对象的方法存储在它所属的类中,那类的方法呢?这时就需要类对象所属的类来存储类方法了,它叫 meta class(元类)。对象的类、父类、元类之间的关系如下(实现是 super_class 指针,虚线是 isa 指针):

注意到所有的元类的元类都是 root class(meta),而这个根元类的元类是它自己,它的父类是 NSObject;NSObject 的元类也是那个根元类,但它没有父类。

4、成员变量

其中 objc_ivar_list 是成员变量列表,定义如下:

1
// runtime.h
2
struct objc_ivar_list {
3
    int ivar_count;
4
    int space;
5
    struct objc_ivar ivar_list[1];
6
}
7
struct objc_ivar {
8
    char *ivar_name;
9
    char *ivar_type;
10
    int ivar_offset;
11
    int space;
12
13
typedef struct objc_ivar *Ivar;

可见成员变量列表 objc_ivar_list 结构体存储着由成员变量 objc_ivar 结构体组成的数组,objc_ivar 结构体存储着单个成员变量的名字、类型、偏移量等信息。

5、方法

objc_method_list 是方法列表,定义如下:

1
// runtime.h
2
struct objc_method_list {
3
    struct objc_method_list *obsolete;
4
    int method_count;
5
    int space;
6
    struct objc_method method_list[1];
7
} 
8
struct objc_method {
9
    SEL method_name;
10
    char *method_types;
11
    IMP method_imp;
12
13
typedef struct objc_method *Method;

可见方法列表 objc_method_list 结构体存储着由方法 objc_method 结构体组成的数组,objc_method 结构体存储着单个方法的信息:名称(SEL类型的)、参数类型和返回值类型(method_types中)和具体实现(IMP类型的)。

6、IMP

IMP(method implementation,方法实现) 的定义是:

1
// objc.h
2
typedef id (*IMP)(id, SEL, ...);

所以它其实是一个函数指针,指向某个方法的具体实现。它的类型和 objc_msgSend 函数相同,参数中也都包含有 id 和 SEL 类型,这是因为一个 id 和 一个 SEL 参数就能确定唯一的方法实现地址。

7、Cache

objc_class 结构体中还有个指向 objc_cache 结构体的指针,它的定义如下:

1
// runtime.h
2
typedef struct objc_cache *Cache
3
// objc-cache.m
4
struct objc_cache {
5
    // 当前能达到的最大 index
6
    uintptr_t mask;          
7
    // 被占用的槽位。因为缓存是以散列表的形式存在,所以会有空槽
8
    uintptr_t occupied;        
9
    // 用数组表示的 hash 表
10
    cache_entry *buckets[1];
11
};
12
typedef struct {
13
    SEL name;    
14
    void *unused;
15
    IMP imp;  
16
} cache_entry;
17
// _uintptr_t.h
18
typedef unsigned long uintptr_t;

所以它用来做缓存的,用 buckets 数组来存储被调用过的方法。因为一个方法被调用过,那它以后有可能还会被调用,所以将其存储起来,下次要找某方法先到缓存中找,如果找到的话,免去后面的寻找过程,速度虽然仍会比直接调用函数慢一点点,但已经有很大提升。

8、属性

还有我们常用的属性其实也是结构体,它的定义如下:

1
// runtime.h
2
typedef struct objc_property *objc_property_t;
3
typedef struct {
4
    const char *name;
5
    const char *value;
6
} objc_property_attribute_t;
7
8
// objc-runtime-new.h
9
typedef struct objc_property {
10
    const char *name;
11
    const char *attributes;
12
} property_t;
13
typedef struct property_list_t {
14
    uint32_t entsize;
15
    uint32_t count;
16
    property_t first;
17
} property_list_t;

所以一个 property_t 结构包含了属性的名称和属性字符串。与属性相关的一些方法如下:

1
#define newproperty(p) ((property_t *)p)
2
// 返回协议中的属性列表,属性个数存储在参数 outCount 中
3
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
4
// 返回类中的属性列表,属性个数存储在参数 outCount 中
5
objc_property_t *class_copyPropertyList(Class cls_gen, unsigned int *outCount)
6
// 返回属性列表中的属性数组,属性个数存储在参数 outCount 中
7
static property_t **copyPropertyList(property_list_t *plist, unsigned int *outCount)
8
// 返回类中的特定名字的属性
9
objc_property_t class_getProperty(Class cls_gen, const char *name)
10
// 返回某个属性的名字
11
const char *property_getName(objc_property_t prop)
12
// 返回某个属性的属性字符串
13
const char *property_getAttributes(objc_property_t prop)

三、objc_msgSend

使用某对象的方法,都是给这个对象发送消息,消息和方法实现直到运行时才会绑定。Runtime 系统会把使用方法转换为调用函数:

1
objc_msgSend(receiver, selector)

注意到此时函数多了两个参数:消息接收者、方法的 selector。这是每个方法调用时都会默认存在的隐藏参数。如果还有其他参数则是:

1
objc_msgSend(receiver, selector, arg1, arg2, ...)

objc_msgSend 要做的事件有三件:(1)找到 selector 对应的方法实现;(2)调用该方法实现,并把消息接收者(如果有参数则加上那些参数)传给它;(3)把方法实现的返回值传回去(它自己并没有任何返回值)。
其中第(1)件事的最关键的,具体过程如下:

(1)检查该 selector 是不是要忽略的;
(2)检查这个 target 是否为 nil。在 OC 中给 nil 发送任何消息都不会出错,返回的结果都是 0 或 nil。
(3)开始找这个类的 IMP。先在 cache 中找,找到则调到对应的方法实现中去执行。
(4)在 cache 中没找到,则在该类的方法分发表(dispatch table,即方法列表)中找,找到则执行。
(5)在该类的方法分发表中找不到,则到父类的分发表中找,再找不到则往上找,直到 NSObject 类为止。这两个过程的示意图如下:

如果找到,还会根据是否把消息传给父类、返回值是否数据结构而选择下面四个函数中的一个来调用:

1
// message.h
2
id objc_msgSend(id self, SEL op, ...)
3
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
4
void objc_msgSend_stret(id self, SEL op, ...)
5
void objc_msgSendSuper_stret(struct objc_super *super, SEL op, ...)

函数中的 super 关键字是指向 objc_super 结构体的指针,objc_super 结构体定义如下:

1
struct objc_super {
2
    // receiver 仍是 self 本身
3
    __unsafe_unretained id receiver;
4
    // 父类的类型
5
    __unsafe_unretained Class super_class;
6
};

要注意的是如果想获取某个类的父类,要用 cls->super_classclass_getSuperclass 方法,而不应该用 [super class],因为 [super class] 会变为 objc_msgSend(objc_super->receiver, @selector(class)),即获得的是 objc_super->receiver 的类,跟 [self class] 的结果是一样的。
(6)动态方法解析(Dynamic Method Resolution):如果该类及其继承体系的分发表都没找到,则开始动态方法解析,这是 Runtime 系统在报错前给我们的第一次补救的机会,它会调用 resolveInstanceMethod: 或者 resolveClassMethod: 方法,所以我们可以在这两方法中分别用 class_addMethod 给某个类或对象的某个 selector 动态添加一个方法实现。

如在 main 函数中调用 Person 对象的一个 aMethod 方法:

1
    Person *p = [[Person alloc] init];
2
    [p aMethod];

它的 .h 和 .m 文件如下:

1
//  Person.h
2
#import <Foundation/Foundation.h>
3
@interface Person : NSObject
4
- (void)aMethod;
5
@end
6
7
//  Person.m
8
#import "Person.h"
9
#import <objc/runtime.h>
10
11
// 要被动态添加的方法实现
12
void dynamicMethodIMP(id self, SEL _cmd) {
13
    NSLog(@"dynamicMethodIMP");
14
}
15
16
@implementation Person
17
// 动态方法解析
18
+ (BOOL)resolveInstanceMethod:(SEL)sel {
19
    // 如果是要被添加方法实现的 selector
20
    if (sel  == @selector(aMethod)) {
21
        // 给 self 的类的 sel 方法选择器动态添加方法实现 dynamicMethodIMP
22
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
23
        // 返回 YES 后, Runtime 重新给对象发送 aMethod 消息,这次就可以找到 dynamicMethodIMP 方法实现并调用它了
24
        return YES;
25
    }
26
    return [super resolveInstanceMethod:sel];
27
}
28
29
@end

(7)重定向:如果在上面的方法中不做处理或返回 NO,Runtime 系统在报错前还会给第二次补救机会,就是会调用 forwardingTargetForSelector: 方法索要一个能响应这个消息的对象,所以我们可以在这里返回另外一个能处理该消息的对象:

1
//  Person.m
2
- (id)forwardingTargetForSelector:(SEL)aSelector {
3
    // 如果是要被添加方法实现的 selector
4
    if (aSelector == @selector(aMethod)) {
5
        // 返回另外一个对象,让它去接收该消息
6
        return [[Car alloc] init];
7
    }
8
    return [super forwardingTargetForSelector:aSelector];
9
}

上面返回的是一个 Car 对象,如果 Car 类定义如下:

1
//  Car.h
2
#import <Foundation/Foundation.h>
3
@interface Car : NSObject
4
- (void)aMethod;
5
@end
6
//  Car.m
7
#import "Car.h"
8
@implementation Car
9
- (void)aMethod {
10
    NSLog(@"car aMethod");
11
}
12
@end

则输出结果就是 “car aMethod”了。

(8)消息转发:如果在上一步中不做处理或者返回 nil 或 self,则 Runtime 系统会在报错前给我们最后一次补救机会。系统会先调用 methodSignatureForSelector: 方法,在该方法返回一个包含了消息的描述信息的方法签名(NSMethodSignature对象),并用此方法签名去生成一个 NSInvocation 对象,然后调用 forwardInvocation: 方法并把刚生成的 NSInvocation 对象作参数传进去。我们就可以重写 forwardInvocation: 方法,在这里将消息转发给其他对象:

1
// 获取一个方法签名,用于生成 NSInvocation 对象
2
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
3
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
4
    if (!signature) {
5
        signature = [[Car new] methodSignatureForSelector:aSelector];
6
    }
7
    return signature;
8
}
9
10
- (void)forwardInvocation:(NSInvocation *)anInvocation {
11
    // 如果另一个对象能响应该方法
12
    if ([[Car new] respondsToSelector:[anInvocation selector]]) {
13
        // 则让另一个对象来响应该方法
14
        [anInvocation invokeWithTarget:[Car new]];
15
    } else {
16
        [super forwardInvocation:anInvocation];
17
    }
18
}

尽管消息转发的效果类似于多继承,让一个对象看起来能处理自己不拥有的方法,但 NSObject 类不会将两者混淆。如上面的例子, [p respondsToSelector:@selector(aMethod)] 的结果还是 NO

PS: 上面调用的方法顺序也可以这样获得:在程序启动之后暂停,然后在 gdb 中输入这个命令:call (void)instrumentObjcMessageSends(YES),再运行,则发送的所有消息都会打印到 /tmp/msgSend-xxxx 文件里了。如新建一个 Teacher 类,给它的实例发送一条错误的消息:

1
    Teacher *teacher = [[Teacher alloc] init];
2
    [teacher aMehtod];

则打印结果如下

四、应用

上面讲的术语和响应消息可能还是比较抽象的,下面介绍下我们在项目中用得上的一些 Runtime API:

1、自定义 tabBar

大多 App 都是使用继承自 UITabBarController 的自定义控制器做 windowrootViewController,系统提供的 tabBar 可能满足不了我们的需求,此时我们可以通过以下方法使用我们自定义的 tabBar 并布局其中的按钮:

1
//  YGMainViewController.m
2
- (void)viewDidLoad {
3
    [super viewDidLoad];
4
    //创建并使用自定义的 tabBar
5
    YGMainTabBar *mainTarBar = [YGMainTabBar new];
6
    [self setValue:mainTarBar forKey:@"tabBar"];
7
}
8
//  YGMainTabBar.m
9
- (void)layoutSubviews {
10
    [super layoutSubviews];
11
    for (UIView *subView in self.subviews) {
12
        if ([subView isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
13
            // 布局按钮
14
        }
15
    }
16
}

2、获取属性名

我们在用字典生成模型时一般是使用 - setValuesForKeysWithDictionary: 方法来赋值,并用 - setValue:forUndefinedKey: 方法来过滤掉多余的键值。我们也可以用 Runtime 提供的方法来获取某个类的共有属性名,再逐一使用 - setValue:forKey: 进行 KVC 赋值:

1
// 类方法:字典 --> 模型, KVC
2
+ (instancetype)cycleWithDict:(NSDictionary *)dict{
3
    id obj = [[self alloc] init];
4
    
5
    for (NSString *key in [self publicProperties]) {
6
        if (dict[key]) {
7
            [obj setValue:dict[key] forKey:key];
8
        }
9
    }
10
    
11
    return obj;
12
}
13
14
// 通过 runtime 方法获取所有公有属性名
15
+ (NSArray *)publicProperties{
16
    unsigned int count = 0;
17
    // 获取当前类的属性列表(即数组)
18
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
19
    
20
    NSMutableArray *ocProperties = [NSMutableArray array];
21
    for (int i = 0; i < count; i++) {
22
        // 取出每一个属性
23
        objc_property_t property = propertyList[i];
24
        // 取出属性名
25
        const char *cPropertyName = property_getName(property);
26
        // C --> OC
27
        NSString *ocPropertyName = [[NSString alloc] initWithCString:cPropertyName
28
                                                            encoding:NSUTF8StringEncoding];
29
        
30
        [ocProperties addObject:ocPropertyName];
31
    }
32
    
33
    // 释放
34
    free(propertyList);
35
    
36
    return ocProperties.copy;
37
}

3、关联属性

我们还可能希望给某些常用的类添加 category,但 category 是只能添加方法而不能添加存储属性的。现在我们可以用 Runtime 来间接在 category 添加属性了,如在给 UIButton 的 category 中添加一个属性作回调:

1
//  UIButton+Extension.h
2
#import <UIKit/UIKit.h>
3
typedef void (^CallbackBlock)();
4
@interface UIButton (Extension)
5
@property (copy, nonatomic) CallbackBlock callback;
6
@end
7
8
//  UIButton+Extension.m
9
#import "UIButton+Extension.h"
10
#import <objc/runtime.h>
11
const void *yg_callbackKey = @"yg_callbackKey";
12
@implementation UIButton (Extension)
13
14
- (void)setCallback:(CallbackBlock)callback {
15
    // 设置关联属性
16
    objc_setAssociatedObject(self, yg_callbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
17
}
18
- (CallbackBlock)callback {
19
    // 获取关联属性
20
    return objc_getAssociatedObject(self, yg_callbackKey);
21
}
22
23
@end

这样就可以把 callback 当做按钮的属性来用了:

1
//  ViewController.m
2
#import "ViewController.h"
3
#import "UIButton+Extension.h"
4
5
@interface ViewController ()
6
@property (weak, nonatomic) IBOutlet UIButton *button;
7
@end
8
9
@implementation ViewController
10
11
- (void)viewDidLoad {
12
    [super viewDidLoad];
13
14
    // 设置按钮的 callback “属性”的内容
15
    self.button.callback = ^{
16
        NSLog(@"button callback");
17
    };
18
    // 获取并执行按钮的 callback “属性”
19
    self.button.callback();
20
}
21
22
@end

我们常用的第三方库中有很多也是这样用的,如 SDWebImage 会用这样的方法来存储传进来的图片的 URL

1
// UIImageView+WebCache.m
2
- (void)sd_setImageWithURL:(NSURL *)url
3
          placeholderImage:(UIImage *)placeholder
4
                   options:(SDWebImageOptions)options
5
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
6
                 completed:(SDWebImageCompletionBlock)completedBlock {
7
    [self sd_cancelCurrentImageLoad];
8
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
9
	...
10
}
11
- (NSURL *)sd_imageURL {
12
    return objc_getAssociatedObject(self, &imageURLKey);
13
}

4、Method Swizzling

这个 Objective-C 中的“黑科技”,具体见 我之前的文章


本文用到的例子可见 我的Demo


参考:

Runtime 源代码

Objective-C Runtime Programming Guide

Understanding the Objective-C Runtime

Objective-C Runtime

Video Tutorial: Objective-C Runtime

Objective-C 格致余论 1 - Selector

objc_msgSend() Tour Part 1: The Road Map

深入理解Objective-C:方法缓存

iOS - NSInvocation的使用