本文分为4个部分
1.介绍OC和C语言之间的转换
2.介绍运行时和相关术语
3.介绍消息发送机制已及怎样找到函数实现
4.runtime的应用
一、OC和C语言之间的转换
Apple 官方对OC的定义
Objective-C is the primary programming language you use when writing software for OS X and iOS. It’s a superset of the C programming language and provides object-oriented capabilities and a dynamic runtime. Objective-C inherits the syntax, primitive types, and flow control statements of C and adds syntax for defining classes and methods. It also adds language-level support for object graph management and object literals while providing dynamic typing and binding, deferring many responsibilities until runtime
总结:OC = C+runtime+面向对象;
C语言是编译型静态语言,也就是说C语言编译完后,是不可以更改代码的结构的,如更改某个方法的实现,如调用eat(),但执行的确实run()的实现。
而OC虽然是基于C的但是因为有了runtime机制,却是编译型动态语言,这种语言可以动态的创建类,属性以及交换方法的实现,这种交换方法实现有个热门词叫Method Swizzling(俗称黑魔法)。
runtime对OC来说,类似操作系统的功能,很多方法如kvc、kvo等功能因为有运行时才得以实现。可以说 C语言和面向对象特性是OC的身体,而Runtime才是OC的灵魂。
二、运行时和相关术语
1.运行时是什么?
运行时是用Apple提供给OC的一套用C语言编写的底层API,它属于1个C语言库,平时我们编写的OC代码最终都转换成了
运行时的C语言代码。
当在 OC 中使用方法 是这样的:
[self doSomething];
实际上会被转化为调用objc_msgSend函数,给某个对象发送消息:
objc_msgSend(self, @selector(doSomething));
佐证:1、苹果官方文档
In Objective-C, messages aren’t bound to method implementations until runtime. The compiler converts a message expression,
[receiver message]
into a call on a messaging function,objc_msgSend.
我们可以在这里查看官方文档
佐证:2 编译OC代码
编译前
编译后
2、相关术语
我们通过 objc_msgSend来了解runtime的相关术语
它的声明是这样的:
// message.h
id objc_msgSend(id self, SEL op, ...);
那具体是怎样转换的?OC 中的类、方法、属性等在 C 语言中是怎样被表示的?下面先看下一些相关术语。
1、SEL
SEL是转换后的函数中第二个参数的类型,它的对象selector(方法选择器) ,顾名思义,是用来识别和选择要执行的 OC 方法的。它的定义如下:
// objc.h
typedef struct objc_selector *SEL;
其实它就是个映射到方法的 C 字符串,上面就是通过@selector(doSomething)来获取一个名字叫 doSomething 的selector。
2、id
id 是转换后的函数中第一个参数的类型,它在 OC 中被称为万能指针,可以指向任何类的实例。它的定义如下:
// objc.h
typedef struct objc_object *id;
struct objc_object {
Class isa;
};
objc_object结构体中的第一个元素是isa指针,根据它可以找到对象所属的类。(但要注意在 KVO 中 isa 指针指向的是一个中间类了,kVO利用运行时机制动态的创建了一个继承被观察类的中间类,通过对中间类属性变化,通知观察对象,这个应用我们在应用部分再详细解释)
3、Class
上面说的 isa 的类型是Class,而它的定义如下:
// objc.h
typedef struct objc_class *Class;
// runtime.h
struct objc_class {
Class isa; // 类同样有自己父类,指向所属的父类
Class super_class; // 父类指针
const char *name; // 类名
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars; // 成员变量列表
struct objc_method_list **methodLists; // 方法列表
struct objc_cache *cache; // 缓存
struct objc_protocol_list *protocols; // 协议列表
};
可见在 Runtime 系统中,一个类还关联了它的父类指针、类名、成员变量、方法、缓存、协议。
注意到不仅表示对象的objc_object结构体中有个isa指针,表示类的objc_class结构体中也有个isa指针,这是因为在 OC 中,类本身也是一个对象(类对象)。对象的方法存储在它所属的类中,那类的方法呢?这时就需要类对象所属的类来存储类方法了,它叫meta class(元类)。对象的类、父类、元类之间的关系如下(实现是 super_class 指针,虚线是 isa 指针):
注意到所有的元类的元类都是root class(meta),而这个根元类的元类是它自己,它的父类是NSObject;NSObject 的元类也是那个根元类,但它没有父类。
由上图可知 实例对象的isa都指向自己的类对象,类对象中的superClass指向自己的父类,类对象中的isa指向了元类。
这样清晰的继承关系,将会是 runtime 消息机制发送消息的基础。
4、成员变量
其中 objc_ivar_list 是成员变量列表,定义如下:
// runtime.h
struct objc_ivar_list {
int ivar_count;
int space;
struct objc_ivar ivar_list[1];//成员变量的数组
}
struct objc_ivar {
char *ivar_name; //单个变量的名字
char *ivar_type; //变量类型
int ivar_offset; //偏移量
int space;
}
typedef struct objc_ivar *Ivar;
可见成员变量列表objc_ivar_list结构体存储着由成员变量objc_ivar结构体组成的数组,objc_ivar结构体存储着单个成员变量的名字、类型、偏移量等信息。
注意:成员变量和属性的区别,属性可以1.生成成员变量 2.声明和实现set、get方法(在分类中除外,因为分类中不能用成员变量)
5、方法
objc_method_list是方法列表,定义如下:
// runtime.h
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
int space;
struct objc_method method_list[1];
}
struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
}
typedef struct objc_method *Method;
可见方法列表objc_method_list结构体存储着由方法objc_method结构体组成的数组,objc_method 结构体存储着单个方法的信息:名称(SEL类型的)、参数类型和返回值类型(method_types中)和具体实现(IMP类型的)。
6、IMP
IMP(method implementation,方法实现) 的定义是:
// objc.h
typedef id (*IMP)(id, SEL, ...);
所以它其实是一个函数指针,指向某个方法的具体实现。它的类型和objc_msgSend函数相同,参数中也都包含有 id 和 SEL 类型,这是因为一个 id 和 一个 SEL 参数就能确定唯一的方法实现地址。
7、Cache
在objc_class结构体中还有个指向objc_cache结构体的指针,它的定义如下:
// runtime.h
typedef struct objc_cache *Cache
// objc-cache.m
struct objc_cache {
// 当前能达到的最大 index
uintptr_t mask;
// 被占用的槽位。因为缓存是以散列表的形式存在,所以会有空槽
uintptr_t occupied;
// 用数组表示的 hash 表
cache_entry *buckets[1];
};
typedef struct {
SEL name;
void *unused;
IMP imp;
} cache_entry;
// _uintptr_t.h
typedef unsigned long uintptr_t;
所以它用来做缓存的,用buckets数组来存储被调用过的方法。因为一个方法被调用过,那它以后有可能还会被调用,所以将其存储起来,下次要找某方法先到缓存中找,如果找到的话,免去后面的寻找过程,速度虽然仍会比直接调用函数慢一点点,但已经有很大提升。
8、属性
还有我们常用的属性其实也是结构体,它的定义如下:
// runtime.h
typedef struct objc_property *objc_property_t;
typedef struct {
const char *name;
const char *value;
} objc_property_attribute_t;
// objc-runtime-new.h
typedef struct objc_property {
const char *name;
const char *attributes;
} property_t;
typedef struct property_list_t {
uint32_t entsize;
uint32_t count;
property_t first;
} property_list_t;
所以一个property_t结构包含了属性的名称和属性字符串。与属性相关的一些方法如下:
#define newproperty(p) ((property_t *)p)
// 返回协议中的属性列表,属性个数存储在参数 outCount 中
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
// 返回类中的属性列表,属性个数存储在参数 outCount 中
objc_property_t *class_copyPropertyList(Class cls_gen, unsigned int *outCount)
// 返回属性列表中的属性数组,属性个数存储在参数 outCount 中
static property_t **copyPropertyList(property_list_t *plist, unsigned int *outCount)
// 返回类中的特定名字的属性
objc_property_t class_getProperty(Class cls_gen, const char *name)
// 返回某个属性的名字
const char *property_getName(objc_property_t prop)
// 返回某个属性的属性字符串
const char *property_getAttributes(objc_property_t prop)
三、介绍消息发送机制已及怎样找到函数实现
OC是一种动态语言,它把编译和链接时候要做的很多事情移到了运行时去做,例如C语言中,源代码要想通过编译,函数声明和函数实现必须都存在,它应该在编译时就把函数实现的地址和函数名称关联了。但是OC是动态语言,没有方法(面向对象称为方法)实先,只有方法声明也是可以编译通过的,方法名称和方法实现是在运行时动态绑定的如官方所述(In Objective-C, messages aren’t bound to method implementations until runtime)。
那么问题来了,在运行时,方法是怎么被找到的呢?
使用某对象的方法,都是给这个对象发送消息,消息和方法实现直到运行时才会绑定。Runtime 系统会把使用方法转换为调用函数:
objc_msgSend(receiver, selector)
注意到此时函数多了两个参数:消息接收者、方法的 selector。这是每个方法调用时都会默认存在的隐藏参数。如果还有其他参数则是:
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 类为止。这两个过程的示意图如下:
如果找到,还会根据是否把消息传给父类、返回值是否数据结构而选择下面四个函数中的一个来调用:
// message.h
id objc_msgSend(id self, SEL op, ...)
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
void objc_msgSend_stret(id self, SEL op, ...)
void objc_msgSendSuper_stret(struct objc_super *super, SEL op, ...)
函数中的super关键字是指向objc_super结构体的指针,objc_super结构体定义如下:
struct objc_super {
// receiver 仍是 self 本身
__unsafe_unretained id receiver;
// 父类的类型
__unsafe_unretained Class super_class;
};
要注意的是如果想获取某个类的父类,要用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 方法:
Person *p = [[Person alloc] init];
[p aMethod];
它的 .h 和 .m 文件如下:
// Person.h
#import
@interface Person : NSObject
- (void)aMethod;
@end
// Person.m
#import "Person.h"
#import
// 要被动态添加的方法实现
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"dynamicMethodIMP");
}
@implementation Person
// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果是要被添加方法实现的 selector
if (sel == @selector(aMethod)) {
// 给 self 的类的 sel 方法选择器动态添加方法实现 dynamicMethodIMP
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
// 返回 YES 后, Runtime 重新给对象发送 aMethod 消息,这次就可以找到 dynamicMethodIMP 方法实现并调用它了
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
(7)重定向:如果在上面的方法中不做处理或返回 NO,Runtime 系统在报错前还会给第二次补救机会,就是会调用forwardingTargetForSelector:方法索要一个能响应这个消息的对象,所以我们可以在这里返回另外一个能处理该消息的对象:1
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 如果是要被添加方法实现的 selector
if (aSelector == @selector(aMethod)) {
// 返回另外一个对象,让它去接收该消息
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
上面返回的是一个 Car 对象,如果 Car 类定义如下:
// Car.h
#import
@interface Car : NSObject
- (void)aMethod;
@end
// Car.m
#import "Car.h"
@implementation Car
- (void)aMethod {
NSLog(@"car aMethod");
}
@end
则输出结果就是 “car aMethod”了。
(8)消息转发:如果在上一步中不做处理或者返回 nil 或 self,则 Runtime 系统会在报错前给我们最后一次补救机会。系统会先调用methodSignatureForSelector:方法,在该方法返回一个包含了消息的描述信息的方法签名(NSMethodSignature对象),并用此方法签名去生成一个NSInvocation对象,然后调用forwardInvocation:方法并把刚生成的NSInvocation对象作参数传进去。我们就可以重写forwardInvocation:方法,在这里将消息转发给其他对象:
// 获取一个方法签名,用于生成 NSInvocation 对象
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
signature = [[Car new] methodSignatureForSelector:aSelector];
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 如果另一个对象能响应该方法
if ([[Car new] respondsToSelector:[anInvocation selector]]) {
// 则让另一个对象来响应该方法
[anInvocation invokeWithTarget:[Car new]];
} else {
[super forwardInvocation:anInvocation];
}
}
尽管消息转发的效果类似于多继承,让一个对象看起来能处理自己不拥有的方法,但 NSObject 类不会将两者混淆。如上面的例子,[p respondsToSelector:@selector(aMethod)]的结果还是NO。
PS: 上面调用的方法顺序也可以这样获得:在程序启动之后暂停,然后在 gdb 中输入这个命令:call (void)instrumentObjcMessageSends(YES),再运行,则发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。如新建一个Teacher类,给它的实例发送一条错误的消息:
Teacher *teacher = [[Teacher alloc] init];
[teacher aMehtod];
四、runtime的应用
1.归档和解档(NSCoding)
利用runtime函数 遍历模型的所有属性
代码如下图
归档代码
接档代码
调用代码
结果
优势:如果模型对象比较多,不用一个个复制
注意:因为runtime是C语言文件,它的内存不属于ARC管理因此涉及到copy等方法时要手动管理内存。
2.Method Swizzing
这个就是俗称“黑魔法”,其本质在于交换两个函数的IMP,从而达到调用A方法但是执行的却是B方法,其具体实现和原理参考这篇OC中hook方案(一):method swizzling 文章
3.关联属性
提示:MJRefresh 里面的UIScrollerView 本身就是利用关联属性在category 里面添加 header 和 footer的。
我们还可能希望给某些常用的类添加 category,但 category 是只能添加方法而不能添加存储属性的。或者更严谨的说是分类不允许添加成员变量,因为属性的作用就是
1.生成下划线成员变量
2.声明和实现set 和get方法
因为属性不能添加成员变量所以就不能实现添加属性
现在我们可以用 Runtime 来间接在 category 添加属性了,如在给 UIButton 的 category 中添加一个属性作回调:
// UIButton+Extension.h
#import <UIKit/UIKit.h>
typedef void (^CallbackBlock)();
@interface UIButton (Extension)
@property (copy, nonatomic) CallbackBlock callback;
@end
// UIButton+Extension.m
#import "UIButton+Extension.h"
#import <objc/runtime.h>
const void *yg_callbackKey = @"yg_callbackKey";
@implementation UIButton (Extension)
- (void)setCallback:(CallbackBlock)callback {
// 设置关联属性
objc_setAssociatedObject(self, yg_callbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (CallbackBlock)callback {
// 获取关联属性
return objc_getAssociatedObject(self, yg_callbackKey);
}
@end
这样就可以把 callback 当做按钮的属性来用了:
// ViewController.m
#import "ViewController.h"
#import "UIButton+Extension.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *button;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 设置按钮的 callback “属性”的内容
self.button.callback = ^{
NSLog(@"button callback");
};
// 获取并执行按钮的 callback “属性”
self.button.callback();
}
@end
我们常用的第三方库中有很多也是这样用的,如 SDWebImage 会用这样的方法来存储传进来的图片的 URL:
// UIImageView+WebCache.m
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
...
}
- (NSURL *)sd_imageURL {
return objc_getAssociatedObject(self, &imageURLKey);
}
参考文章