OC 被称之为动态运行时语言, 最主要的原因就是因为两个特性, 一个是运行时也就是 runtime, 一个是多态.
runtime
runtime 又叫运行时, 是一套底层的 c 语言 API, 其为 iOS 内部核心之一. OC 是动态运行时语言, 它会将一些工作放在代码运行时去处理, 而非编译时, 比如动态的遍历属性和方法, 动态的添加属性和方法, 动态的修改属性和方法等.
了解 runtime, 首先要先了解它的核心 -- 消息传递.
消息传递
消息直到运行时才会与方法实践绑定起来.
一个实例对象调用实例方法, 像这样[obj doSomething];, 编译器转成消息发送 objc_msgSend(obj, @selector(doSomething),,);,
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
runtime 时的运行流程如下:
首先通过调用对象的 isa 找到 class;
在 class 的 method_list 里面找该方法, 这里如果是实例对象, 则去实例对象的类的方法列表中找, 如果是类对象调用类方法, 则去元类的方法列表中找, 具体下面解释;
如果 class 里没找到, 继续往它的 superClass 里找;
一旦找到 doSomething 这个函数, 就去执行它的实现 IMP;
下面介绍一下对象 (object), 类(class), 方法(method) 的结构体:
- // 对象
- struct objc_object {
- Class isa OBJC_ISA_AVAILABILITY;
- };
- // 类
- struct objc_class {
- Class isa OBJC_ISA_AVAILABILITY;
- #if !__OBJC2__
- Class super_class OBJC2_UNAVAILABLE;
- const char *name OBJC2_UNAVAILABLE;
- long version OBJC2_UNAVAILABLE;
- long info OBJC2_UNAVAILABLE;
- long instance_size OBJC2_UNAVAILABLE;
- struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
- struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
- struct objc_cache *cache OBJC2_UNAVAILABLE;
- struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
- #endif
- } OBJC2_UNAVAILABLE;
- // 方法列表
- struct objc_method_list {
- struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
- int method_count OBJC2_UNAVAILABLE;
- #ifdef __LP64__
- int space OBJC2_UNAVAILABLE;
- #endif
- /* variable length structure */
- struct objc_method method_list[1] OBJC2_UNAVAILABLE;
- } OBJC2_UNAVAILABLE;
- // 方法
- struct objc_method {
- SEL method_name OBJC2_UNAVAILABLE;
- char *method_types OBJC2_UNAVAILABLE;
- IMP method_imp OBJC2_UNAVAILABLE;
- }
类对象(objc_class)
OC 中类是 Class 来表示, 实际上是一个指向 objc_class 结构体的指针.
- // 对象
- struct objc_object {
- Class isa OBJC_ISA_AVAILABILITY;
- };
- // 类
- struct objc_class {
- Class isa OBJC_ISA_AVAILABILITY;
- #if !__OBJC2__
- Class super_class OBJC2_UNAVAILABLE;
- const char *name OBJC2_UNAVAILABLE;
- long version OBJC2_UNAVAILABLE;
- long info OBJC2_UNAVAILABLE;
- long instance_size OBJC2_UNAVAILABLE;
- struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
- struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
- struct objc_cache *cache OBJC2_UNAVAILABLE;
- struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
- #endif
- } OBJC2_UNAVAILABLE;
- // 方法列表
- struct objc_method_list {
- struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
- int method_count OBJC2_UNAVAILABLE;
- #ifdef __LP64__
- int space OBJC2_UNAVAILABLE;
- #endif
- /* variable length structure */
- struct objc_method method_list[1] OBJC2_UNAVAILABLE;
- }
观察一下对象的结构体和类对象的结构体, 可以看到里面都有一个 isa 指针, 对象的 isa 指针指向类, 类的 isa 指针指向元类(metaClass), 元类也是类, 元类的 isa 指针最终指向根元类(rootMetaClass), 根元类的 isa 指针指向自己, 最终形成一个闭环.
1552095614854.jpg
可以看到类结构体中有一个 methodLists, 也就解释了上文提到的成员方法记录在 class method-list 中, 类方法记录在 metaClass 中. 即 Instance-object 的信息记录在 class-object 中, 而 class-object 的信息记录在 meta-class 中.
结构体中有一个 ivars 指针指向 objc_ivar_list 结构体, 是该类的属性列表, 因为编译器编译顺序是父类, 子类, 分类, 所以这也就是为什么分类 category 不能添加属性, 因为类在编译的时候已经注册在 runtime 中了, 属性列表 objc_ivar_list 和 instance_size 内存大小都已经确定了, 同时 runtime 会调用 class_setIvarLayout 和 class_setWeakIvarLayout 来处理 strong 和 weak 引用. 可以通过 runtime 的关联属性来给分类添加属性(原因是 category 结构体中有一个 instanceProperties, 下文会讲到). 因为编译顺序是父类, 子类, 分类, 所以消息遍历的顺序是分类, 子类, 父类, 先进后出.
objc_cache 结构体, 是一个很有用的方法缓存, 把经常调用的方法缓存下来, 提高遍历效率. 将方法的 method_name 作为 key,method_imp 作为 value 保存下来.
Method(objc_method)
结构体如下:
- // 方法
- struct objc_method {
- SEL method_name OBJC2_UNAVAILABLE;
- char *method_types OBJC2_UNAVAILABLE;
- IMP method_imp OBJC2_UNAVAILABLE;
- }
可以看到里面有一个 SEL 和 IMP, 这里讲一下两者的区别.
SEL 是 selector 的 OC 表示, 数据结构为: typedef struct objc_selector *SEL; 是个映射到方法的 c 字符串; 不同于函数指针, 函数指针直接保存了方法地址, SEL 只是一个编号; 也是 objc_cache 中的 key.
ps. 这也带来了一个弊端, 函数重载不适用, 因为函数重载是方法名相同, 参数名不同, 但是 SEL 只记了方法名, 没有参数, 所以没法区分不同的 method.
ps. 在不同的类中, 相同的方法名, 方法选择器也是相同的.
IMP 是函数指针, 数据结构为 typedef id (IMP)(id,SEL,**); 保存了方法地址, 由编译器绑定生成, 最终方法执行哪段代码由 IMP 决定. IMP 指向了方法的实现, 一组 id 和 SEL 可以确定唯一的实现.
有了 SEL 这个中间过程, 我们可以对一个编号和方法实现做些中间操作, 也就是说我们一个 SEL 可以指向不同的函数指针, 这样就可以完成一个方法名在不同的时候执行不同的函数体. 另外可以将 SEL 作为参数传递给不同的类执行, 也就是我们某些业务只知道方法名但需要根据不同的情况让不同的类执行. 个人理解, 消息转发就是利用了这个中间过程.
runtime 是如何通过 selector 找到对应的 IMP 的?
上文讲了类对象中有实例方法的列表, 元类对象中有类方法的列表, 列表中记录着方法的名称, 参数和实现. 而 selector 本质就是方法名称也就是 SEL, 通过方法名称可以在列表中找到方法实现.
在寻找 IMP 的时候, runtime 提供了两种方法:
- IMP class_getMethodImplementation(Class cls, SEL name);
- IMP method_getImplementation(Method m);
对于第一种方法来说, 实例方法和类方法都是调用这个方法来找到 IMP, 不同的是第一个参数, 实例方法传的参数是[obj class];, 而类方法传的参数是 objc_getMetaClass("obj");
对于第二种方法来说, 传入的参数只有 Method, 区分类方法和实例方法在于封装 Method 的函数, 类方法: Method class_getClassMethod(Class cls, SEL name); 实例方法: Method class_getInstanceMethod(Class cls, SEL name);
Category(objc_category)
category 是表示指向分类的一个结构体指针, 结构体如下:
- struct category_t {
- const char *name;
- classref_t cls;
- struct method_list_t *instanceMethods;
- struct method_list_t *classMethods;
- struct protocol_list_t *protocols;
- struct property_list_t *instanceProperties;
- };
name: 是指 class_name 而不是 category_name.
cls: 要扩展的类对象, 编译期间是不会定义的, 而是在 Runtime 阶段通过 name 对应到对应的类对象.
instanceMethods:category 中所有给类添加的实例方法的列表.
classMethods:category 中所有添加的类方法的列表.
protocols:category 实现的所有协议的列表.
instanceProperties: 表示 Category 里所有的 properties, 这就是我们可以通过 objc_setAssociatedObject 和 objc_getAssociatedObject 增加实例变量的原因, 不过这个和一般的实例变量是不一样的.
从上面的结构体可以看出, 分类 category 可以添加实例方法, 类方法, 协议, 以及通过关联对象添加属性, 不可以添加成员变量.
runtime 消息转发
前文讲到, 到一个方法被执行, 也就是发送消息, 会去相关的方法列表中寻找对应的方法实现 IMP, 如果一直到根类都没找到就会进入到消息转发阶段, 下面介绍一下消息转发的最后三个集会.
动态方法解析
备用接收者
完整消息转发
动态方法解析
首先, 当消息传递到根类都找不到方法实现时, 运行时 runtime 会调用 + resolveInstanceMethod: 或者 + resolveClassMethod:, 让你有机会提供一个函数实现. 如果你添加了函数, 并返回了 yes, 那运行时就会重新走一步消息发送的过程.
实现一个动态方法解析的例子如下:
- - (void)viewDidLoad {
- [super viewDidLoad];
- // Do any additional setup after loading the view, typically from a nib.
- // 执行 foo 函数
- [self performSelector:@selector(foo:)];
- }
- + (BOOL)resolveInstanceMethod:(SEL)sel {
- if (sel == @selector(foo:)) {// 如果是执行 foo 函数, 就动态解析, 指定新的 IMP
- class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
- return YES;
- }
- return [super resolveInstanceMethod:sel];
- }
- void fooMethod(id obj, SEL _cmd) {
- NSLog(@"Doing foo");// 新的 foo 函数
- }
可以看到虽然没有实现 foo 这个函数, 但是我们通过 class_addMethod 动态的添加了一个新的函数实现 fooMethod, 并返回了 yes.
如果返回 no, 就会进入下一步,- forwardingTargetForSelector:.
备用接收者
实现的例子如下:
- #import "ViewController.h"
- #import "objc/runtime.h"
- @interface Person: NSObject
- @end
- @implementation Person
- - (void)foo {
- NSLog(@"Doing foo");//Person 的 foo 函数
- }
- @end
- @interface ViewController ()
- @end
- @implementation ViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- // Do any additional setup after loading the view, typically from a nib.
- // 执行 foo 函数
- [self performSelector:@selector(foo)];
- }
- + (BOOL)resolveInstanceMethod:(SEL)sel {
- return NO;// 返回 NO, 进入下一步转发
- }
- - (id)forwardingTargetForSelector:(SEL)aSelector {
- if (aSelector == @selector(foo)) {
- return [Person new];// 返回 Person 对象, 让 Person 对象接收这个消息
- }
- return [super forwardingTargetForSelector:aSelector];
- }
- @end
可以看到我们通过 - forwardingTargetForSelector: 方法将当前 viewController 的 foo 函数转发给了 Person 的 foo 函数去执行了.
如果在这一步还不能处理未知的消息, 则进入下一步完整消息转发.
完整消息转发
首先会发送 - methodSignatureForSelector: 消息获得函数的参数和返回值类型. 如果 - methodSignatureForSelector: 返回 nil,runtime 会发出 - doseNotRecognizeSelector 消息, 程序会挂掉; 如果返回一个函数标签, runtime 就会创建一个 NSInvocation 对象, 并发送 - forwardInvocation: 消息给目标对象.
实现例子如下:
- #import "ViewController.h"
- #import "objc/runtime.h"
- @interface Person: NSObject
- @end
- @implementation Person
- - (void)foo {
- NSLog(@"Doing foo");//Person 的 foo 函数
- }
- @end
- @interface ViewController ()
- @end
- @implementation ViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- // Do any additional setup after loading the view, typically from a nib.
- // 执行 foo 函数
- [self performSelector:@selector(foo)];
- }
- + (BOOL)resolveInstanceMethod:(SEL)sel {
- return NO;// 返回 NO, 进入下一步转发
- }
- - (id)forwardingTargetForSelector:(SEL)aSelector {
- return nil;// 返回 nil, 进入下一步转发
- }
- - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
- if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
- return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 签名, 进入 forwardInvocation
- }
- return [super methodSignatureForSelector:aSelector];
- }
- - (void)forwardInvocation:(NSInvocation *)anInvocation {
- SEL sel = anInvocation.selector;
- Person *p = [Person new];
- if([p respondsToSelector:sel]) {
- [anInvocation invokeWithTarget:p];
- }
- else {
- [self doesNotRecognizeSelector:sel];
- }
- }
- @end
通过签名, runtime 生成了一个 anInvocation 对象, 发送给了 forwardInvocation:, 我们再 forwardInvocation: 里面让 Person 对象去执行了 foo 函数.
以上就是 runtime 的三次函数转发流程.
Better Late Than Never!
努力是为了当机会来临时不会错失机会.
共勉!
来源: http://www.jianshu.com/p/4ae997a6c599