本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具有了灵活的动态特性,使这门古老的语言焕发生机。主要内容以下:html
引言
曾经以为Objc特别方便上手,面对着 Cocoa 中大量 API,只知道简单的查文档和调用。还记得初学 Objective-C 时把 [receiver message]
当成简单的方法调用,而无视了“发送消息”这句话的深入含义。其实 [receiver message]
会被编译器转化为:node
1 |
objc_msgSend(receiver, selector) |
若是消息含有参数,则为:ios
1 |
objc_msgSend(receiver, selector, arg1, arg2, ...) |
若是消息的接收者可以找到对应的 selector
,那么就至关于直接执行了接收者这个对象的特定方法;不然,消息要么被转发,或是临时向接收者动态添加这个 selector
对应的实现内容,要么就干脆玩完崩溃掉。git
如今能够看出 [receiver message]
真的不是一个简简单单的方法调用。由于这只是在编译阶段肯定了要向接收者发送 message
这条消息,而 receive
将要如何响应这条消息,那就要看运行时发生的状况来决定了。程序员
Objective-C 的 Runtime 铸就了它动态语言的特性,这些深层次的知识虽然平时写代码用的少一些,可是倒是每一个 Objc 程序员须要了解的。github
简介
由于Objc是一门动态语言,因此它老是想办法把一些决定工做从编译链接推迟到运行时。也就是说只有编译器是不够的,还须要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个 Objc 运行框架的一块基石。objective-c
Runtime其实有两个版本: “modern” 和 “legacy”。咱们如今用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 以后的 64 位程序中。而 maxOS 较老的32位程序仍采用 Objective-C 1 中的(早期)Legacy 版本的 Runtime 系统。这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你须要从新编译它的子类,而现行版就不须要。算法
Runtime 基本是用 C 和汇编写的,可见苹果为了动态系统的高效而做出的努力。你能够在这里下到苹果维护的开源代码。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。编程
与 Runtime 交互
Objc 从三种不一样的层级上与 Runtime 系统进行交互,分别是经过 Objective-C 源代码,经过 Foundation 框架的NSObject
类定义的方法,经过对 runtime 函数的直接调用。数组
Objective-C 源代码
大部分状况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳做着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而建立的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(好比 objc_msgSend
函数及其参数列表中的 id
和 SEL
都是啥)
NSObject 的方法
Cocoa 中大多数类都继承于 NSObject
类,也就天然继承了它的方法。最特殊的例外是 NSProxy
,它是个抽象超类,它实现了一些消息转发有关的方法,能够经过继承它来实现一个其余类的替身类或是虚拟出一个不存在的类,说白了就是领导把本身展示给你们风光无限,可是把活儿都交给幕后小弟去干。
有的NSObject
中的方法起到了抽象接口的做用,好比description
方法须要你重载它并为你定义的类提供描述内容。NSObject
还有些方法能在运行时得到类的信息,并检查一些特性,好比class
返回对象的类;isKindOfClass:
和isMemberOfClass:
则检查对象是否在指定的类继承体系中;respondsToSelector:
检查对象可否响应指定的消息;conformsToProtocol:
检查对象是否实现了指定协议类的方法;methodForSelector:
则返回指定方法实现的地址。
Runtime 的函数
Runtime 系统是一个由一系列函数和数据结构组成,具备公共接口的动态共享库。头文件存放于/usr/include/objc
目录下。许多函数容许你用纯C代码来重复实现 Objc 中一样的功能。虽然有一些方法构成了NSObject
类的基础,可是你在写 Objc 代码时通常不会直接用到这些函数的,除非是写一些 Objc 与其余语言的桥接或是底层的debug工做。在 Objective-C Runtime Reference 中有对 Runtime 函数的详细文档。
Runtime 基础数据结构
还记得引言中的objc_msgSend:
方法吧,它的真身是这样的:
1 |
id objc_msgSend ( id self, SEL op, ... ); |
下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。熟悉 Objective-C 类的内存模型或看过相关源码的能够直接跳过。
SEL
objc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objc中的表示类型(Swift中是Selector
类)。selector
是方法选择器,能够理解为区分方法的 ID,而这个 ID 的数据结构是SEL
:
1 |
typedef struct objc_selector *SEL; |
其实它就是个映射到方法的C字符串,你能够用 Objc 编译器命令 @selector()
或者 Runtime 系统的 sel_registerName
函数来得到一个 SEL
类型的方法选择器。
不一样类中相同名字的方法所对应的方法选择器是相同的,即便方法名字相同而变量类型不一样也会致使它们具备相同的方法选择器,因而 Objc 中方法命名有时会带上参数类型(NSNumber
一堆抽象工厂方法拿走不谢),Cocoa 中有好多长长的方法哦。
id
objc_msgSend
第一个参数类型为id
,你们对它都不陌生,它是一个指向类实例的指针:
1 |
typedef struct objc_object *id; |
那objc_object
又是啥呢,参考 objc-private.h 文件部分源码:
1 |
struct objc_object { |
objc_object
结构体包含一个 isa
指针,类型为 isa_t
联合体。根据 isa
就能够顺藤摸瓜找到对象所属的类。isa
这里还涉及到 tagged pointer 等概念。由于 isa_t
使用 union
实现,因此可能表示多种形态,既能够当成是指针,也能够存储标志位。有关 isa_t
联合体的更多内容能够查看 Objective-C 引用计数原理。
PS: isa
指针不老是指向实例对象所属的类,不能依靠它来肯定类型,而是应该用 class
方法来肯定实例对象的类。由于KVO的实现机理就是将被观察对象的 isa
指针指向一个中间类而不是真实的类,这是一种叫作 isa-swizzling 的技术,详见官方文档
Class
Class
实际上是一个指向 objc_class
结构体的指针:
1 |
typedef struct objc_class *Class; |
而 objc_class
包含不少方法,主要都为围绕它的几个成员作文章:
1 |
struct objc_class : objc_object { |
objc_class
继承于 objc_object
,也就是说一个 ObjC 类自己同时也是一个对象,为了处理类和对象的关系,runtime 库建立了一种叫作元类 (Meta Class) 的东西,类对象所属类型就叫作元类,它用来表述类对象自己所具有的元数据。类方法就定义于此处,由于这些方法能够理解成类对象的实例方法。每一个类仅有一个类对象,而每一个类对象仅有一个与之相关的元类。当你发出一个相似 [NSObject alloc]
的消息时,你事实上是把这个消息发给了一个类对象 (Class Object) ,这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类 (root meta class) 的实例。全部的元类最终都指向根元类为其超类。全部的元类的方法列表都有可以响应消息的类方法。因此当 [NSObject alloc]
这条消息发给类对象的时候,objc_msgSend()
会去它的元类里面去查找可以响应消息的方法,若是找到了,而后对这个类对象执行方法调用。
上图实线是 superclass
指针,虚线是isa
指针。 有趣的是根元类的超类是 NSObject
,而 isa
指向了本身,而 NSObject
的超类为 nil
,也就是它没有超类。
能够看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
cache_t
1 |
struct cache_t { |
_buckets
存储 IMP
,_mask
和 _occupied
对应 vtable
。
cache
为方法调用的性能进行优化,通俗地讲,每当实例对象接收到一个消息时,它不会直接在isa
指向的类的方法列表中遍历查找可以响应消息的方法,由于这样效率过低了,而是优先在 cache
中查找。Runtime 系统会把被调用的方法存到 cache
中(理论上讲一个方法若是被调用,那么它有可能从此还会被调用),下次查找的时候效率更高。
bucket_t
中存储了指针与 IMP 的键值对:
1 |
struct bucket_t { |
有关缓存的实现细节,能够查看 objc-cache.mm 文件。
class_data_bits_t
objc_class
中最复杂的是 bits
,class_data_bits_t
结构体所包含的信息太多了,主要包含 class_rw_t
, retain/release/autorelease/retainCount
和 alloc
等信息,不少存取方法也是围绕它展开。查看 objc-runtime-new.h 源码以下:
1 |
struct class_data_bits_t { |
注意 objc_class
的 data
方法直接将 class_data_bits_t
的data
方法返回,最终是返回 class_rw_t
,保了好几层。
能够看到 class_data_bits_t
里又包了一个 bits
,这个指针跟不一样的 FAST_
前缀的 flag 掩码作按位与操做,就能够获取不一样的数据。bits
在内存中每一个位的含义有三种排列顺序:
32 位:
0 | 1 | 2 - 31 |
---|---|---|
FAST_IS_SWIFT | FAST_HAS_DEFAULT_RR | FAST_DATA_MASK |
64 位兼容版:
0 | 1 | 2 | 3 - 46 | 47 - 63 |
---|---|---|---|---|
FAST_IS_SWIFT | FAST_HAS_DEFAULT_RR | FAST_REQUIRES_RAW_ISA | FAST_DATA_MASK | 空闲 |
64 位不兼容版:
0 | 1 | 2 | 3 - 46 | 47 |
---|---|---|---|---|
FAST_IS_SWIFT | FAST_REQUIRES_RAW_ISA | FAST_HAS_CXX_DTOR | FAST_DATA_MASK | FAST_HAS_CXX_CTOR |
48 | 49 | 50 | 51 | 52 - 63 |
FAST_HAS_DEFAULT_AWZ | FAST_HAS_DEFAULT_RR | FAST_ALLOC | FAST_SHIFTED_SIZE_SHIFT | 空闲 |
其中 64 位不兼容版每一个宏对应的含义以下:
1 |
// class is a Swift class |
这里面除了 FAST_DATA_MASK
是用一段空间存储数据外,其余宏都是只用 1 bit 存储 bool 值。class_data_bits_t
提供了三个方法用于位操做:getBit
,setBits
和 clearBits
,对应到存储 bool 值的掩码也有封装函数,好比:
1 |
bool isSwift() { |
重头戏在于最大的那块存储区域–FAST_DATA_MASK
,它其实就存储了指向 class_rw_t
的指针:
1 |
class_rw_t* data() { |
对这片内存读写处于并发环境,但并不须要加锁,由于会经过对一些状态(realization or construction)判断来决定是否可读写。
class_data_bits_t
甚至还包含了一些对 class_rw_t
中 flags
成员存取的封装函数。
class_ro_t
objc_class
包含了 class_data_bits_t
,class_data_bits_t
存储了 class_rw_t
的指针,而 class_rw_t
结构体又包含 class_ro_t
的指针。
class_ro_t
中的 method_list_t
, ivar_list_t
, property_list_t
结构体都继承自 entsize_list_tt<Element, List, FlagMask>
。结构为 xxx_list_t
的列表元素结构为 xxx_t
,命名很工整。protocol_list_t
与前三个不一样,它存储的是 protocol_t *
指针列表,实现比较简单。
entsize_list_tt
实现了 non-fragile 特性的数组结构。假如苹果在新版本的 SDK 中向 NSObject
类增长了一些内容,NSObject
的占据的内存区域会扩大,开发者之前编译出的二进制中的子类就会与新的 NSObject
内存有重叠部分。因而在编译期会给 instanceStart
和 instanceSize
赋值,肯定好编译时每一个类的所占内存区域起始偏移量和大小,这样只需将子类与基类的这两个变量做对比便可知道子类是否与基类有重叠,若是有,也可知道子类须要挪多少偏移量。更多细节能够参考后面的章节 Non Fragile ivars。
1 |
struct class_ro_t { |
class_ro_t->flags
存储了不少在编译时期就肯定的类的信息,也是 ABI 的一部分。下面这些 RO_
前缀的宏标记了 flags
一些位置的含义。其中后三个并不须要被编译器赋值,是预留给运行时加载和初始化类的标志位,涉及到与 class_rw_t
的类型强转。运行时会用到它作判断,后面会讲解。
1 |
|
class_rw_t
class_rw_t
提供了运行时对类拓展的能力,而 class_ro_t
存储的大可能是类在编译时就已经肯定的信息。两者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不一样。
class_rw_t
中使用的 method_array_t
, property_array_t
, protocol_array_t
都继承自 list_array_tt<Element, List>
, 它能够不断扩张,由于它能够存储 list 指针,内容有三种:
- 空
- 一个
entsize_list_tt
指针 entsize_list_tt
指针数组
class_rw_t
的内容是能够在运行时被动态修改的,能够说运行时对类的拓展大都是存储在这里的。
1 |
struct class_rw_t { |
class_rw_t->flags
存储的值并非编辑器设置的,其中有些值可能未来会做为 ABI 的一部分。下面这些 RW_
前缀的宏标记了 flags
一些位置的含义。这些 bool 值标记了类的一些状态,涉及到声明周期和内存管理。有些位目前甚至还空着。
1 |
|
demangledName
是计算机语言用于解决实体名称惟一性的一种方法,作法是向名称中添加一些类型信息,用于从编译器中向连接器传递更多语义信息。
realizeClass
在某个类初始化以前,objc_class->data()
返回的指针指向的实际上是个 class_ro_t
结构体。等到 static Class realizeClass(Class cls)
静态方法在类第一次初始化时被调用,它会开辟 class_rw_t
的空间,并将 class_ro_t
指针赋值给 class_rw_t->ro
。这种偷天换日的行为是靠 RO_FUTURE
标志位来记录的:
1 |
ro = (const class_ro_t *)cls->data(); |
注意以前 RO 和 RW flags 宏标记的一个细节:
1 |
|
也就是说 ro = (const class_ro_t *)cls->data();
这种强转对于接下来的 ro->flags & RO_FUTURE
操做彻底是 OK 的,两种结构体第一个成员都是 flags
,RO_FUTURE
与 RW_FUTURE
值同样的。
通过 realizeClass
函数处理的类才是『真正的』类,调用它时不能对类作写操做。
Category
Category
为现有的类提供了拓展性,它是 category_t
结构体的指针。
1 |
typedef struct category_t *Category; |
category_t
存储了类别中能够拓展的实例方法、类方法、协议、实例属性和类属性。类属性是 Objective-C 2016 年新增的特性,沾 Swift 的光。因此 category_t
中有些成员变量是为了兼容 Swift 的特性,Objective-C 暂没提供接口,仅作了底层数据结构上的兼容。
1 |
struct category_t { |
在 App 启动加载镜像文件时,会在 _read_images
函数间接调用到 attachCategories
函数,完成向类中添加 Category
的工做。原理就是向 class_rw_t
中的 method_array_t
, property_array_t
, protocol_array_t
数组中分别添加 method_list_t
, property_list_t
, protocol_list_t
指针。以前讲过 xxx_array_t
能够存储对应 xxx_list_t
的指针数组。
在调用 attachCategories
函数以前,会先使用 unattachedCategoriesForClass
函数获取类中还未添加的类别列表。这个列表类型为 locstamped_category_list_t
,它封装了 category_t
以及对应的 header_info
。header_info
存储了实体在镜像中的加载和初始化状态,以及一些偏移量,在加载 Mach-O 文件相关函数中常常用到。
1 |
struct locstamped_category_t { |
因此更具体来讲 attachCategories
作的就是将 locstamped_category_list_t.list
列表中每一个 locstamped_category_t.cat
中的那方法、协议和属性分别添加到类的 class_rw_t
对应列表中。header_info
中的信息决定了是不是元类,从而选择应该是添加实例方法仍是类方法、实例属性仍是类属性等。源码在 objc-runtime-new.mm 文件中,很好理解。
Method
Method
是一种表明类中的某个方法的类型。
1 |
typedef struct method_t *Method; |
而 objc_method
在上面的方法列表中提到过,它存储了方法名,方法类型和方法实现:
1 |
struct method_t { |
- 方法名类型为
SEL
,前面提到过相同名字的方法即便在不一样类中定义,它们的方法选择器也相同。 - 方法类型
types
是个char
指针,其实存储着方法的参数类型和返回值类型。 imp
指向了方法的实现,本质上是一个函数指针,后面会详细讲到。
Ivar
Ivar
是一种表明类中实例变量的类型。
1 |
typedef struct ivar_t *Ivar; |
而 ivar_t
在上面的成员变量列表中也提到过:
1 |
struct ivar_t { |
能够根据实例查找其在类中的名字,也就是“反射”:
1 |
-(NSString *)nameWithInstance:(id)instance { |
class_copyIvarList
函数获取的不只有实例变量,还有属性。但会在本来的属性名前加上一个下划线。
objc_property_t
@property
标记了类中的属性,这个没必要多说你们都很熟悉,它是一个指向objc_property
结构体的指针:
1 |
typedef struct property_t *objc_property_t; |
能够经过 class_copyPropertyList
和 protocol_copyPropertyList
方法来获取类和协议中的属性:
1 |
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) |
返回类型为指向指针的指针,哈哈,由于属性列表是个数组,每一个元素内容都是一个 objc_property_t
指针,而这两个函数返回的值是指向这个数组的指针。
举个栗子,先声明一个类:
1 |
@interface Lender : NSObject { |
你能够用下面的代码获取属性列表:
1 |
id LenderClass = objc_getClass("Lender"); |
你能够用 property_getName
函数来查找属性名称:
1 |
const char *property_getName(objc_property_t property) |
你能够用class_getProperty
和 protocol_getProperty
经过给出的名称来在类和协议中获取属性的引用:
1 |
objc_property_t class_getProperty(Class cls, const char *name) |
你能够用property_getAttributes
函数来发掘属性的名称和@encode
类型字符串:
1 |
const char *property_getAttributes(objc_property_t property) |
把上面的代码放一块儿,你就能从一个类中获取它的属性啦:
1 |
id LenderClass = objc_getClass("Lender"); |
对比下 class_copyIvarList
函数,使用 class_copyPropertyList
函数只能获取类的属性,而不包含成员变量。但此时获取的属性名是不带下划线的。
protocol_t
虽然 Objective-C 的 Category
和 protocol
拓展能力有限,但也得为了将就 Swift 的感觉,充个胖子。
flags
32 位指针最后两位是给加载 Mach-O 的 fix-up 阶段使用的,前 16 位预留给 Swift 用的。
protocol
主要内容实际上是(可选)方法,其次就是继承其余 protocol
。Swift 还支持 protocol
多继承,因此须要 protocols
数组来作兼容。
1 |
struct protocol_t : objc_object { |
IMP
IMP
在objc.h
中的定义是:
1 |
typedef void (*IMP)(void /* id, SEL, ... */ ); |
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息以后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP
这个函数指针就指向了这个方法的实现。既然获得了执行某个实例某个方法的入口,咱们就能够绕开消息传递阶段,直接执行方法,这在后面会提到。
你会发现 IMP
指向的方法与 objc_msgSend
函数类型相同,参数都包含 id
和 SEL
类型。每一个方法名都对应一个 SEL
类型的方法选择器,而每一个实例对象中的 SEL
对应的方法实现确定是惟一的,经过一组 id
和 SEL
参数就能肯定惟一的方法实现地址;反之亦然。
消息
前面作了这么多铺垫,如今终于说到了消息了。Objc 中发送消息是用中括号([]
)把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
有关消息发送和消息转发机制的原理,能够查看这篇文章。
objc_msgSend 函数
在引言中已经对objc_msgSend
进行了一点介绍,看起来像是objc_msgSend
返回了数据,其实objc_msgSend
从不返回数据而是你的方法被调用后返回了数据。下面详细叙述下消息发送步骤:
- 检测这个
selector
是否是要忽略的。好比 Mac OS X 开发,有了垃圾回收就不理会retain
,release
这些函数了。 - 检测这个 target 是否是
nil
对象。ObjC 的特性是容许对一个nil
对象执行任何一个方法不会 Crash,由于会被忽略掉。 - 若是上面两个都过了,那就开始查找这个类的
IMP
,先从cache
里面找,完了找获得就跳到对应的函数去执行。 - 若是
cache
找不到就找一下方法分发表。 - 若是分发表找不到就到超类的分发表去找,一直找,直到找到
NSObject
类为止。 - 若是还找不到就要开始进入动态方法解析了,后面会提到。
PS:这里说的分发表其实就是Class
中的方法列表,它将方法选择器和方法实现地址联系起来。
其实编译器会根据状况在objc_msgSend
, objc_msgSend_stret
, objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四个方法中选择一个来调用。若是消息是传递给超类,那么会调用名字带有”Super”的函数;若是消息返回值是数据结构而不是简单值时,那么会调用名字带有”stret”的函数。排列组合正好四个方法。
值得一提的是在 i386 平台处理返回类型为浮点数的消息时,须要用到objc_msgSend_fpret
函数来进行处理,这是由于返回类型为浮点数的函数对应的 ABI(Application Binary Interface) 与返回整型的函数的 ABI 不兼容。此时objc_msgSend
再也不适用,因而objc_msgSend_fpret
被派上用场,它会对浮点数寄存器作特殊处理。不过在 PPC 或 PPC64 平台是不须要麻烦它的。
PS:有木有发现这些函数的命名规律哦?带“Super”的是消息传递给超类;“stret”可分为“st”+“ret”两部分,分别表明“struct”和“return”;“fpret”就是“fp”+“ret”,分别表明“floating-point”和“return”。
方法中的隐藏参数
咱们常常在方法中使用self
关键字来引用实例自己,但从没有想过为何self
就能取到调用当前方法的对象吧。其实self
的内容是在方法运行时被偷偷的动态传入的。
当objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中全部的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是
self
指向的内容) - 方法选择器(
_cmd
指向的内容)
之因此说它们是隐藏的是由于在源代码方法的定义中并无声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中咱们仍然能够引用它们。在下面的例子中,self
引用了接收者对象,而_cmd
引用了方法自己的选择器:
1 |
- strange |
在这两个参数中,self
更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的super
关键字接收到消息时,编译器会建立一个objc_super
结构体:
1 |
struct objc_super { id receiver; Class class; }; |
这个结构体指明了消息应该被传递给特定超类的定义。但receiver
仍然是self
自己,这点须要注意,由于当咱们想经过[super class]
获取超类时,编译器只是将指向self
的id
指针和class
的SEL传递给了objc_msgSendSuper
函数,由于只有在NSObject
类才能找到class
方法,而后class
方法调用object_getClass()
,接着调用objc_msgSend(objc_super->receiver, @selector(class))
,传入的第一个参数是指向self
的id
指针,与调用[self class]
相同,因此咱们获得的永远都是self
的类型。
获取方法地址
在IMP
那节提到过能够避开消息绑定而直接获取方法的地址并调用方法。这种作法不多用,除非是须要持续大量重复调用某方法的极端状况,避开消息发送泛滥而直接调用该方法会更高效。
NSObject
类中有个methodForSelector:
实例方法,你能够用它来获取某个方法选择器对应的IMP
,举个栗子:
1 |
void (*setter)(id, SEL, BOOL); |
当方法被当作函数调用时,上节提到的两个隐藏参数就须要咱们明确给出了。上面的例子调用了1000次函数,你能够试试直接给target
发送1000次setFilled:
消息会花多久。
PS:methodForSelector:
方法是由 Cocoa 的 Runtime 系统提供的,而不是 Objc 自身的特性。
动态方法解析
你能够动态地提供一个方法的实现。例如咱们能够用@dynamic
关键字在类的实现文件中修饰一个属性:
1 |
@dynamic propertyName; |
这代表咱们会为这个属性动态提供存取方法,也就是说编译器不会再默认为咱们生成setPropertyName:
和propertyName
方法,而须要咱们动态提供。咱们能够经过分别重载resolveInstanceMethod:
和resolveClassMethod:
方法分别添加实例方法实现和类方法实现。由于当 Runtime 系统在Cache
和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:
或resolveClassMethod:
来给程序员一次动态添加方法实现的机会。咱们须要用class_addMethod
函数完成向特定类添加特定方法实现的操做:
1 |
void dynamicMethodIMP(id self, SEL _cmd) { |
上面的例子为resolveThisMethodDynamically
方法添加了实现内容,也就是dynamicMethodIMP
方法中的代码。其中 “v@:
” 表示返回值和参数,这个符号涉及 Type Encoding
PS:动态方法解析会在消息转发机制浸入前执行。若是 respondsToSelector:
或 instancesRespondToSelector:
方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP
的机会。若是你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:
返回NO
。
评论区有人问如何用 resolveClassMethod:
解析类方法,我将他贴出有问题的代码作了纠正和优化后以下,能够顺便将实例方法和类方法的动态方法解析对比下:
头文件:
1 |
|
m 文件:
1 |
|
须要深入理解 [self class]
与 object_getClass(self)
甚至 object_getClass([self class])
的关系,其实并不难,重点在于 self
的类型:
- 当
self
为实例对象时,[self class]
与object_getClass(self)
等价,由于前者会调用后者。object_getClass([self class])
获得元类。 - 当
self
为类对象时,[self class]
返回值为自身,仍是self
。object_getClass(self)
与object_getClass([self class])
等价。
凡是涉及到类方法时,必定要弄清楚元类、selector、IMP 等概念,这样才能作到触类旁通,随机应变。
消息转发
重定向
在消息转发机制执行前,Runtime 系统会再给咱们一次偷梁换柱的机会,即经过重载- (id)forwardingTargetForSelector:(SEL)aSelector
方法替换消息的接受者为其余对象:
1 |
- (id)forwardingTargetForSelector:(SEL)aSelector |
毕竟消息转发要耗费更多时间,抓住此次机会将消息重定向给别人是个不错的选择,不过千万别返回 若是此方法返回nil或self,则会进入消息转发机制(self
,由于那样会死循环。forwardInvocation:
);不然将向返回的对象从新发送消息。
若是想替换类方法的接受者,须要覆写 + (id)forwardingTargetForSelector:(SEL)aSelector
方法,并返回类对象:
1 |
+ (id)forwardingTargetForSelector:(SEL)aSelector { |
转发
当动态方法解析不做处理返回NO
时,消息转发机制会被触发。在这时forwardInvocation:
方法会被执行,咱们能够重写这个方法来定义咱们的转发逻辑:
1 |
- (void)forwardInvocation:(NSInvocation *)anInvocation |
该消息的惟一参数是个NSInvocation
类型的对象——该对象封装了原始的消息和消息的参数。咱们能够实现forwardInvocation:
方法来对不能处理的消息作一些默认的处理,也能够将消息转发给其余对象来处理,而不抛出错误。
这里须要注意的是参数anInvocation
是从哪的来的呢?其实在forwardInvocation:
消息发送前,Runtime系统会向对象发送methodSignatureForSelector:
消息,并取到返回的方法签名用于生成NSInvocation
对象。因此咱们在重写forwardInvocation:
的同时也要重写methodSignatureForSelector:
方法,不然会抛异常。
当一个对象因为没有相应的方法实现而没法响应某消息时,运行时系统将经过forwardInvocation:
消息通知该对象。每一个对象都从NSObject
类中继承了forwardInvocation:
方法。然而,NSObject
中的方法实现只是简单地调用了doesNotRecognizeSelector:
。经过实现咱们本身的forwardInvocation:
方法,咱们能够在该方法实现中将消息转发给其它对象。
forwardInvocation:
方法就像一个不能识别的消息的分发中心,将这些消息转发给不一样接收对象。或者它也能够象一个运输站将全部的消息都发送给同一个接收对象。它能够将一个消息翻译成另一个消息,或者简单的”吃掉“某些消息,所以没有响应也没有错误。forwardInvocation:
方法也能够对不一样的消息提供一样的响应,这一切都取决于方法的具体实现。该方法所提供是将不一样的对象连接到消息链的能力。
注意: forwardInvocation:
方法只有在消息接收对象中没法正常响应消息时才会被调用。 因此,若是咱们但愿一个对象将negotiate
消息转发给其它对象,则这个对象不能有negotiate
方法。不然,forwardInvocation:
将不可能会被调用。
转发和多继承
转发和继承类似,能够用于为Objc编程添加一些多继承的效果。就像下图那样,一个对象把消息转发出去,就好似它把另外一个对象中的方法借过来或是“继承”过来同样。
这使得不一样继承体系分支下的两个类能够“继承”对方的方法,在上图中Warrior
和Diplomat
没有继承关系,可是Warrior
将negotiate
消息转发给了Diplomat
后,就好似Diplomat
是Warrior
的超类同样。
消息转发弥补了 Objc 不支持多继承的性质,也避免了由于多继承致使单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,并且转发机制是透明的。
替代者对象(Surrogate Objects)
转发不只能模拟多继承,也能使轻量级对象表明重量级对象。弱小的女人背后是强大的男人,毕竟女人遇到难题都把它们转发给男人来作了。这里有一些适用案例,能够参看官方文档。
转发与继承
尽管转发很像继承,可是NSObject
类不会将二者混淆。像respondsToSelector:
和 isKindOfClass:
这类方法只会考虑继承体系,不会考虑转发链。好比上图中一个Warrior
对象若是被问到是否能响应negotiate
消息:
1 |
if ( [aWarrior respondsToSelector: |
结果是NO
,尽管它可以接受negotiate
消息而不报错,由于它靠转发消息给Diplomat
类来响应消息。
若是你为了某些意图偏要“弄虚做假”让别人觉得Warrior
继承到了Diplomat
的negotiate
方法,你得从新实现 respondsToSelector:
和 isKindOfClass:
来加入你的转发算法:
1 |
- (BOOL)respondsToSelector:(SEL)aSelector |
除了respondsToSelector:
和 isKindOfClass:
以外,instancesRespondToSelector:
中也应该写一份转发算法。若是使用了协议,conformsToProtocol:
一样也要加入到这一行列中。相似地,若是一个对象转发它接受的任何远程消息,它得给出一个methodSignatureForSelector:
来返回准确的方法描述,这个方法会最终响应被转发的消息。好比一个对象能给它的替代者对象转发消息,它须要像下面这样实现methodSignatureForSelector:
:
1 |
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector |
健壮的实例变量 (Non Fragile ivars)
在 Runtime 的现行版本中,最大的特色就是健壮的实例变量。当一个类被编译时,实例变量的布局也就造成了,它代表访问类的实例变量的位置。从对象头部开始,实例变量依次根据本身所占空间而产生位移:
上图左边是NSObject
类的实例变量布局,右边是咱们写的类的布局,也就是在超类后面加上咱们本身类的实例变量,看起来不错。但试想若是哪天苹果更新了NSObject
类,发布新版本的系统的话,那就悲剧了:
咱们自定义的类被划了两道线,那是由于那块区域跟超类重叠了。惟有苹果将超类改成之前的布局才能拯救咱们,但这样也致使它们不能再拓展它们的框架了,由于成员变量布局被死死地固定了。在脆弱的实例变量(Fragile ivars) 环境下咱们须要从新编译继承自 Apple 的类来恢复兼容性。那么在健壮的实例变量下会发生什么呢?
在健壮的实例变量下编译器生成的实例变量布局跟之前同样,可是当 runtime 系统检测到与超类有部分重叠时它会调整你新添加的实例变量的位移,那样你在子类中新添加的成员就被保护起来了。
须要注意的是在健壮的实例变量下,不要使用sizeof(SomeClass)
,而是用class_getInstanceSize([SomeClass class])
代替;也不要使用offsetof(SomeClass, SomeIvar)
,而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))
来代替。
优化 App 的启动时间 讲过加载 Mach-O 文件时有个步骤是经过 fix-up 修改偏移量来解决 fragile base class。
Objective-C Associated Objects
在 OS X 10.6 以后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有如下三个:
1 |
void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy ); |
这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
1 |
enum { |
这些常量对应着引用关联值的政策,也就是 Objc 内存管理的引用计数机制。有关 Objective-C 引用计数机制的原理,能够查看这篇文章。
Method Swizzling
以前所说的消息转发虽然功能强大,但须要咱们了解而且能更改对应类的源代码,由于咱们须要实现本身的转发逻辑。当咱们没法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,可是有时没法达到目的。这里介绍的是 Method Swizzling ,它经过从新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的作法更为隐蔽,甚至有些冒险,也增大了debug的难度。
PS: 对于熟练使用 Method Swizzling 的开发者,能够跳过此章节,看看我另外一篇『稍微深刻』一点的文章 Objective-C Method Swizzling。
这里摘抄一个 NSHipster 的例子:
1 |
|
上面的代码经过添加一个Tracking
类别到UIViewController
类中,将UIViewController
类的viewWillAppear:
方法和Tracking
类别中xxx_viewWillAppear:
方法的实现相互调换。Swizzling 应该在+load
方法中实现,由于+load
是在一个类最开始加载时调用。dispatch_once
是GCD中的一个方法,它保证了代码块只执行一次,并让其为一个原子操做,线程安全是很重要的。
若是类中不存在要替换的方法,那就先用class_addMethod
和class_replaceMethod
函数添加和替换两个方法的实现;若是类中已经有了想要替换的方法,那么就调用method_exchangeImplementations
函数交换了两个方法的 IMP
,这是苹果提供给咱们用于实现 Method Swizzling 的便捷方法。
可能有人注意到了这行:
1 |
// When swizzling a class method, use the following: |
object_getClass((id)self)
与 [self class]
返回的结果类型都是 Class
,但前者为元类,后者为其自己,由于此时 self
为 Class
而不是实例.注意 [NSObject class]
与 [object class]
的区别:
1 |
+ (Class)class { |
PS:若是类中没有想被替换实现的原方法时,class_replaceMethod
至关于直接调用class_addMethod
向类中添加该方法的实现;不然调用method_setImplementation
方法,types
参数会被忽略。method_exchangeImplementations
方法作的事情与以下的原子操做等价:
1 |
IMP imp1 = method_getImplementation(m1); |
最后xxx_viewWillAppear:
方法的定义看似是递归调用引起死循环,其实不会的。由于[self xxx_viewWillAppear:animated]
消息会动态找到xxx_viewWillAppear:
方法的实现,而它的实现已经被咱们与viewWillAppear:
方法实现进行了互换,因此这段代码不只不会死循环,若是你把[self xxx_viewWillAppear:animated]
换成[self viewWillAppear:animated]
反而会引起死循环。
看到有人说+load
方法自己就是线程安全的,由于它在程序刚开始就被调用,不多会碰到并发问题,因而 stackoverflow 上也有大神给出了另外一个 Method Swizzling 的实现:
1 |
- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 { |
上面的代码一样要添加在某个类的类别中,相比第一个种实现,只是去掉了dispatch_once
部分。
Method Swizzling 的确是一个值得深刻研究的话题,找了几篇不错的资源推荐给你们:
- Objective-C的hook方案(一): Method Swizzling
- Method Swizzling
- How do I implement method swizzling?
- What are the Dangers of Method Swizzling in Objective C?
- JRSwizzle
在用 SpriteKit 写游戏的时候,由于 API 自己有一些缺陷(增删节点时不考虑父节点是否存在啊,很容易崩溃啊有木有!),我在 Swift 上使用 Method Swizzling弥补这个缺陷:
1 |
extension SKNode { |
而后其余地方调用那两个类方法:
1 |
SKNode.yxy_swizzleAddChild() |
由于 Swift 中的 extension 的特殊性,最好在某个类的load()
方法中调用上面的两个方法.我是在AppDelegate 中调用的,因而保证了应用启动时可以执行上面两个方法.
总结
咱们之因此让本身的类继承 NSObject
不只仅由于苹果帮咱们完成了复杂的内存分配问题,更是由于这使得咱们可以用上 Runtime 系统带来的便利。可能咱们平时写代码时可能不多会考虑一句简单的 [receiver message]
背后发生了什么,而只是当作方法或函数调用。深刻理解 Runtime 系统的细节更有利于咱们利用消息机制写出功能更强大的代码,好比 Method Swizzling 等。
Update 20170820: 使用 objc4-709 源码重写部分章节,更新至 Swift 4 代码示例。
参考连接: