runtime 小结

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时的运行流程如下:

1、首先通过调用对象的isa找到class;
2、在class的method_list里面找该方法,这里如果是实例对象,则去实例对象的类的方法列表中找,如果是类对象调用类方法,则去元类的方法列表中找,具体下面解释;
3、如果class里没找到,继续往它的superClass里找;
4、一旦找到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指针指向自己,最终形成一个闭环。



可以看到类结构体中有一个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提供了两种方法:

1、IMP class_getMethodImplementation(Class cls, SEL name);
2、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,如果一直到根类都没找到就会进入到消息转发阶段,下面介绍一下消息转发的最后三个集会。

1、动态方法解析
2、备用接收者
3、完整消息转发

动态方法解析

首先,当消息传递到根类都找不到方法实现时,运行时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!
努力是为了当机会来临时不会错失机会。
共勉!

链接:https://www.jianshu.com/p/4ae997a6c599

0 个评论

要回复文章请先登录注册