iOS Runtime
简书内容都是个人的知识点整理和笔记。
iOS Runtime理解是我们每一个iOS开发者在深刻掌握Objective-C这门语言的必经之路。在深入学习Runtime之前我们需要清楚认识一件事:Objective-C是一门动态语言,它将类的类型和数据变量的类型都是在运行时确定的。
作为最典型的运行时机制,Objective-C代码在程序运行的过程中都转换成了Runtime中的C语言代码。
1.定义
在进一步了解Runtime之前,我们需要首先掌握一些基础定义。
Class
如下面代码所示Objective-C中Class类是一个指向objc_class结构体的指针。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
所有类对象都有一个名为“isa”的指针,这个指针指向一个objc_class结构体,objc_class结构体中包含存储了变量列表、方法列表、遵守的协议列表等等信息。
Meta Class
我们在描述Class时指出所有类对象都有一个指向objc_class结构体名为“isa”的指针。而这个结构体中又存在着一个isa指针,这个指针指向的就是MetaClass,元类。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表objc_method_list中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
这种关系由如下图片所示:
消息发送
id
id是我们在编程中广泛用到的数据类型,它的实质是一个指向类实例的指针,只是一个指针。
/// A pointer to an instance of a class.
typedef struct objc_object *id;
SEL
在Objective-C编译时,根据每一个方法生成一个整型标识地址,这个标识就是SEL。本质上SEL只是一个指向方法的指针。
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
IMP
IMP是一个指向方法实现的指针。在定义中我们可以看见一个方法的指针由方法的实例指针和标志地址构成。我们在上面提到SEL只是一个指向方法的指针,它的存在是为了加快方法的查询速度,这个查到的对象就是IMP。
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
2.消息发送
在Objective-C中,一个对象调用方法,是因为接收到了一个消息时,然后开始进入正常的方法调用流程。方法调用的本质,就是让对象发送消息。消息直到运行时才会与方法实现进行绑定。
在运行时,默认情况下编译器会将消息表达式转化为一个消息函数的调用,即objc_msgSend。如果object无法响应message消息时,编译器会报错。但如果是以perform的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。
我们来看一看objc_msgSend函数
objc_msgSend(void /* id self, SEL op, ... */ )
这个函数将消息接收者和方法名作为其必须参数,将方法其余参数作为可选参数与实现方法进行绑定。首先它找到SEL对应的方法实现,然后根据接收者的类来找到方法的确切的实现,即前文我们提到的IMP指针。之后将接收者对象及方法的所有参数传给它,最后实现返回的值作为它自己的返回值。
当一个对象接收到方法时objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector,一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程。其流程如下图所示:
方法调用流程
特别注意的是:当消息发送给一个对象时首先从运行时系统缓存使用过的方法中寻找。这就是我们前文叙述的objc_class结构体中的objc_cache的使用。
消息转发
在消息发送中我们提到,当对象无法接收到消息时,就会走消息转发流程。消息转发流程的目的就是让我们告诉对象如何处理未知的消息,而在默认情况下,对象无法接收到消息时程序便会崩溃,就是我们最常见的:
unrecognized selector sent to instance xxxxxxx
但在系统发出警告之前,Runtime会发送给对象一条"forwardInvocation"消息,该条消息里面含有一个囊括了所有方法细节的NSInvocation类对象作为参数。你可以通过实现"forwardInvocation:"方法转发给其他对象,或是用其他方法规避这个错误。
为了转发这个消息"forwardInvocation:"需要完成这两件事:
(1)确定这条消息转发的对象;
(2)将消息和参数发送到选中的对象。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
消息转发与多重继承
通过消息转发,我们将接收消息的对象与转发对象建立起了某种关系,表面上看任然是原对象在处理消息,但这种方式更像是一种对多重继承的模拟。
一个对象响应消息的转发通过模仿“继承”的方法实现定义在另一个类,如下图所示:
Forwarding
Warrior对象通过实现forwardInvocation:将调用negotiate方法的消息转发给Diplomat对象,最后Diplomat对象调用了negotiate方法。从代码上讲,最终是
Diplomat实现了对negotiate方法的调用,但也像是Warrior继承了Diplomat对象从而实现了对方法的调用。
动态方法解析
有些时候你希望可以动态的提供一些方法接口,就像是@dynamic修饰词一样,由我们自己来生成setter和getter方法。在这里我们可以通过实现resolveInstanceMethod:或resolveClassMethod:方法分别为实例和类方法的给定的选择题动态地提供实现。
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
消息转发和动态方法是正交的,所谓正交是指一个类有机会在转发机制启动之前动态地解析一个方法。
备用接收者
在进行消息转发前,如果已知一个对象实现了方法,那么在进行消息转发之前我们还可以直接指定该对象为备用的消息接受者。
指定一个备用的消息接受者使用的方法是:
- (id)forwardingTargetForSelector:(SEL)aSelector;
该方法在消息转发前给你提供了一个更快更轻量的方案。
总结:当一个对象无法接收某一消息时,就会启动消息转发,但在这之前我们可以采取一些措施,避免程序的崩溃。我们可以将这个步骤归纳为:
(1)动态方法解析
(2)备用接收者
(3)完成转发
消息转发
3.关联对象
我们在 iOS 开发中经常需要使用类别(Category),为已经存在的类扩展方法,但当我们想要添加某些属性时一般都会去继承这个类,我们也可以使用Runtime的关联属性去实现。
关联对象我们主要适用以下三个方法:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
objc_removeAssociatedObjects(id _Nonnull object);
设置关联
objc_setAssociatedObject这个方法的作用是通过key和关联策略为给定对象设置关联值。
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);
//object: 被设置关联值得对象。
//key: 关联值的Key
//value: 关联值
//policy: 关联策略
其中关联策略objc_AssociationPolicy包含如下:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /** 指定关联对象的弱引用。 */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /** 指定对关联对象的强引用,非原子操作*/
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /** 指定关联的对象的拷贝,非原子操作*/
OBJC_ASSOCIATION_RETAIN = 01401, /**指定对关联对象的强引用*/
OBJC_ASSOCIATION_COPY = 01403 /** 指定关联的对象的拷贝*/
};
对于如何选择关联策略和我们平时设置属性选择assign,strong,copy以及atomic和nonatomic是一样的。
atomic和nonatomic区别用来决定编译器生成的getter和setter是否为原子操作。atomic提供多线程安全,是描述该变量是否支持多线程的同步访问,如果选择了atomic 那么就是说,系统会自动的创建lock锁,锁定变量。nonatomic禁止多线程,变量保护,提高性能。
获取关联
objc_getAssociatedObject这个方法是通过key获取某被关联对象的关联值。
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
//object: 同objc_setAssociatedObject
//key: 同objc_setAssociatedObject
移除关联
objc_removeAssociatedObjects移除被关联对象的所有关联值,注意,是所有!
这个是本文章DEMO的地址:Github
参考资料
Objective-C Runtime Programming Guide - Message
Objective-C Runtime Programming Guide - Message Forwarding
Objective-C Runtime Programming Guide - Type Encodings
详解Objective-C的isa与meta-class
iOS运行时(Runtime)详解+Demo
iOS~runtime理解