Reference:
Objective-C Runtime Programming Guide
iOS Runtime详解
简述
Runtime抽象的说OC的运行时系统,实质点讲它是一个由C和汇编编写的库(后逐步用C++重写)。这个库完成了OC对C的扩展,在编译期,通过它实现了OC向C/C++的装换;同时在运行时,它是OC面向对象和动态特性的基石:它让OC可以动态的创建类和对象、并通过方法调用来完成函数的动态绑定,进行消息的传递和转发。
概览
/1.png)
要点
- 类和对象都有个指向类的
isa
指针,所以类又称类对象,对象的isa
指向所属类,类的isa
指向元类; - 所有元类的
isa
都指向根类(NSObject)的元类,包括它自己; - 类别可以为类添加属性,但不能添加成员变量,可以用关联对象来代替成员变量,保存属性的值;
- 消息决策时先在当前类的方法缓存中查找,再去继承链上的方法列表中查找,继承连上找到后,会缓存到当前类的方法缓存中;
- 消息转发的系列方法中要注意调用父类的方法,以便处理当前类未处理的消息;
详解
Runtime的代码是开源的,从objc4-493版本开始,Runtime的实现代码逐步用C++进行重写,本文参照的源码是最后一个没有C++的版本objc4-437.3。
面向对象
1.对象
源码中对象的定义:
1 | typedef struct objc_object { |
对象的定义是一个结构体,内部只有一个指向所属类的isa
指针。
2.类
源码中类的定义:
1 | typedef struct objc_class *Class; |
Meta Class
类的结构体中也有一个指向所属类的isa
指针,还有一个指向父类的super_class
指针,类的isa
指向的是类的类,被称之为元类;元类的super_class
指针指向元类的父类,元类的isa
指针则都指向根元类(即NSObject的元类);值得注意的是根元类的super_class
指向的是根类(即NSObject),isa
指针依然指向根元类(即它自己)。
对象、类和元类之间的关系如图:
/2.png)
类对象:对象和类都有isa
指针指向所属的类,甚至在C++实现的Runtime版本中,类的结构体直接继承自对象的结构体,所以说类也是对象,称之为类对象,类对象和元类是一一对应的,都是运行时由系统创建的单例。
objc_ivar_list
类中struct objc_ivar_list *ivars
是成员变量列表,源码中定义的成员变量其结构如下:
1 | struct objc_ivar { |
其中ivar_type存储的是成员变量的数据类型、内存管理、访问权限等信息编码后的结果;提供Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
函数来获取一个类的成员变量列表。
在类中使用@property
为类同时添加了属性和成员变量,属性的本质是set和get方法,提供objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
函数获取一个类的属性列表。
objc_method_list
类中struct objc_method_list **methodLists
是对象方法列表,类方法就是类对象的方法,保存在元类的方法列表中,源码中方法的结构如下:
1 | struct objc_method { |
方法中method_name即Selector,method_types是方法的签名,method_imp则是方法的实现,提供Method *class_copyMethodList(Class cls, unsigned int *outCount)
来获取一个类的对象方法列表。
objc_cache
类中struct objc_cache *cache
是Selector的方法缓存,其中保存的数据结构如下:
1 | typedef struct { |
方法缓存的设计源于一个理念:类的某个方法调用后,接下来会有很大的概率被再次调用。当向一个对象发送消息后,需要查找Selecotr对应的实现(IMP),查找顺序是先查找当前类的方法缓存,如果没有找到,则从当前类开始,沿着继承链在类的方法列表中查找,找到后会将Selector和IMP的映射缓存在当前类的方法缓存中。
objc_protocol_list
类中struct objc_protocol_list *protocols
是协议列表,源码中协议的结构如下:
1 | @interface Object |
协议是一个继承自Object的类,其内有该协议遵循的协议列表、对象方法列表、和类方法列表;提供Protocol **class_copyProtocolList(Class cls, unsigned int *outCount)
函数来获取一个类的协议列表。
3.类别
源码中类别的结构如下:
1 | struct objc_category { |
通过类别可以为类添加属性,但不能添加成员变量,类别添加属性时,可以通过objc_setAssociatedObject
添加关联对象,代替成员变量来保存属性的值。
主类和类别中的方法、协议等都是在Runtime初始化函数_objc_init
中map_images
时,添加到类的列表中,类别虽然后添加,但却是插在列表前面,所以类别的方法可以覆盖主类中的同名方法。
_objc_init
中的回调map_images
完成后,类、协议、类别等已经初始化完成,然后在load_images
触发时,再调用各个类和类别的+load
方法,所以在iOS中+load
调用时,App相关的类都已完成初始化,因此,在iOS中+load
方法的不安全指的是不能保证其他类的+load
已经执行,而不是指其它类未完成初始化,关于+load
的执行顺序总结如下:
- 父类的
+load
执行后子类的+load
才执行; - 所有类的
+load
执行后类别的+load
才执行; - 类别之间、没有依赖关系的类之间,
+load
执行的顺序与编译顺序相同(即buildPhases -> Compile Sources
中的文件顺序);
initialize:除了
+load
方法还可以选择在initialize
方法中做类的初始化相关工作,关于initialize
的调用规则需要分清以下几点:
initialize
是在类的加载完成之后,第一次调用类的方法之前被调用;- 子类的
initialize
中不需要显式的调用父类的方法,父类的initialize
会被自动的调用;- Category中的
initialize
会覆盖类中的initialize
方法;
消息传递
Runtime通过方法调用来实现函数的动态绑定,即所调用的函数地址不是编译期硬编码在指令中,而是需要在运行期读取。方法调用又叫消息传递,消息由selector及其参数所构成,消息传递的核心是objc_msgSend
函数:
1 | id objc_msgSend(id self, SEL op, ...); |
在objc_msgSend
函数中,第一个参数是消息的接收类/对象,第二个参数是selector,后面才是消息的参数,所以在一个方法实现的内部,有两个默认的参数self和_cmd,它们分别代表方法调用者自身和方法的selector。
消息传递,即对象进行方法调用,到执行对应实现,所执行的步骤大致如下:
- 根据对象/类的
isa
指针找到对象/类所属的类/元类; - 先在当前类/元类的方法缓存中查找selector对应的IMP,找到IMP则去执行对应的函数实现;
- 方法缓存中没找到,则沿着继承链在类/元类的方法列表找查找selector对应的方法;
- 找到对应方法后,将方法中的IMP和selector的映射添加到当前类/元类的方法缓存中,并执行IMP对应的函数实现;
- 如果最终沿着继承链没有找到对应的方法,则启动消息转发机制;
消息转发
当一个消息在类及其继承链上最终没有找到对应的方法实现,将启动消息转发机制,消息转发的流程如图:
/3.png)
启动消息转发后,消息的接收者还有三次机会来对消息做出处理:
- 动态方法解析:在
+resolveClassMethod:
/+resolveInstanceMethod:
中,如果为消息动态的添加了一个类/对象方法,则重新再发送一次消息,如果没有为消息动态的添加方法,则继续消息转发的下一步(与动态解析方法返回YES/NO没有关系); - 备援接受者:在
-forwardingTargetForSelector:
中如果返回了一个非nil的备援接受者,则将消息发送给备援接受者,如果返回nil,则继续消息转发的下一步; - 转发消息:需要先在
-methodSignatureForSelector:
返回消息对应的方法签名,再在-forwardInvocation:
中处理NSInvocation,如果没有返回方法签名,则会调用-doesNotRecognizeSelector:
方法,并抛出异常;
在为当前类实现以上消息转发方法时,对于没有处理的消息,都应调用父类的方法,交由父类处理。在第三步转发消息时,方法签名可以参看官方文档Type Encodings,处理NSInvocation时,可以简单的转发给另外一个target,但这就跟返回备援接受者效果一样,一般如果消息转发需要走到这一步,则是需要编辑NSInvocation来更改消息内容,比如追加参数、更换selector等。
应用
Runtime是OC动态特性的基石,使用它可以实现OC语言层面的拓展,Runtime的应用非常的广泛,Runtime实现的功能也非常的实用,Runtime编程可以参见Objective-C Runtime Programming Guide,这里只对一些常见的应用做一个简单的介绍
关联对象
Runtime提供了三个函数来管理关联对象:
1 | //关联对象 |
关联对象一个非常常用的地方是在类别中添加属性时,代替成员变量来保存属性的值。添加关联对象时,需要指明和属性修饰符类似的关联对象内存策略,常用的有以下几个:
- OBJC_ASSOCIATION_ASSIGN,同@property (assign)或 @property (unsafe_unretained);
- OBJC_ASSOCIATION_RETAIN_NONATOMIC,同@property (nonatomic, strong);
- OBJC_ASSOCIATION_COPY_NONATOMIC,同@property (nonatomic, copy);
ISA Swizzling
对象的isa
指针指向了它所属的类,ISA Swizzling是指修改对象isa
指向的类的技术。Runtime提供Class object_setClass(id obj, Class cls)
函数来修改一个对象所指向的类,ISA Swizzling只会影响做了替换操作的对象,不会影响到整个类,替换示例:
1 | BOOL swizzlingObjClass(id obj, Class newCls) |
上面的示例中使用了size_t class_getInstanceSize(Class cls)
来比较ISA Swizzling前后类的实例的大小,这是因为将要ISA Swizzling的对象的内存是已经分配好的,如果替换后类的实例大于替换之前分配的内存,会导致EXC_BAD_ACCESS
错误,所以ISA Swizzling时,替换后的类一般为替换前类的子类,而且子类中没有新增任何成员变量或合成属性。KVO就是通过ISA Swizzling技术来实现的。
当对某个类A
的对象添加KVO之后,KVO机制会动态的创建一个A
的子类(通常命名为NSKVONotifying_A),然后让被观察对象的isa
指向这个子类,在子类中重写了-class
方法,让-class
依然返回原来的类,并重写了被观察keyPath
的setter
方法,在setter
方法中调用了-willChangeValueForKey:
和-didChangeValueForKey:
,当通过setter
方法更改被观察的值时,观察者就能收到通知。
Method Swizzling
Runtime允许动态的新增、替换方法的实现,用新的实现替换原有方法的实现,就叫做Method Swizzling。一种特殊的Method Swizzling - 交换两个已有方法的实现,配合类别可以为原方法添加hook
,完成对没有源码的原方法的修改。简单的hook
使用method_exchangeImplementations
即可实现,但实际上hook
时有许多需要注意的问题,安全而健壮的hook
可以参看文章Objective-C Method Swizzling。
Zombie Objects
Zombie Objects是Xcode中用来调试野指针(EXC_BAD_ACCESS
)这类内存问题的工具,它充分的应用了Runtime技术:它首先通过Method Swizzling替换原来的-dealloc
方法,当对象引用计数为0的时候,会调用新的__dealloc_zombie
方法;新的方法中又使用ISA Swizzling技术让对象的isa
指向僵尸类,然后释放对象的引用,但并不释放当前对象;最后当程序再向本该已释放的对象发送消息时,在僵尸类的消息转发中就会输出调试信息。
Zombie Objects中__dealloc_zombie
打大致实现(非源码):
1 | - (void)__dealloc_zombie |
YYModel
YYModel运用Runtime面向对象的特性,为根类NSObject添加扩展,实现JSON数据和OC模型之间自动转换。YYModel非常的轻量级,代码也非常的直观,其他利用Runtime特性实现JSON与OC模型互相转换的库还有很多(如:MJExtension),它们对Runtime的利用更加复杂,功能也相对丰富。
JSPatch
JSPatch是一个热修复库,它的基本原理是:JS传递字符串给OC,OC通过Runtime动态调用接口和替换OC方法的实现。JSPatch对Runtime的应用还包括动态的注册类,为类添加方法等,示例代码如下:
1 | Class cls = objc_allocateClassPair(superCls, "JPObject", 0); |