首先我们先看看文档如何描述Runtime的,如下:
The Objective-C language defers as many decisions as it can from compile time and link time to runtime. Whenever possible, it does things dynamically. This means that the language requires not just a compiler, but also a runtime system to execute the compiled code. The runtime system acts as a kind of operating system for the Objective-C language; it’s what makes the language work.
大概的意思就是:OC语言尽可能的动态地
处理事情,将决定尽可能地从编译时
和链接时
推迟到运行时
。意味着,OC语言不仅仅需要一个编译器,还需要一个运行时系统
执行已经编译好的代码。
下面将从两个方面学习Runtime:
- OC 运行时系统是如何工作的?
- 如何使用运行时系统?
补充:
其实发展到现在,Runtime技术已经发展了两个版本,一个OC2.0以后的modern
版本,和之前legacy
版本。
这两个版本的主要区别是:
- 类中实例变量的排序改变后,是否需要重新编译该类的子类,在
legacy
版本是必须的,而modern
则不必须。 modern
版本支持@property属性
支持的平台:
modern
:iPhone系统 && 64-bit OS X v10.5后的系统
legacy
:32bit OS X 系统
与运行时系统交互的的方式( 详细的交互在后面章节再说明 )
分为三个不同的层次:
- Objective-C 源码
- NSObject 的方法
- 直接调用 runtime 函数
Objective-C 源码
当编译器编译OC的类和方法时,会生成数据结构
和函数调用
实现语言的动态特性。
-
数据结构
在哪里获取到信息?- 类和分类定义 / 协议声明
- 类对象和协议对象
- 方法选择器( method selector )
- 实例变量模板
- 源码提取到的其它信息
-
函数调用
:- 最主要的运行时函数就是发送消息,由源码的消息表达式调用。例如:[dog run]这个表达式会调用运行时的
发送消息函数
- 最主要的运行时函数就是发送消息,由源码的消息表达式调用。例如:[dog run]这个表达式会调用运行时的
NSObject 的方法
其中一些方法可以简单查询运行时系统获取信息。这些方法可以让对象实现自省
+(Class)class // 获得类对象
-(BOOL)isKindOfClass:(Class)aClass // 是否是指定类或者其子类
-(BOOL)isMemberOfClass:(Class)aClass // 是否是指定类
-(BOOL)respondsToSelector:(SEL)aSelector // 是否实现指定方法或者其父类实现
+(BOOL)conformsToProtocol:(Protocol *)aProtocol // 类是否实现了指定协议
-(IMP)methodForSelector:(SEL)aSelector // 定位和返回接收者方法实现的地址,所以可以像函数调用一样
Cocoa中大多数的类都继承于NSObject( 特例:NSProxy ),所以大多数类都拥有上述自省
方法。在NSObject中,大多数方法只是简单地实现,例如:
+(NSString *)description // 默认返回类名和地址
详细实现功能还需自己重写。
直接调用 runtime 函数
运行时系统是一个动态共享库
,/usr/include/objc 目录下提供了一套函数
和数据结构
的公共接口。
补充:
-
什么是selector?
一个对象选择一个方法执行的名称
,又或者是源码编译后,取代这个名称
的唯一标识
。 -
selector的作用:
在OC源码阶段
,selector一般当前是方法的唯一标识。而在运行时
,selector就作为一个动态函数指针
,根据给的方法名指向相对应类中的方法的具体实现
。 -
获得一个selector:
编译好的类型是:SEL
,有两种途径可以获得编译时
,使用@selector
指令:SEL aSelector = @selector(methodName);
运行时
,使用NSSelectorFromString
函数SEL aSelector = NSSelectorFromString(@"methodName");
-
调用selector:
使用performSelector:方法SEL aSelector = @selector(run); // 运行时会自动指向 对应类 的方法实现
[aDog performSelector:aSelector];
[anAthlete performSelector:aSelector];
消息机制
在Objective-C中,最重要的就是消息发送机制。本文从两个方面深入学习该机制:
- 消息表达式如何转换为调用
objc_msgSend
函数 && 如何通过方法名
发送消息? - 如何利用
objc_msgSend
函数 && 必要时如何避免动态绑定?
objc_msgSend
在Objective-C语言里,消息
直到运行时
才会绑定到具体的方法实现。编译器把消息表达式
[receiver message]
转化为调用消息发送函数
objc_msgSend
函数。该函数有两个主要的参数:- receiver:消息接收者
- selector:方法选择器
objc_msgSend(receiver, selector)
如果消息表达式有参数,则转化如下:
objc_msgSend(receiver, selector, arg1, arg2, ...)
动态绑定就是在消息发送函数
objc_msgSend
里实现的- 根据
receiver的类
找到selector
指向的方法的实现 - 调用找到的方法实现,把消息
接收对象
( 实际上传递的是对象数据的指针 )和方法的相关参数
传递进去 - 将执行方法实现后的返回值返回出去
其实当一个
对象
被创建、分配好内存、初始化实例变量时,第一个被创建的变量就是指向类结构体
的指针( isa 指针 )note:一个对象与在运行时系统上工作,isa 指针是必须有的。定义一个对象的结构体,必须与
struct objc_object { Class isa; }
相符合( 在objc/objc.h中定义 )。但是我们很少去定义这个结构体,因为NSObject
和NSProxy
中的alloc
和allocWithZone:
方法会调用class_createInstance
生成objc_object
如下整个消息发送的流程框架:实现了上面(1.)步骤
Messaging.png
-
在上面内容中已经知道,调用
发送消息函数
时,已经获得对象数据的指针
。通过该指针,我们可以找到对象内存地址,从而找到isa指针
。由上图可知,获得isa指针后,我们可以遍历相关的类对象。每个类对象中都包含了一个方法列表
,配合已知receiver的类
,则可以定位到要调用的方法( 当前类对象方法列表没有,则寻找父类的,直到找到或者到NSObject为止 )。 -
上述整个过程就是
动态绑定
的过程( OOP编程 )
补充:
对象方法
存储在类对象
的方法列表当中,而类方法
存储在元类( metaclass )对象
的方法列表当中- 其实为了加快整个消息发送的过程,运行时系统
缓存
了我们曾经使用的方法名和方法地址( 如方法列表中的 selector ... address )。每个类的缓存是分开的,包括重写了的父类方法。在查找方法列表之前,会先查找缓存。如果方法已经缓存,发送消息只是比直接调用函数慢一点。
使用隐藏参数
由上面内容可以,
objc_msgSend
函数实现中,当找到方法的实现后,会执行该实现,并传入相关的对象和参数。而相关对象就是:- 消息接收的对象 ( self )
- 方法的动态指针 ( _cmd )
note:为什么叫隐藏参数?因为在方法声明实现时,在参数列表中,我们没有明显的写出来
如下( strange方法动态绑定大概实现猜测 ):
- strange { id target = getTheReceiver(); SEL method = getTheMethod(); if ( target == self || method == _cmd ) return nil; return [target performSelector:method]; // 根据方法名调用方法 }
获取方法的地址
想要避免动态绑定,唯一方法就是获得获得方法地址,然后想函数一样调用它。一般这种情况很少用,例如想
大量重复调用
方法,而你又减少动态绑定的资源开销。- 使用
NSObject
的methodForSelector:
方法可以根据方法名获得方法的地址
例子:
setFilled: (BOOL)
重复调用// - (IMP) methodForSelector:(SEL)aSelector 返回的是类型是IMP // id (*IMP)(id, SEL, ...):方法实现对应的C语言函数的函数指针 void (*setter)(id, SEL, BOOL); // 函数指针定义必须与方法实现的参数一一对应 int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES);
- 定义
IMP
函数指针时,隐藏参数不能省略,方法参数需必须传入。- id:消息接收对象
- SEL:方法选择器
- BOOL:
setFilled:
的参数
其实如果不是大量的循环发送消息,这种资源的浪费是很少的,所以没必要去主动去避免动态绑定。
本文介绍runtime的两个进阶用法:
- 动态方法解析:如何动态地提供
方法的实现
? - 消息转发:发送消息时,如果消息接收者没有实现该方法,则运行时会报错。当消息接收者没有实现该方法时,应该如何实现将消息转发给
另一个
接收者?
动态方法解析
有一些情况,可能是需要动态提供方法的实现的,如下:
- 编译器指令
@dynamic
:在 xcode4.5 之前,@property
只是声明了 setter/getter 方法,如果不使用@synthesize
,则必须自己提供 setter/getter 或者 getter 方法。另外一个就是@dynamic
,但是需动态添加 getter/setter 方法的实现。
如何给指定方法动态添加实现?
-
在
resolveInstanceMethod:
和resolveClassMethod:
中根据给定的方法名,使用class_addMethod( )
把函数作为一个方法添加到类里。void dynamicMethodIMP(id self, SEL _cmd) { // 若方法有参数,则需在后面添加相应参数 // implementation ... 方法的实现 }
-
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
- cls:需要添加方法的类
- name:方法名
- imp:方法实现对应的函数,必须有 self 和 _cmd 两个参数
- types:C语言字符串,描述实现函数对应的参数类型( 包括返回值,第1个为描述返回值,因为2、3分别是id self,SEL _cmd,所以必须为"@:" )
@implementation MyClass + (BOOL)resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { // 是否为resolveThisMethodDynamically方法 class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); // 动态添加方法 return YES; // 返回YES,则不会转发 } return [super resolveInstanceMethod:aSEL]; // 不是需要动态添加的方法,按照父类处理 } @end
动态添加方法实现与消息的转发是不可以同时存在的,动态添加优先于消息转发。如果实现了
resolveInstanceMethod:
和resolveClassMethod:
,并且返回YES,则不会进行消息的转发。如果没有实现,则转发消息。补充:动态加载
Objective-C的程序允许在运行时加载和链接新的类与分类,此时加载与开始时加载无异。
动态加载类与分类一个比较重要的应用就是加载动态库
。- 使用方法:
- 使用运行时函数
objc_loadModules
( 在objc/objc-load.h
头文件中定义 ),动态加载Mach-O
文件里的Objective-C的功能模块(动态库) - NSBundle类中提供了简便的接口进行动态加载
Mach-O
文件
- 使用运行时函数
Mach-O
文件:是Mac和iOS系统的可执行二进制文件,类似于window中的.exe文件消息转发
当给对象发送消息时,如果该对象没有实现相应的方法处理该消息,则会调用
-forwardInvocation:
方法。NSObject类中实现了该方法,默认调用-doesNotRecognizeSelector:
方法,所以如果继承于NSObject的类接收没有实现的消息,则会报运行时错误
。
所以,在-forwardInvocation:
方法中实现消息转发即可。-
转发一条消息,在
-forwardInvocation:
方法中需要做的事情:- 决定把消息转发给谁
-
传递原始的参数
在
-forwardInvocation:
方法中,会传递给该方法唯一一个参数,NSInvocation的对象( 包含了原始的消息和参数 )- (void)forwardInvocation:(NSInvocation *)anInvocation { if ([someOtherObject respondsToSelector:[anInvocation selector]]) // 如果指定对象实现了该方法 [anInvocation invokeWithTarget:someOtherObject]; // 给指定对象发送消息 else [super forwardInvocation:anInvocation]; // 按照父类处理 }
转发与多重继承
如下图所示:
转发.png
Warrior类没有实现
negotiate
方法。当接收negotiate
消息时,会调用Diplomat的对象方法,并且会传递原始参数。所以,这个过程类似于继承调用方法的过程。而Warrior本来就有父类,所以这种现象相当于使用转发机制
“ 实现了“多重继承。转发与继承的对比:
继承是把所有父类的“能力”都集中到一个类中,而转发只是仅仅调用了一下方法,把问题分离出去。在一定程度上还是有很大区别的。转发与继承
转发是模仿继承的。但是NSObject永远不会混淆这两个概念。就好像方法
-respondsToSelector:
和-isKindOfClass:
只会在继承层次中有效,而在转发链中是无效的,永远只会返回NO
。但是有些情况下,你也想转发链中返回的是
YES
。那样就必须重写-respondsToSelector:
和-isKindOfClass:
方法。如下重写
-respondsToSelector:
方法:- (BOOL)respondsToSelector:(SEL)aSelector { if ( [super respondsToSelector:aSelector] ) // 如果父类实现了,则返回YES 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; }
当你如果想将转发完全模仿了继承,重写
-respondsToSelector:
和-isKindOfClass:
方法是远远不够的。以下是大概上还需要重写的方法:
instancesRespondToSelector:
- 如果使用了协议,则需重写
conformsToProtocol:
- 重写
methodSignatureForSelector:
方法,返回转发后的方法描述- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { NSMethodSignature* signature = [super methodSignatureForSelector:selector]; // 如果父类实现了该方法,才会有方法的描述,返回父类的即可 if (!signature) { // 若没有,则返回转发对象的 signature = [surrogate methodSignatureForSelector:selector]; } return signature; }
note:这是一个高级别的技术点,当真的没有其它解决方案时才会考虑使用转发机制。千万不要用来替代继承,先考虑使用继承。如果真的需要使用,要保证自己对
实行转发
和接收转发
这两个类的所有行为有一个完全的理解。
- 消息表达式如何转换为调用