转自:http://www.jianshu.com/p/ea1743715609
runtime 详解
本文结构:
-
简介
-
runtime版本和平台
-
与runtime交互
-
runtime术语
-
消息
-
动态方法解析
-
消息转发
-
健壮的实例变量
-
总结
1、简介
Cocoa的Objective-C语言可以在编译和链接的时候不知道类或者成员变量,只有在runtime(运行时)的时候才知道它们。runtime是iOS的一套底层API。它就像是Objective-C的操作系统,处理并执行Objective-C语言。比如:
//一个OC的方法调用
[receiver message];
//编译器会把它编译为:
objc_msgSend(receiver,message);
//假如消息中含有参数,会转化为如下形式:
[receiver message:arg...];
objc_msgSend(receiver,message,arg1,arg2,...);
runtime会在运行的时候知道这些经过编译之后的代码,然后去执行它。
2、runtime版本和平台
-
logacy version: 运行在之前Objective-C 1.0的早期32位版本中。
-
modern version:现在我们用的就是modern version,运行在iOS 和OSX 10.5之后的64位程序中。
3、与runtime交互
Objective-C 源代码
runtime会在后台自动的给你执行这些编译过的代码,我们只需写Objective-C代码即可。当然了我们也可以直接写runtime相关的代码,类似上面的例子中objc_msgSend()函数等。消息的执行会使用到编译器为实现动态语言创建的数据结构和函数,Objective-C中的类、方法和协议等在runtime中都是有一些结构体来定义。如objc_msgSend()函数已经参数 id 和 SEL。
NSObject 方法
Cocoa 中的大部分方法都是继承与NSObject,也继承了它的方法。唯一特殊的是NSProxy,它是个抽象的超类,它实现了一些消息转发的方法,可以继承它实现一些类的替身类或者是虚拟出一个不存在的类。
有的方法起到了抽象接口的作用,比如description方法需要你在重载它并为你提供的类添加描述信息。NSObject还有一些方法能获取到运行时类的信息,比如class:
返回对象的类;isKindOfClass:
和isMemberOfClassL:
则检查对象是否存在某个类继承体系中;respondsToSelector
则检查对象能否响应某方法;conformToProtocol
则检查是否实现了指定的协议;mothodForSelector
则返回方法实现的地址。
runtime的函数
runtime系统是一个有一些了函数和结构体组成,具有公共接口的动态库,头文件放在/usr/include/objc/
文件里面,通过包含头文件#import <objc/runtime.h>
就可以查看到这个头文件,里面的方法需要你通过编写C语言的方法来实现Objective-C的方法。在Objective-C Runtime Reference中有对runtime函数的详细文档。
4、runtime术语
还是使用上面的objc_msgSend()方法吧,它的真身是
id objc_msgSend(id self,SEL op...)
id
objc_msgSend函数的第一个参数就是id,它是一个指向对象的指针。在/usr/include/objc/objc.h
中
typedef struct objc_object *id;
objc_object 是什么呢?
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
objc_object它是一个含有isa指针,通过它就可以找到对象所属的类。
注:isa指针在代码运行的时候并不总指向实例所属的类,所有不能靠它来确定类型,可以通过-class
来确定。KVO的实现机理就是通过isa指针指向一个中间类而不是真实类型来实现。详情可看KVO的实现
SEL
objc_msgSend的第二个参数是SEL,它是selector在Objective-C中的表示。selector是一个方法选择器,可以理解为确保方法的ID,这个ID的数据结构是SEL
typedef struct objc_selector *SEL;
你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。
Class
isa
是指针是因为Class 是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;
而objc_class结构体里面包含许多信息
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
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 ;
#endif
}
它有指向超类的super_class指针。其中 objc_ivar_list 是成员变量objc_ivar 列表;objc_method_list是objc_method方法列表。
// 成员变量列表
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} 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;
objc_class里面还有一个isa指针,这是因为Objective-C本身也是对象,为了区别对象和类,ObjC创建了Meta Class 的东西,类对象所属的类就叫做元类。
我们熟悉的类方法,就来源于元类,可以理解为类方法就是类对象的实例方法,每一个类只有一个类对象,每一个类对象只有一个类,当发出一个[NSObject new]的消息时,事实上是把这个消息发送给了类对象,这个类对象是元类的一个实例,而这个元类也是一个根元类(root meta class),所有的元类最终都指向根元类为自身的父类。所有的元类的方法列表中都有能够相应消息的方法,所有当[NSObject new]消息发送给类对象的时候,objc_msgSend就会去方法列表中去找能够相应的方法,找到了就去响应。
![](https://i-blog.csdnimg.cn/blog_migrate/9ae39d87b53bdc37e143cd046b5e1524.webp?x-image-process=image/format,png)
该图实现是super_class指针,虚线是isa指针。根元类的超类是NSObject,isa指针指向自己,NSObject的父类是nil,它没有超类。
Method
Method代表了一直在类中定义的方法
typedef struct objc_method *Method;
而objc_method存储了方法名、方法类型和方法实现
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
- SEL:代表了方法名类型,在不同的类中定义,它们的方法选择器也不一样
- 方法类型method_types是一个char指针,存储着方法的参数类型和返回类型
- method_imp指向了方法的实现,本质上是一个函数指针
Ivar
ivar代表类中实例变量的类型
typedef struct objc_ivar *Ivar;
而objc_ivar在上面也提过
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
IMP
在objc.h中定义的是,
typedef id (*IMP)(id, SEL, ...);
它是一个函数指针,有编译器生成,当发送一个消息后,指向的那个代码,就是由这个函数指针指定的。这个IMP指针就执行了这个方法的实现。获得了指向的入口,就会跳过消息传递阶段,直接执行。IMP方法也包含id、SEL,每一个SEL都指向一个方法选择器,每个实例对象都有唯一的SEL方法,通过id和SEL就能够确定唯一的方法执行地址。反之亦然。
Cache
在runtime.h中cache的定义是
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE;
在之前的objc_class中的objc_cache,它是什么的缓存?
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
Cache为方法调用的性能进行优化,也就是在方法调用的时候会首先从这个缓存列表中找,如果找到就执行,没有找到就通过isa指针在类的方法列表中寻找,找到之后执行并把这个方法添加到这个缓存列表中,以供下一次调用,当下一次调用的时候直接从缓存列表中找,这样就提高了很高的性能,不用通过isa指针去查找。
Property
在runtime.h中的定义,表示一个Objective-c类的属性,它是一个指向objc_property结构体的指针
typedef struct objc_property *objc_property_t;
可以通过class_copyPropertyList 和 protocol_copyPropertyList方法来获取类和协议中的属性:
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
返回类型为指向指针的指针,以为属性列表是一个数组,每个元素内容欧式一个objc_property_t指针,而这两个方法返回的是指向数组的指针。
举个例子:
定义一个Person类
@interface Person : NSObject
@property(copy,nonatomic)NSString *name;
@property(assign,nonatomic)NSInteger age;
@end
然后通过下面的代码获取到Person的属性
//通过这个方法获取类
id personClass = objc_getClass("Person");
//通过下面的方法获取属性列表
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(personClass, &outCount);
for (int i = 0 ; i < outCount; i++) {
objc_property_t property = properties[i];
printf("%s:%s\n",property_getName(property),property_getAttributes(property));
//打印结果
//name:T@"NSString",C,N,V_name
//age:Tq,N,V_age
}
free(properties); //释放数组
5、消息
发送消息的步骤
Objective-C通过[]
将接受者和消息包裹起来,到运行的时候才把消息与方法实现绑定。
在简介中的objc_msgSend()函数并不返回数据,而是objc_msgSend()调用的方法执行之后返回了数据。下面就是消息的发送步骤
![](https://i-blog.csdnimg.cn/blog_migrate/b456ed34c8bb77e2c584b9785c6785c3.webp?x-image-process=image/format,png)
- 检测selector是否要执行,比如有了垃圾回收机制就不需要retain和release等方法选择器 。
- 检测targer是否为nil,Objective-C允许我们队一个nil对象执行方法,然后忽略掉。
- 上面都执行成功了,就开始查找这个方式实现的IMP,先从Cache中查找,如果找到了就执行。
- 如果找不到就在类的方法列表中去查找
- 如果在类的方法列表中找不到就去父类的方法列表中去查找,一直到NSObject为止。
- 如果还找不到,就开始
动态方法解析
了,后面会讲解。
消息传递中,编译器会根据情况在objc_msgSend、objc_msgSend_stret、 objc_msgSendSuper、objc_msgSendSuper_stret,如果消息传递给父类,就调用带有Super的方法。如果消息的返回值不是结构体而是简单值时就会调用带有stret的函数。
方法中隐藏的参数
当objc_msgSend找到执行的函数,在执行这段函数的时候会隐式的传递两个参数 (self,_cmd),一个是消息的接受者,一个是方法的选择器。它们并不在源代码中声明,在编译的时候会直接插入到方法的实现中。尽管没有显示的声明,但是我们依然可以引用它们。在下面的例子中 self引用了接受者对象,_cmd引用了方法本身的选择器。
- strange
{
id target = getTheReceiver();
SEL method = getTheMethod();
if ( target == self || method == _cmd )
return nil;
return [target performSelector:method];
}
获取方法地址
NSObject有一个methodForSelector方法,可以通过它获取方法的地址,然后执行IMP:
给上面的Person复写description方法
+ (NSString *)description
{
NSLog(@"person have name and age property");
return @"description";
}
获取IMP(方法地址) ,并执行。
//定义一个IMP
void (*methodPointer)(id ,SEL);
//methodForSelector根据@selector返回IMP指针地址
methodPointer = (void (*)(id, SEL))[Person methodForSelector:@selector(description)];
//执行这个IMP
methodPointer([Person class],@selector(description));
//会打印:person have name and age property
6、动态方法解析
我们可以动态的提供一个方法实现,比如先给Person类声明一个属性weight(体重),并在实现文件.m中用@dynamic修饰
@dynamic weight
这表明系统不会自动的给weight属性添加setWeight:
和weight
方法,需要我们动态的提供。我们可以通过重载resolveInstanceMethod:
和resolveClassMethod:
来添加实例方法和类方法。因为当runtime系统在Cache和方法分发列表中找不到要执行的方法的时候,runtime会调用这两个方法给我们一次动态添加方法的机会。我们可以用class_addMethod
完成向特定类或者类实例添加方法的功能。话不多说,上例子:
person.weight = 100;
因为在weight前面加了@dynamic指令,并没有setWeight:
调用(在Cache和方法分发列表中都找不到),就会调用resolveInstanceMethod
方法,这时候我们复写这个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(setWeight:)){
class_addMethod([self class], sel,(IMP)setPropertyDynamic, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
因为Objective的语法会调用set方法,这时候就给Person类添加一个方法为我们自己定义的(IPM)setPropertyDynamic
void setPropertyDynamic(id self,SEL _cmd){
//方法的实现部分
NSLog(@"This is a dynamic method added for Person instance");
}
这里我们就简答的打印了一句话,结果就会打印出:This is a dynamic method added for Person instance
。说明已经给类添加了setPropertyDynamic方法,并调用。
上的class_addMethod方法后面的参数:第一个是哪个类,即类名;第二个是被替换的SEL;第三个是要替换并执行的SEL;第四个是返回值和参数(V代表返回值,具体参考Type Encodings)。
注:动态方法解析会在消息转发之前执行,如果resolveInstanceMethod:
和resolveClassMethod:
执行,就会给动态方法解析器提供一个方法选择器对应的IMP的机会。如果想让该方法选择器直接跳到转发机制,这两个方法返回NO即可。
7、消息转发
![](https://i-blog.csdnimg.cn/blog_migrate/e4f35e092316cb021446aa46a38768bf.webp?x-image-process=image/format,png)
重定向
如果上面的resolveInstanceMethod:
或者resolveClassMethod:
返回NO,消息转发就会进入消息转发机制,但是runtime又给我们一次机会再次修改接受者的机会,即当前的接受者不能收到这个消息,我们通过重载- (id)forwardingTargetForSelector:(SEL)aSelector
这个消息转发给其他能接受消息的对象。还那上面的Person的weight属性作为例子,以为它没有weight
,即getter方法,我们通过调用
NSInteger weightValue = person.weight;
时,因Person的实例没有getter方法,而resolveInstanceMethod:
也没有找到替换的方法,就会执行我们重载的forwardingTargetForSelector
方法
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(weight)) {
People *people = [People new];
return people;
}
return [super forwardingTargetForSelector:aSelector];
}
在这个方法中我们判断假如消息是weight
即weight属性的getter方法,就把这个消息转发给我们创建的另外一个类People,我们只是添加了一个weight方法,返回一个跟weight相同类型的NSInteger值
@interface People : NSObject
@end
@implementation People
- (NSInteger)weight
{
return 666;
}
@end
经过forwardingTargetForSelector
返回了People
的实例people
作为weight
消息的接受者,然后people
调用weight
方法并返回了666
。打印上面的weightValue
就可以看到结果为666。
转发
当重定向还不作处理的时候,这消息转发机制就会出发,这个时候就会调用- (void)forwardInvocation:(NSInvocation *)anInvocation
这个方法,我们可以重写这个方法来定义我们的转发逻辑。
首先在Person
类添加一个NSString
类型的identifier
属性,并用@dynamic
指令修饰,表明不让系统给我们创建setter、getter
方法,然后我们调用
perosn.identifier = @"iyaqi";
因为resolveInstanceMethod:
和上面的forwardingTargetForSelector
都没有处理这个消息,所有person的setter消息就会被转发,执行forwardInvocation
方法,但是在这个方法执行之前会先调用methodSignatureForSelector
方法,所以我们也要复写这个方法,否则就会异常
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(setIdentifier:)) {
NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];//这个参数要参考上面的Objective-C Type Encodings
return sign;
}
return nil;
}
这个方法是根据SEL
去生成一个方法签名,然后系统用这个方法签名去生成NSInvocation
对象,NSInvocation
对象封装了最原始的消息和参数,然后执行forwardInvocation
方法
- (void)forwardInvocation:(NSInvocation *)anInvocation{
People *people = [[People alloc]init];
if ([people respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:people];
}else{
[super forwardInvocation:anInvocation];
}
}
这个方法就会将消息传递给People对象,我们在Perople实现文件里面添加了一个setIdentifier:
方法
- (void)setIdentifier:(NSString*)str
{
NSLog(@"This is a forward method:%@",str);
}
people对象就会接受setIdentifier:
消息并执行,打印结果为:This is a forward method:iyaqi
当一个类无法响应某个消息的时候,runtime会通过forwardInvocation
通知该对象。每个对象都继承了NSObject的forwardInvocation
方法,但是NSObject并没有实现,所以需要我们手动的实现。forwardInvocation
就像是一个消息分发中心或者是一个中转站,能将消息分发给不同的对象。它还可以将某些消息更改,或者是'吃掉',不响应这些消息也不会出错。
转发和多继承
转发和继承相似,就像是实现了多重继承一样,为Objective-C添加继承的效果。就像下图一样,将一个消息发出去,把其他对象的消息借过来用一样。
![](https://i-blog.csdnimg.cn/blog_migrate/313902b072563cfb8ce582d1659fc067.webp?x-image-process=image/format,png)
这使不同继承体系下的两个类可以继承对方的方法,上面的Warrior
和 Diplomat
虽然没有继承关系,但是将Warrior
的消息发给Diplomat
之后就好像有了“继承”关系一样。
消息转发弥补了Objective-C不支持多继承的特性,也避免了单个类因为多继承而复杂的情况。
替代者对象
虽然消息转发实现了继承的功能,但是像respondsToSelector:
和isKindOfClass:
仍然只支持继承体系,不会考虑转发机制。比如上图中的negotiate
消息
if ([Warrior respondsToSelector:@selector(negotiate)]){
}
Warrior
肯定不能响应negotiate
消息,尽管因为转发机制而不报错。
如果你想让别人以为Warrior
继承了Diplomat
的negotiate
方法,就得复写respondsToSelector:
和isKindOfClass:
来加入你的转发算法。
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([super respondsToSelector:aSelector]) {
return YES;
}else{
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */
}
return NO;
}
除此之外,instancesRespondToSelector:
也应该写转发算法。如果使用了协议,conformsToProtocol:
也应该加入到这个队列中。类似的,如果一个对象转发了它接受的任何远程消息,它得给出一个methodSignatureForSelector
来返回准确的方法描述,这个方法最终会响应被转发的消息。比如一个对象给它的替代者发消息,需要下面的的实现
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}
return signature;
}
8、健壮的实例变量(Non Fragile ivars)
在现在的runtime版本中,健壮的实例变量算是最大的特点。当一个类被编译过后,实例变量的的布局也就确定了。它表明访问类的实例变量的位置,从对象头部开始,实例变量依据自己所占的空间依次发生位移。
![](https://i-blog.csdnimg.cn/blog_migrate/4e62edb549915b1129be0235172aaae4.webp?x-image-process=image/format,png)
上图左边的是NSObject的实例变量布局图,右边是我们自己写的类的实例变量布局图,也就是在超类的实例变量后面添加了我们自己写的类的实例变量,但假如哪天NSObject发布新版本的时候,就会出现问题
![](https://i-blog.csdnimg.cn/blog_migrate/1e622510a63d17d6fe117211de2803cb.webp?x-image-process=image/format,png)
NSObject的实例变量就会把我们自己的实例变量给覆盖,只有apple把布局改为之前的就可以处理这个问题,但这样也不能扩展他们的框架了,因为成员变量被固定起来。在Fragile ivars 环境下我们需要重新编译继承自NSObject的类来恢复兼容性,那么在健壮的实例变量情况下,
![](https://i-blog.csdnimg.cn/blog_migrate/5e6eefe11dfc60c0f3e58e6b380b91bd.webp?x-image-process=image/format,png)
健壮的实例变量下编译器生成的实例变量布局跟以前一样,但是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。
需要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
来代替。
总结
runtime是一个很有意思的东西,有了它我们能做好多事情。在看一下开源的第三方库,里面大部分都是用runtime相关的知识来写的,作为一个apple developer ,学会并弄懂Cocoa的底层实现原理也是很有必要的,本文参考了一些大牛的博客,也看了官网对runtime的讲解。还是从头到尾码一遍才有了深刻的了解,runtime不只是本文这些知识,还有一些其他相关的知识,比如Objective-C Associated Objects
、Method Swizzling
已经runtime的常用方法等,后面还会去学习并介绍。
本文参考了这些博客或者文献:
Objective-C Runtime Programming Guide
在查看本文的时候,如果发现错误或者有什么交流的,可以给我联系:ls_xyq@126.com.本文的Demo文件已放到GitHub上面:runtime详解 。
原文链接:http://www.jianshu.com/p/ea1743715609
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。