runtime 是什么?
- runtime 又叫做运行时,是一套底层的 C 语言API,其为 iOS 内部的核心之一,我们平时编写 oc代码,底层都是基于它来实现的。比如
[receiver message]
// 底层运行时会被编译器转化为:
objc_msgSend(receiver, message);
// 有参数的
[receiver message:(id)arg...];
objc_msgSend(receiver, seletor, arg1, arg2, ...);
为什么需要 runtime
- oc 是一门动态语言,它会将一些工作放在代码运行时才处理并非编译时。也就是说,有很多类和成员变量在我们编译的时候是不知道的,而在运行时,我们编写的代码才会被转换成完整的确定 的代码运行。
- 因此,编译器是不够的,我们还需要一个运行时系统(runtime system)来处理编译后的代码。
- runtime 基本是用 c 和汇编写成的,由此可见苹果为了动态系统的高效做出的努力。苹果的 GNU 各自维护一个开源的 runtime 版本,这两个版本之间都在努力保持一致。
runtime 的作用
- oc 在3个层面上与 runtime 系统进行交互:
- 通过 oc 源码,只要需要 oc 代码,runtime 系统自动在幕后搞定一切,调用方法,编译器会将 oc 代码转化成运行时代码,在运行时确定数据结构和函数。
- 通过 Foundation 框架的 NSObject 类定义方法。cocoa程序中绝大多数都是 NSObject 的子类,所有都继承了 NSObject 的行为。(NSProxy 类是个例外,它是一个抽象类)。
- 一些情况下 NSObject 类仅仅定义了完成某件事情的模板,并没有提供所需要的代码。例如:- description 方法,该类方法返回类内容的字符串表示,该方法主要用来调试程序。NSObject 类并不知道子类的内容,所以它只是返回类的名字和对象的地址,NSObject 的子类可以重新实现。
- 还有一些 NSObject 的方法可以通过 runtime 系统中获取信息,允许对象进行自我检查。例如:
- -class 方法返回对象的类:
- -isKindOfClass:和-IsMemberOfClass:方法检查对象是否存在指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量)
- -respondsToSelector:检查对象是否响应指定的消息
- -conformsToProtocol:检查对象是否实现了指定协议类的方法
- -methodForSelector:返回指定方法实现的地址
- 通过对 Runtime 库函数的直接调用
- runtime 系统是具有公共接口的动态共享库。
- 许多函数可以让你使用纯 C 代码实现 objc 同样的功能。除非是写一些 objc 与其他语言桥接或者底层的 debug 工作,你在写 objc 代码时一般不会用到这些 c 语言函数。
runtime 的相关术语
-
SEL
- 它是selector 在 objc 中的表示。selector 是方法选择器,其实作用和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 objc 在相同的类中不会有命名相同的两个方法。selector 对方法进行包装,以便找到对应的方法实现。他的数据结构是:typedef struct objc_selector *SEL; 我们可以看出它是一个映射到方法 C 字符串,你可以通过 objc 编译器命令@selector()或者 runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器。
- 注意:不同类中相同名字的方法对应的 selector 是相同的,由于变量类型不同,所以不会导致他们调用方法实现混乱。
-
id
- id 是一个参数类型,他是指向某个类的实例指针。定义如下:
typedef struct objc_object *id; struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };
- 以上定义,看到 objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对应所属的类。
- 注意:isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要响确定类型还是需要用对象的 -class 方法。PS:KVO 的实现原理就是将被观察对象的 isa 指针指向一个中间类而不是真实类型。
-
Class
- typedef struct objc_class *Class;
- class 其实是指向 objc_class 的结构体的指针。objc_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;、
- 从 objc_class 可以看出,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属协议。
- 其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表:
// objc_ivar_list 的实现 struct objc_ivar_list { int ivar_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE; } // objc_method_list的实现 struct objc_method_list { struct objc_method_list * _Nullable obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; }
- 由此可见,我们可以动态修改 *methodList 的值来添加成员方法,这也是 category 实现的原理,同样解释了 Category 不能添加属性的原因
- objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法信息由 objc_method 结构体存储。
- 值得注意的是,objc_class 中也有一个 isa 指针,这说明 objc 类本身也是一个对象。为了处理类和对象的关系,runtime 库创建一个叫做 Meta Class(元类)的东西,类对象所属的类叫做元类。meta Class 表述了对象本身所具备的元数据。
- 我们所熟悉的类方法,就源自于 meta Class。我们可以理解为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。
- 当你发出一个类似[NSObject alloc](类方法)消息时,实际上,这个消息被发送给一个类对象(Class object),这个类对象丙戌是一个元类的实例,而这个元类同时也是一个根元类(root meta Class)的实例。所有元类的 isa 指针最终都指向根元类。
- 所以当[NSObject alloc];这条消息发送给类对象的时候,运行时代码 objc_msgSend()会去元类中查找能够响应的方法实现,如果找到了,就会对这个类对象执行方法调用。
- 最后 objc_class 中还有一个 objc_cahce,缓存。
-
method
- method 代表类中某个方法的类型
struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; }
- 方法类型是 SEL
- 方法类型 method_types 是一个char 指针,存储方法的参数类型和返回值类型
- method_imp 指向了方法实现,本质是一个函数指针
- Ivar 是表示成员变量的类型。
struct objc_ivar { char * _Nullable ivar_name OBJC2_UNAVAILABLE; char * _Nullable ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
- 其中 ivar_offset 是基地址便宜字节
-
IMP
- IMP 在 objc.h 中定义的是
typedef void (*IMP)(void /* id, SEL, ... */ );
- 他是一个函数指针,这是由编译器生成的。当你发起一个 objc 消息之后,最终他会执行哪段代码,就是由这个函数指针制定的。而 IMP 这个函数指针就指向了这个方法的实现。
- 如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法。
- 你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含了 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址。
- 而一个确定方法也只有唯一一组 id 和 SEL 参数。
-
cache
- 定义如下
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE; struct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method _Nullable buckets[1] OBJC2_UNAVAILABLE; };
- cache 为方法调用的性能进行了优化,每当实例对象接收一个消息时,它不会直接在 isa 指针指向的类的方法类别中遍历查找能够响应的方法,因为每次都要查找的效率太低了,而是优先在 cache 中找。
- runtime 系统会吧调用到的方法 cache 中,如果一个方法被调用,那么他有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 cache 一样。
-
property
typedef struct objc_property *objc_property_t;
- 可以通过 class_copyPropertyList 和 protocol_copyPropertyList 方法获取类和协议中的属性
OBJC_EXPORT objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT objc_property_t _Nonnull * _Nullable protocol_copyPropertyList(Protocol * _Nonnull proto, unsigned int * _Nullable outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 返回的是属性列表,列表中的每个元素都是一个 objc_property_t 指针
@interface Person () @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) int age; @property (nonatomic, assign) double weight; @end // 写 person 添加3个属性。通过 runtime 获取运行时属性。 unsigned int outCount = 0; objc_property_t *properties = class_copyPropertyList([Person class], &outCount); NSLog(@"%d", outCount); for (NSInteger i = 0; i < outCount; ++i) { NSString *name = @(property_getName(properties[i])); NSString *attributes = @(property_getAttributes(properties[i])); NSLog(@"name:%@\nattributes:%@", name, attributes); } [10522:615669] 4 [10522:615669] name:name attributes:T@"NSString",&,N,V_name [10522:615669] name:age attributes:Ti,N,V_age [10522:615669] name:weight attributes:Td,N,V_weight
runtime 与消息
- 消息知道运行时才会与方法实现进行绑定。
- objc_msgSend 方法看起来好像返回了数据,其实 objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。消息发送步骤:
- 首先你要检测 selector 是不是要忽略。mac 开发有了垃圾回收旧不理会 retain、release 这些函数。
- 检测这个 selector 的 target 是不是 nil。objc 允许我们对一个 nil 对象执行任何方法不会 crash,因为运行时会被忽略掉。
- 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 中找,如果找到了就运行对应的函数去执行相应的代码。
- 如果 cache 找不到就找类的方法列表中是否有对应的方法。
- 如果累的方法列表中找不到就到父类的方法列表中找,一直找到 NSObject 类为止。
- 如果还没找到,就要开始进入动态方法解析了。
- 在消息传递中,编译器会根据情况在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper——stret 这个四个方法中选择一个调用。如果消息传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用带有 stret 的函数。
方法中的隐藏参数
- 疑问:我们经常用到关键字 self,但是 self 是如何获取当前方法的对象的呢?其实这也是 runtime 系统的作用,self 是在方法运行时被动态传入的。
- 当 objc_msgSend 找到方法对应实现时,他将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时还有两个隐藏参数:
- 接受消息的对象(self 所指向的内容,当前方法的对象指针)
- 方法选择器(_cmd 指向的内容,当前指针的 SEL 指针)
- 因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们实在代码编译阶段被插入方法实现中的。尽管这些参数没有被明确声明,在源码中我们仍然可以引用它们。
- 这两个参数中,self 更实用。他是在方法实现中访问消息接收者对象的实例变量的途径。
- 这时我们会想到另一个关键字 Super,实际上 Super 关键字接收消息时,编译器会创建一个 objc_super 结构体
消息转发
- 重定向
- 消息转发机制执行前,runtime 系统允许我们替换消息的接收者为其他对象。通过- (id)forwardingTargetForSelector:(SEL)aSelector 方法。
- 如果返回为 nil 或者 self,则会计入消息转发机制(forwardInvocation:),否则向返回的对象重新发送消息。
- 转发
- 当动态方法解析不做处理返回 NO 时,则会触发消息转发机制。
动态绑定
- 在运行时确定要调用的方法,动态绑定将调用方法的确定也推迟到运行时。在编译
时,方法的调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的
代码。通过动态类型和动态绑定技术,代码每次执行都可以得到不同的结果。运行时
因子负责确定消息的接收者和被调用的方法。运行时的消息分发机制为动态绑定提供
支持。当向一个动态类型确定了的对象发送消息时,运行环境系统会通过接收者的isa
指针定位对象的类,并以此为起点确定被调用的方法,方法和消息是动态绑定的。而
且,不必在0bjective-C 代码中做任何工作,就可以自动获取动态绑定的好处。在每次发送消息时,特别是当消息的接收者是动态类型已经确定的对象时,动态绑定就会例行而透明地发生