Objective-C runtime之运行时的基本特点(一)
作为一门动态编程语言,Objective-C 会尽可能的将编译和链接时要做的事情推迟到运行时。只要有可能,Objective-C
总是使用动态 的方式来解决问题。这意味着 Objective-C 语言不仅需要一个编译环境,同时也需要一个运行时系统来执行编译好的代码。运行时系统(runtime)扮演的角色类似于
Objective-C 语言的操作系统,Objective-C 基于该系统来工作。因此,runtime好比Objective-C的灵魂,很多东西都是在这个基础上出现的。所以它是值的你花功夫去理解的。
我们将从以下几个方面了解Objective-C的运行时:
一、与静态语言编译后的区别
1、静态语言
一个静态语言程序,如下所示的C程序:
#include < stdio.h > int main(int argc, const char **argv[]) { printf("Hello World!"); return 0; } |
会经过编译器的语法分析,优化然后将你最佳化的代码翻译成汇编语言,然后完全按照你设计的逻辑和你的代码自上而下的执行。
2、Objective-C
很常见的一个消息发送语句:
会被编译器转化成
objc_msgSend(receiver, selector) |
如果有参数则为
objc_msgSend(receiver, selector, arg1, arg2, …) |
消息只有到运行时才会和函数实现绑定起来,而不是按照编译好的逻辑一成不变的执行。按照我的理解,编译阶段只是确定了要去向receiver对象发送message消息,但是却没有发送,真正发送是等到运行的时候进行。因此,编译阶段完全不知道message方法的具体实现,甚至,该方法到底有没有被实现也不知道。这就有可能导致运行时崩溃问题。
二、Objective-c runtime的几点说明
1、runtime是开源的
是的,你没看错,runtime确实是开源的。目前苹果公司和GNU各自维护一个开源的runtime版本,这两个版本之间都在努力的保持一致。其中苹果的版本可以猛击该链接下载objc4-437.1.tar.gz
2、runtime是由C语言实现的
runtime做为Objective-C最核心的部分,几乎全部由C语言实现。这里的“几乎”所指的例外就包含有的方法(比如下面要说道的objc_msgSend方法)甚至是用汇编实现的!!
3、runtime的两个版本
Objective-C运行时系统有两个已知版本:早期版本(Legacy)和现行版本(Modern)。
在现行版本中,最显著的新特性就是实例变量是"健壮“(non-fragile)的:
在早期版本中,如果您改变类中实例变量的布局,您必须重新编译该类的所有子类。
在现行版本中,如果您改变类中实例变量的布局,您无需重新编译该类的任何子类。
此外,现行版本支持声明property的synthesis属性器。
目前iPhone 程序和 Mac OS X v10.5 及以后的系统中的
64 位程序使用的都是 Objective-C 运行时系统的现行版 本。其它情况(Mac OS X 系统中的
32 位程序)使用的是早期版本。
三、和runtime system交互的三种方式
1、通过Objective-C源代码
大部分情况下,运行时系统在后台自动运行,我们只需编写和编译 Objective-C
源代码。
当编译Objective-C类和方法时,编译器为实现语言动态特性将自动创建一些数据结构和函数。这些数据
结构包含类定义和协议类定义中的信息,如在Objective-C 2.0 程序设计语言中定义类和协议类一节所讨论
的类的对象和协议类的对象,方法选标,实例变量模板,以及其它来自于源代码的信息。运行时系统的主要功能就是根据源代码中的表达式发送消息。
2、通过类NSObject的方法
Cocoa程序中绝大部分类都是NSObject类的子类,所以大部分都继承了NSObject类的方法,因而继承
了NSObject的行为(NSProxy类是个例外)。然而,某些情况下, NSObject类仅仅定义了完成某件事情的模板,而没有提供所有需要的代码。
例如,NSObject 类定义了description方法,返回该类内容的字符串表示。这主要是用来调试程序
——GDB 中的 print-object 方法就是直接打印出该方法返回的字符串。NSObject 类中该方法的
实现并不知道子类中的内容,所以它只是返回类的名字和对象的地址。NSObject 的子类可以重新实现该方法以提供更多的信息。例如,NSArray
类改写了该方法来返回 NSArray 类包含的每个对象的内容。
某些 NSObject 的方法只是简单地从运行时系统中获得信息,从而允许对象进行一定程度的自我检查。
例如,class 返回对象的类;isKindOfClass:和 isMemberOfClass:则检查对象是否在指定的
类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:
检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。
3、通过运行时系统的函数
运行时系统是一个有公开接口的动态库,由一些数据结构和函数的集合组成,这些数据结构和函数的声明 头文件在/usr/include/objc中。这些函数支持用纯C的函数来实现和Objective-C同样的功能。还有一些函数构成了
NSObject 类方法的基础。这些函数使得访问运行时系统接口和提供开发工具成为可 能。尽管大部分情况下它们在
Objective-C 程序不是必须的,但是有时候对于 Objecitve-C 程序来说某些函 数是非常有用的。
这些函数的文档参见 Objective-C 2.0 运行时系统参考库。
Objective-C runtime之消息(二)
今天开始说说runtime system中最关键的消息相关内容。
一、runtime中的消息
1、什么是消息
进入今天的正题之前,先来说说跟message息息相关的几个概念
①message(消息)
message的具体定义很难说,因为并没有真正的代码描述,简单的讲message
是一种抽象,包括了函数名+参数列表,他并没有实际的实体存在。
②method(方法)
method是真正的存在的代码。如:- (int)meaning { return
42; }
③selector(方法选择器)
selector 通过SEL类型存在,描述一个特定的method 或者说
message。在实际编程中,可以通过selector进行检索方法等操作。
2、两个跟消息相关的概念
①SEL
SEL又叫方法选择器,这到底是个什么玩意呢?在objc.h中是这样定义的:
typedef struct objc_selector *SEL; |
这个SEL表示什么?首先,说白了,方法选择器仅仅是一个char *指针,仅仅表示它所代表的方法名字罢了,有如下证据:
SEL selector = @selector(message); //@selector不是函数调用,只是给这个坑爹的编译器的一个提示 NSLog (@"%s", (char *)selector); //print message |
这时打印的结果就是:message
Objective-C在编译的时候,会根据方法的名字,生成一个用 来区分这个方法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字相同,那么它们的ID都是相同的。就是说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同那么ID就是一样的。
而这也就导致了Objective-C在处理有相同函数名和参数个数但参数类型不同的函数的能力非常的弱,比如当你想在程序中实现下面两个方法:
-(void)setWidth:(int)width; -(void)setWidth:(double)width; |
这样的函数则被认为是一种编译错误,而这最终导致了一个非常非常奇怪的Objective-C特色的函数命名:
-(void)setWidthIntValue:(int)width; -(void)setWidthDoubleValue:(double)width; |
可能有人会问,runtime费了那么老半天劲,究竟想做什么?GC来了。
刚才我们说道,编译器会根据每个方法的方法名为那个方法生成唯一的SEL,这些SEL组成了一个Set集合,这个Set简单的说就是一个经过了优化过的hash表。而Set的特点就是唯一,也就是SEL是唯一的,因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,犀利,速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect
hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。
到这里,我们明白了,本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度!!!!
②IMP
IMP在objc.h中是如此定义的:
typedef id (*IMP)(id, SEL, ...); |
这个比SEL要好理解多了,熟悉C语言的同学都知道,这其实是一个函数指针。前面介绍过的SEL,就是为IMP服务的。由于每个方法都对应唯一的SEL,因此我们可以通过SEL方便、快速、准确的获得它所对应的IMP(也就是函数指针),而在取得了函数指针之后,也就意味着我们取得了执行的时候的这段方法的代码的入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。当然我们可以把函数指针作为参数传递到其他的方法,或者实例变量里面,从而获得极大的动态性。
下面的例子,介绍了取得函数指针,即函数指针的用法:
void (* performMessage)(id,SEL);//定义一个IMP(函数指针) performMessage = (void (*)(id,SEL))[self methodForSelector:@selector(message)];
//通过methodForSelector方法根据SEL获取对应的函数指针 performMessage(self,@selector(message));
//通过取到的IMP(函数指针)跳过runtime消息传递机制,直接执行message方法 |
用IMP 的方式,省去了runtime消息传递过程中所做的一系列动作,比直接向对象发送消息高效一些。
3、传递消息所用的几个runtime方法
上篇文章中我们说过,下面的方法:
在编译后会变成:
objc_msgSend(receiver, selector) |
实际上,同objc_msgSend方法类似的还有几个:
objc_msgSend_stret(返回值是结构体) objc_msgSend_fpret(返回值是浮点型) objc_msgSendSuper(调用父类方法) |
它们的作用都是类似的,为了简单起见,后续介绍消息和消息传递机制都以objc_msgSend方法为例。
二、消息调用流程
一切还是从消息表达式[receiver message]开始,在被转换成objc_msgSend(receiver,
SEL)后,在运行时,runtime system会做以下事情:
1、检查忽略的Selector,比如当我们运行在有垃圾回收机制的环境中,将会忽略retain和release消息。
2、检查receiver是否为nil。不像其他语言,nil在objective-C中是完全合法的,并且这里有很多原因你也愿意这样,比如,至少我们省去了给一个对象发送消息前检查对象是否为空的操作。如果receiver为空,则会将
selector也设置为空,并且直接返回到消息调用的地方。如果对象非空,就继续下一步。
3、接下来会根据SEL到当前类中查找对应的IMP,首先会在cache中检索它,如果找到了就根据函数指针跳转到这个函数执行,否则进行下一步。
4、检索当前类对象中的方法表(method list),如果找到了,加入cache中,并且就跳转到这个函数之行,否则进行下一步。
5、从父类中寻找,直到根类:NSObject类。找到了就将方法加入对应类的cache表中,如果仍为找到,则要进入后文介绍的内容:动态方法决议。
6、如果动态方法决议仍不能解决问题,只能进行最后一次尝试,进入消息转发流程。
7、如果还不行,去死吧。
下面的图部分展示了这个调用过程:
写到这大家肯定会发出这样的疑问:我仅仅想调用一个方法而已,却不得不经历那么多步骤,效率上怎么保证??苹果也做了一些优化上的工作。
三、函数检索优化措施
主要从下面两个方面着手:
1、通过SEL进行IMP匹配
先来看看类对象中保存的方法列表和方法的数据结构:
typedef struct method_list_t { uint32_t entsize_NEVER_USE; uint32_t count; struct method_t first; } method_list_t; typedef struct method_t { SEL name; const char *types;//参数类型和返回值类型 IMP imp; } method_t; |
在前一篇文章介绍SEL的时候,我们已经说过了苹果在通过SEL检索IMP时做的努力,这里不再累述。
2、cache缓存
cache的原则就是缓存那些可能要执行的函数地址,那么下次调用的时候,速度就可以快速很多。这个和CPU的各种缓存原理相通。好吧,说了这么多了,再来认识几个名词:
struct objc_cache { uintptr_t mask; uintptr_t occupied; cache_entry *buckets[1]; }; typedef struct { SEL name; void *unused; IMP imp; } cache_entry; |
看这个结构,有没有搞错又是hash table。
objc_msgSend 首先在cache list 中找SEL,没有找到就在class
method中找,super class method中找(当然super class 也有cache
list)。而cache的机制则非常复杂了,由于Objective-C是动态语言。所以,这里面还有很多的多线程同步问题,而这些锁又是效率的大敌,相关的内容已经远远超过本文讨论的范围。
如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加。据牛人说(没有亲测过),苹果通过这些优化,使消息传递和直接的函数调用效率上的差距已经相当的小。
四、方法调用中的隐藏参数
亲爱的Objective-C程序员们,你们在进行面向对象编程的时候,在实例方法中都是用过self关键字吧,可是你有没有想过,为什么在一个实例方法中,通过self关键字就能取到调用当前方法的对象呢?这就要归功与runtime
system消息的隐藏参数了。(注:在此修正,类方法和实例方法中,都可以访问self和_cmd这两个属性,因为它们都不属于类的实例变量,而是形参!!!!误导大家了,深表歉意!!!!)
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
1.接收消息的对象(也就是self指向的内容)
2.方法选标(_cmd指向的内容)
这些参数帮助方法实现获得了消息表达式的信息。它们被认为是”隐藏“的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就象可以引用消息接收者对象的实例变
量一样)。在方法中可以通过 self 来引用消息接收者对象,通过选标_cmd 来引用方法本身。下面的例子很好的说明了这个问题:
- (void)message { self.name = @"James";//通过self关键字给当前对象的属性赋值 SEL currentSel = _cmd;//通过_cmd关键字取到当前函数对应的SEL NSLog(@"currentSel is :%s",(char *)currentSel); } |
打印结果:
ObjcRunTime[693:403] currentSel is :message |
当然,在这两个参数中,self 更有用,更常用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
Objective-C runtime之消息转发机制(三)
学了那么久的Objective-C,给我的感觉就是它什么都是动态的,你将会听到一个新的名词:
一、动态方法解析
1、+(BOOL) resolveInstanceMethod:(SEL)
sel
这是NSObject根类提供的类方法,调用时机为当被调用的方法实现部分没有找到,而消息转发机制启动之前的这个中间时刻。
2、@dynamic关键字
Objective-C2.0 提供了@dynamic关键字。这个关键字有两个作用:
①告诉编译器不要创建实现属性所用的实例变量;
②告诉编译器不要创建该属性的get和setter方法。
如果我们在@interface接口文件中声明了一个属性,如下所示:
@property(nonatomic,retain) NSString *name; |
默认情况下,编译器会为当前类自动生成一个NSString *_name的实例变量(如果想改变实例变量的名称可以用@synthesize关键字),同时会生成两个名为-
(NSString *)name和- (void)setName:(NSString *)aName的存取方法。
而@dynamic关键字就是告诉编译器不要做这些事,同时在使用了存取方法时也不要报错,即让编译器相信存取方法会在运行时找到。
比如在@implementation文件中做了如下声明:
如果使用了name属性的setter方法,又不想在运行时崩溃,就可以在运行时做点动作:
void dynamicMethodIMP(id self, SEL _cmd) { // implementation .... } + (BOOL)resolveInstanceMethod:(SEL)sel { NSLog(@"sel is %@", NSStringFromSelector(sel)); if(sel == @selector(setName:)){ class_addMethod([self class],sel,(IMP)dynamicMethodIMP,"v@:"); return YES; } return [super resolveInstanceMethod:sel]; } |
在resolveInstanceMethod的实现中,我们通过class_addMethod方法动态的向当前对象增加了dynamicMethodIMP函数,来代替-(void)setName:(NSString
*)name的实现部分,从而达到了动态生成name属性方法的目的。
值得说明的是:
①在上个例子中,我们自己实现了-(void)setName:(NSString *)name方法,则在运行的时候,调用完我们实现的-(void)setName:(NSString
*)name方法后,运行时系统仍然会调+(BOOL) resolveInstanceMethod:(SEL)
sel方法,只不过这里的sel会变成_doZombieMe,从而我们实现重定向的if分支就进不去了,即我们实现的方法不会被覆盖。
②"v@:"属于Objective-C类型编码的内容,感兴趣的同学可以自己google一下。
二、runtime system消息转发机制
对象是谦恭的,它会接收所有发送过来的消息,哪怕这些消息自己无法响应。问题来了:当对象无法响应这些消息时怎么办?runtime提供了消息转发机制来处理该问题。
当外部调用的某个方法对象没有实现,而且resolveInstanceMethod方法中也没有做重定向处理时,就会触发-
(void)forwardInvocation:(NSInvocation *)anInvocation方法。在该方法中,可以实现对不能处理的消息做的一些默认处理,也可以以其它的某种方式来避免错误被抛出。像forwardInvocation:的名字一样,这个方法通常用来将不能处理的消息转发给其它的对象。通常我们重写该方法的方式如下所示:
-(void)forwardInvocation:(NSInvocation *)invocation { SEL invSEL = invocation.selector; if ([someOtherObject respondsToSelector:invSEL]) [anInvocation invokeWithTarget:someOtherObject]; } else { [self doesNotRecognizeSelector:invSEL]; } } |
怎么看着有点像多继承呀???你说对了,消息转发提供了多重继承的很多特性。然而,两者有很大的不同:多重继承是将不同的行为封装到单个的对象中,有可能导致庞大的,复杂的对象。而消息转发是将问题分解到更小的对象中,但是又以一种对消息发送对象来说完全透明的方式将这些对象联系起来。总之,Objective-C通过这种方式,一定程度上减小了自己不支持多继承的劣势。
经过半个月的时间,自己总结、整理出了这三篇文章,到这里,对Objective-C运行时的学习算是告一段落了。文笔的原因,文章结构不是很清晰,还请见谅。对运行时理解不到位,或者是有错误的地方,还请广大博友指出,感激不尽!
|