Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。
这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。
runtime(简称运行时
),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。
对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。
OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
事实证明:在编译阶段,OC 可以 调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。它的定义如下:
typedef struct objc_class *Class;
查看objc/runtime.h中objc_class结构体的定义如下:
1 struct objc_class { 2 Class isa OBJC_ISA_AVAILABILITY; 3 4 #if !__OBJC2__ 5 Class super_class OBJC2_UNAVAILABLE; // 父类 6 const char *name OBJC2_UNAVAILABLE; // 类名 7 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 8 long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识 9 long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小 10 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表 11 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表 12 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存 13 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表 14 #endif 15 16 } OBJC2_UNAVAILABLE;
在这个定义中,下面几个字段是我们感兴趣的
isa:需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类),我们会在后面介绍它。
super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。
cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。
Objective-C runtime目前有两个版本:Modern runtime和Legacy runtime。Modern Runtime 覆盖了64位的Mac OS X Apps,还有 iOS Apps,Legacy Runtime 是早期用来给32位 Mac OS X Apps 用的,也就是可以不用管就是了。
针对cache,我们用下面例子来说明其执行过程:
1 NSArray *array = [[NSArray alloc] init];
其流程是:
[NSArray alloc]先被执行。因为NSArray没有+alloc方法,于是去父类NSObject去查找。
检测NSObject是否响应+alloc方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把isa指针指向NSArray类。同时,+alloc也被加进cache列表里面。
接着,执行-init方法,如果NSArray响应该方法,则直接将其加入cache;如果不响应,则去父类查找。
在后期的操作中,如果再以[[NSArray alloc] init]这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
objc_object是表示一个类的实例的结构体,它的定义如下(objc/objc.h):
1 struct objc_object 2 { 3 Class isa OBJC_ISA_AVAILABILITY; 4 }; 5 6 typedef struct objc_object *id;
可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。
当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。
另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。如:
1 NSArray *array = [NSArray array];
这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念
meta-class是一个类对象的类。
当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
meta-class之所以重要,是因为它存储着一个类的所有类方法。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。
讲了这么多,我们还是来写个例子吧:
1 void TestMetaClass(id self, SEL _cmd) { 2 3 NSLog(@"This objcet is %p", self); 4 NSLog(@"Class is %@, super class is %@", [self class], [self superclass]); 5 6 Class currentClass = [self class]; 7 for (int i = 0; i < 4; i++) { 8 NSLog(@"Following the isa pointer %d times gives %p", i, currentClass); 9 currentClass = objc_getClass((__bridge void *)currentClass); 10 } 11 12 NSLog(@"NSObject's class is %p", [NSObject class]); 13 NSLog(@"NSObject's meta class is %p", objc_getClass((__bridge void *)[NSObject class])); 14 } 15 16 #pragma mark - 17 18 @implementation Test 19 20 - (void)ex_registerClassPair { 21 22 Class newClass = objc_allocateClassPair([NSError class], "TestClass", 0); 23 class_addMethod(newClass, @selector(testMetaClass), (IMP)TestMetaClass, "v@:"); 24 objc_registerClassPair(newClass); 25 26 id instance = [[newClass alloc] initWithDomain:@"some domain" code:0 userInfo:nil]; 27 [instance performSelector:@selector(testMetaClass)]; 28 } 29 30 @endlogs_code_collapse">View Code
这个例子是在运行时创建了一个NSError的子类TestClass,然后为这个子类添加一个方法testMetaClass,这个方法的实现是TestMetaClass函数。
运行后,打印结果是
2014-10-20 22:57:07.352 mountain[1303:41490] This objcet is 0x7a6e22b0 2014-10-20 22:57:07.353 mountain[1303:41490] Class is TestStringClass, super class is NSError 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 0 times gives 0x7a6e21b0 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 1 times gives 0x0 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 2 times gives 0x0 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 3 times gives 0x0 2014-10-20 22:57:07.353 mountain[1303:41490] NSObject's class is 0xe10000
2014-10-20 22:57:07.354 mountain[1303:41490] NSObject's meta class is 0x0
我们在for循环中,我们通过objc_getClass来获取对象的isa,并将其打印出来,依此一直回溯到NSObject的meta-class。分析打印结果,可以看到最后指针指向的地址是0x0,即NSObject的meta-class的类地址。
这里需要注意的是:我们在一个类对象调用class方法是无法获取meta-class,它只是返回类而已。
动态交换两个方法的实现
动态添加属性
实现字典转模型的自动转换
发送消息
动态添加方法 (面试用到)
拦截并替换方法
实现 NSCoding 的自动归档和解档
应用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
注解:OC 中我们很习惯的会用懒加载,当用到的时候才去加载它,但是实际上只要一个类实现了某个方法,就会被加载进内存。当我们不想加载这么多方法的时候,就会使用到 runtime
动态的添加方法。
如果你实现过自定义模型数据持久化的过程,那么你也肯定明白,如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject
和 decodeObjectForKey
方法,如果这样的模型又有很多个,这还真的是一个十分麻烦的事情。下面来看看简单的实现方式。
我们写 OC 代码,它在运行的时候也是转换成了 runtime
方式运行的。任何方法调用本质:就是发送一个消息(用 runtime
发送消息,OC 底层实现通过 runtime
实现)。
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
每一个 OC 的方法,底层必然有一个与之对应的 runtime
方法。
示例代码:OC 方法-->runtime 方法
说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;
1 // Person *p = [Person alloc]; 2 // 底层的实际写法 3 Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")); 4 5 // p = [p init]; 6 p = objc_msgSend(p, sel_registerName("init")); 7 8 // 调用对象方法(本质:让对象发送消息) 9 //[p eat]; 10 11 // 本质:让类对象发送消息 12 objc_msgSend(p, @selector(eat)); 13 objc_msgSend([Person class], @selector(run:),20); 14 15 //--------------------------- <#我是分割线#> ------------------------------// 16 // 也许下面这种好理解一点 17 18 // id objc = [NSObject alloc]; 19 id objc = objc_msgSend([NSObject class], @selector(alloc)); 20 21 // objc = [objc init]; 22 objc = objc_msgSend(objc, @selector(init));View Code
面试:消息机制方法调用流程
eat
方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class
)中方法列表)。
runtime
库会根据对象的 isa
指针找到该对象对应的类或其父类中查找方法。。objc
对象的 isa
的指针指向什么?有什么作用?
获取属性列表
1 objc_property_t *propertyList = class_copyPropertyList([self class], &count); 2 for (unsigned int i=0; i<count; i++) { 3 const char *propertyName = property_getName(propertyList[i]); 4 NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]); 5 }
获取方法列表
1 Method *methodList = class_copyMethodList([self class], &count); 2 for (unsigned int i; i<count; i++) { 3 Method method = methodList[i]; 4 NSLog(@"method---->%@", NSStringFromSelector(method_getName(method))); 5 }
获取成员变量列表
1 Ivar *ivarList = class_copyIvarList([self class], &count); 2 for (unsigned int i; i<count; i++) { 3 Ivar myIvar = ivarList[i]; 4 const char *ivarName = ivar_getName(myIvar); 5 NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]); 6 }
获取协议列表
1 __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count); 2 for (unsigned int i; i<count; i++) { 3 Protocol *myProtocal = protocolList[i]; 4 const char *protocolName = protocol_getName(myProtocal); 5 NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]); 6 }
现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
获得类方法
1 Class PersonClass = object_getClass([Person class]); 2 SEL oriSEL = @selector(test1); 3 Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
获得实例方法
1 Class PersonClass = object_getClass([xiaoming class]); 2 SEL oriSEL = @selector(test2); 3 Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
添加方法
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
替换原方法实现
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
交换两个方法的实现
method_exchangeImplementations(oriMethod, cusMethod);
这是个最基本的用于发送消息的函数。其实编译器会根据情况在objc_msgSend
, objc_msgSend_stret
,,objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有 Super
的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret
的函数。
2、SELobjc_msgSend
函数第二个参数类型为SEL
,它是selector
在Objc中的表示类型(Swift中是Selector类)。selector
是方法选择器,可以理解为区分方法的 ID
,而这个 ID
的数据结构是SEL
:typedef struct objc_selector *SEL;
其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()``或者 Runtime
系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。
3、id
objc_msgSend
第一个参数类型为id
,大家对它都不陌生,它是一个指向类实例的指针:typedef struct objc_object *id;
那objc_object
又是啥呢:struct objc_object { Class isa; };
objc_object
结构体包含一个isa
指针,根据isa
指针就可以顺藤摸瓜找到对象所属的类。
1 struct objc_class { 2 Class isa OBJC_ISA_AVAILABILITY;//每个Class都有一个isa指针 3 4 #if !__OBJC2__ 5 Class super_class OBJC2_UNAVAILABLE;//父类 6 const char *name OBJC2_UNAVAILABLE;//类名 7 long version OBJC2_UNAVAILABLE;//类版本 8 long info OBJC2_UNAVAILABLE;//!*!供运行期使用的一些位标识。如:CLS_CLASS (0x1L)表示该类为普通class; CLS_META(0x2L)表示该类为metaclass等(runtime.h中有详细列出) 9 long instance_size OBJC2_UNAVAILABLE;//实例大小 10 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;//存储每个实例变量的内存地址 11 struct objc_method_list **methodLists OBJC2_UNAVAILABLE;//!*!根据info的信息确定是类还是实例,运行什么函数方法等 12 struct objc_cache *cache OBJC2_UNAVAILABLE;//缓存 13 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//协议 14 #endif 15 16 } OBJC2_UNAVAILABLE; 17 18
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class
结构体中:`ivars是
objc_ivar_list指针;
methodLists是指向
objc_method_list指针的指针。也就是说可以动态修改
*methodLists的值来添加成员方法,这也是
Category`实现的原理。
简单说就是进行方法交换
在Objective-C
中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector
的名字。利用Objective-C
的动态特性,可以实现在运行时偷换selector
对应的方法实现,达到给方法挂钩的目的
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector
的本质其实就是方法名,IMP
有点类似函数指针,指向具体的Method
实现,通过selector
就可以找到对应的IMP
。
method_exchangeImplementations
交换两个方法的实现class_replaceMethod
替换方法的实现method_setImplementation
来直接设置某个方法的IMP
。
作为对Runtime的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。
在Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。
注:Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。
一个数组的类型编码位于方括号中;其中包含数组元素的个数及元素类型。如以下示例:
1 float a[] = {1.0, 2.0, 3.0}; 2 NSLog(@"array encoding type: %s", @encode(typeof(a)));
输出是:
2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]
其它类型可参考Type Encoding,在此不细说。
另外,还有些编码类型,@encode虽然不会直接返回它们,但它们可以作为协议中声明的方法的类型限定符。可以参考Type Encoding。
对于属性而言,还会有一些特殊的类型编码,以表明属性是只读、拷贝、retain等等,详情可以参考Property Type String。
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
typedef struct objc_selector *SEL;
objc_selector结构体的详细定义没有在头文件中找到。方法的selector用于表示运行时方 法的名字。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int类型的地址),这个标识就是SEL。如下 代码所示:
SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);
上面的输出为:
2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在 Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。这也就导致 Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。如在某个类中定义以下两个方法:
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去 找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度 上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么 SEL仅仅是函数名了。
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。
我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
sel_registerName函数
Objective-C编译器提供的@selector()
NSSelectorFromString()方法