本文整理了下 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_class
或 class_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
的自定义控制器做 window
的 rootViewController
,系统提供的 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
参考:
Objective-C Runtime Programming Guide
Understanding the Objective-C Runtime
Video Tutorial: Objective-C Runtime