运行时(runtime)
- 什么是
runtime(运行时)?runtime是一套比较底层的纯C语言API,属于1个C语言库, 包含了很多底层的C语言API,功能强大。 在我们平时编写的OC代码中,程序运行过程时, 其实最终都是转成了runtime的C语言代码,runtime算是OC的幕后工作者。
使用 clang 重写 OC 代码,可以看到我们写的 OC 代码都被转成了 runtime 代码
- 新建一个
命令行项目 - 新建
Person类,添加name和age属性 - 使用
clang重写OC代码clang --help | grep objc #-rewrite-objc Rewrite Objective-C source to C++ clang main.m -rewrite-objc - 对比
OC和 转成的runtime代码OC代码Person *p1 = [[Person alloc] init];- 对应
runtime代码Person *p1 = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")); - 将类型转换代码去掉,精简一下
Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")); Person *p1 = objc_msgSend(p1, sel_registerName("init")); - 总结:
Person *p1 = [[Person alloc] init];- 调用
Person的类方法创建Person对象p, - 在调用
p对象的init对象方法 - OC 调用方法其实都是给对象发消息,使用
objc_msgSend发消息
- 调用
其实在
OC代码中可以直接调用runtime方法int main(int argc, const char * argv[]) { @autoreleasepool { // Person *p1 = [[Person alloc] init]; Person *p = objc_msgSend(NSClassFromString(@"Person"), @selector(alloc)); Person *p1 = objc_msgSend(p, @selector(init)); // p1.name = @"冰冰"; objc_msgSend(p1, @selector(setName:), @"冰冰"); NSString *name = objc_msgSend(p1, @selector(name)); NSLog(@"name = %@", name); } return 0; }- 可以看到将:
换成Person *p1 = [[Person alloc] init];将Person *p = objc_msgSend(NSClassFromString(@"Person"), @selector(alloc)); Person *p1 = objc_msgSend(p, @selector(init));换成p1.name = @"冰冰";将objc_msgSend(p1, @selector(setName:), @"冰冰");换成NSLog(@"name = %@", p1.name);程序依然能够运行NSString *name = objc_msgSend(p1, @selector(name)); NSLog(@"name = %@", name); - 有人会说这没什么了不起的.那么接下来我们将
Person.h里面的name和age属性放到Person.m的@interface-extension里面.这样的话因为没有在Person.h公开,那么外界不知道Person有name和age属性,使用有使用p1.name = @"冰冰";就完全行不通了,但是这个时候依然可以使用objc_msgSend(p1, @selector(setName:), @"冰冰");来调用Person的setName:方法 - 也就是说以后想使用别人没有公开的方法可以直接使用
objc_msgSend
初识 runtime
在
iOS开发中,runtime可以用来干什么呢?,我们先来看看runtime的头文件, 使用command + shift + o查找runtime.h文件,runtime相关的内容基本都定义在这个头文件中了我们在
OC中使用的类实际上是一个结构体,在runtime.h定义如下:struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识 long instance_size OBJC2_UNAVAILABLE; // 类的实例变量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 类的成员变量链表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 #endif } OBJC2_UNAVAILABLE;我们在
OC对象中定义的实例变量如_name实际上也是一个结构体,在runtime.h定义如下:typedef struct objc_ivar *Ivar; struct objc_ivar { char *ivar_name OBJC2_UNAVAILABLE; // 变量名 char *ivar_type OBJC2_UNAVAILABLE; // 变量类型 int ivar_offset OBJC2_UNAVAILABLE; // 基地址偏移字节 #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }我们在
OC对象中定义的方法实际上对应三个结构体,在runtime.h定义如下:SEL实际是objc_selector但是在runtime.h找不到, 仅在objc.h定义如下typedef struct objc_selector *SEL;SEL又叫选择器,是表示一个方法的selector的指针,映射方法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。SEL的作用是作为IMP的KEY,存储在NSSet中,便于hash快速查询方法。在
Objective-C同一个类(及类的继承体系)中SEL不能相同。 所以同一个类(及类的继承体系)中,不能存在2个同名的方法,就算参数类型不同也不行。不同的类可以有相同的方法名,多个方法可以有同一个SEL, 不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。SEL使用定义
Person类, 并定义sayName方法@implementation Person - (void)sayName { NSLog(@"sayName = %@", self); } @end定义
Animal类, 并定义sayName方法@implementation Animal - (void)sayName { NSLog(@"sayName = %@", self); } @end在
main方法中使用sayName创建一个SEL, 不同类的对象调用同一个SELint main(int argc, const char * argv[]) { @autoreleasepool { // 创建 SEL SEL sel = NSSelectorFromString(@"sayName"); Person *p1 = [[Person alloc] init]; Animal* a1 = [[Animal alloc] init]; // 不同类的实例对象执行相同的 selector 时,会在各自的方法列表中去根据 selector 去寻找自己对应的 IMP [p1 performSelector:sel]; // 输出 sayName = <Person: 0x100206ee0> [a1 performSelector:sel]; // 输出 sayName = <Animal: 0x100106fa0> } return 0; }
IMP实际是一个函数指针但是在runtime.h找不到, 仅在objc.h定义如下typedef id (*IMP)(id, SEL, ...);IMP是指向实现函数的指针,通过SEL取得IMP后,我们就获得了最终要找的实现函数的入口IMP使用int main(int argc, const char * argv[]) { @autoreleasepool { Person *p1 = [[Person alloc] init]; Animal* a1 = [[Animal alloc] init]; // 创建 SEL SEL sel = NSSelectorFromString(@"sayName"); // 根据 sel 去不同类的对象里面找对应的IMP IMP imp1 = [p1 methodForSelector:sel]; IMP imp2 = [a1 methodForSelector:sel]; // 每个对象执行对应的imp imp1(p1,sel); imp2(a1,sel); } return 0; }- 由于每个方法对应唯一的
SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针 了。通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。 如果类似于for循环这种情况下频繁调用同一方法,可以直接调用
IMP以提高性能int main(int argc, const char * argv[]) { @autoreleasepool { Person *p1 = [[Person alloc] init]; Animal* a1 = [[Animal alloc] init]; // 创建 SEL SEL sel = NSSelectorFromString(@"sayName"); // 根据 sel 去不同类的对象里面找对应的IMP IMP imp1 = [p1 methodForSelector:sel]; IMP imp2 = [a1 methodForSelector:sel]; // 每个对象执行对应的imp imp1(p1,sel); // 调用同一个方法1000次 for (int i = 0; i < 1000; i++) { imp2(a1,sel); } } return 0; }
Method对应objc_method结构体,在runtime.h定义如下:typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; // 参数类型 IMP method_imp OBJC2_UNAVAILABLE; // 方法实现 } // {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_Person_setName_}- 我们可以看到该结构体中包含一个
SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。 runtime提供了一系列的方法来处理与方法相关的操作// 调用指定方法的实现 id method_invoke (id receiver, Method m, ...); // 调用返回一个数据结构的方法的实现 void method_invoke_stret (id receiver, Method m, ...); // 获取方法名 SEL method_getName (Method m); // 返回方法的实现 IMP method_getImplementation (Method m); // 获取描述方法参数和返回值类型的字符串 const char * method_getTypeEncoding (Method m); // 获取方法的返回值类型的字符串 char * method_copyReturnType (Method m); // 获取方法的指定位置参数的类型字符串 char * method_copyArgumentType (Method m, unsigned int index); // 通过引用返回方法的返回值类型字符串 void method_getReturnType (Method m, char *dst, size_t dst_len); // 返回方法的参数的个数 unsigned int method_getNumberOfArguments (Method m); // 通过引用返回方法指定位置参数的类型字符串 void method_getArgumentType (Method m, unsigned int index, char *dst, size_t dst_len); // 返回指定方法的方法描述结构体 struct objc_method_description * method_getDescription (Method m); // 设置方法的实现 IMP method_setImplementation (Method m, IMP imp); // 交换两个方法的实现 void method_exchangeImplementations (Method m1, Method m2);- 方法调用流程
- 编译器会将消息表达式
[obj method]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收对象(obj)和方法名(method)作为其基础参数,如以下所示:
如果方法需要参数如下所示:objc_msgSend(obj, method)objc_msgSend(obj, method, arg1, arg2, ...) objc_msgSend首先它找到method对应的IMP(方法实现)。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收对象的类来找到的确切的实现。objc_msgSend调用IMP(方法实现),并将接收对象及方法的所有参数传给它。- 最后,
objc_msgSend将IMP(方法实现) 的返回值作为objc_msgSend自己的返回值。
- 编译器会将消息表达式
方法调用过程中的根据
SEL查找对象的IMP主要依赖
objc_class结构体中的: 1.isa2.super_class3.objc_method_list4.objc_cachestruct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识 long instance_size OBJC2_UNAVAILABLE; // 类的实例变量大小 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 类的成员变量链表 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 #endif } OBJC2_UNAVAILABLE;- 寻找IMP的过程:
- 先从当前
class的cache方法列表(cache methodLists)里去找 找到了,跳到对应函数实现 - 没找到,就从
class的方法列表(methodLists)里找 - 还找不到,就到
super class的方法列表里找,直到找到基类(NSObject)为止 - 最后再找不到,就会进入动态方法解析和消息转发的机制。
- 先从当前
- 我们可以看到该结构体中包含一个