关于Runtime的一些理论和实践
前言
网上讲解Runtime的教程非常之多,有些讲的还算不错,有些感觉就是人云亦云,所以读者就应该好好甄别下学习的资料。我在本文的最后,会列出一些我认为讲的不错的参考资料,以供参考。接下来就进入正文
正文
理论部分
基础中的基础
 假设看官有一定的数据结构知识,知道什么叫结构体,因为接下来我要讲述的这个东西就是从结构体开始的。如果有不懂的同学请走这里传送门
 iOS中Class的定义如下
| 1 | struct objc_class {//class 的结构体 | 
 isa指针是实例方法在运行时能够被执行的关键,运行时系统将依靠isa指针找到实例所在的类,进而找到方法列表和缓存方法,进行消息的发送。如果消息得不到执行,将根据类里面的super_classs指针找父类的实例方法。但是如果是类方法或者说是+号方法进行消息发送,那么将会获取类里面的isa指针进而找到元类,获取元类里面的方法列表或缓存方法进行消息处理,如果没找到,那么将去寻找元类里面的super_class指针找父元类的类方法。
 下面是哥们从网上找的一张isa 和super_class指针的指向图

基础
 有了基础中的基础之后,我们才知道Objective-C的本源其实也是来自于C语言,来自于结构体。那么有了这些之后,怎么让OC的类和对象在我们的程序内部运作起来,接下来,有个新的东西要登场了,那就是objc_msgSend,请记住它,正是有了它objective-c才真正称得上是一门面向运行时的语言。
 objc_msgSend实质上就是一个C函数。其次objc_msgSend的参数类型第一个必须是id,第二个是SEL,第三个是va_list参数列表。从cocoa的objc.h文件中,我们可以看到它的定义。
| 1 | id objc_msgSend(id self, SEL op, ...)//运行时消息发送的函数 | 
 当你以为这就是运行时发送消息的全部时,很不幸的告诉你,远远没有这么简单,objc_msgSend还有一大帮兄弟姐妹,它们如下
| 1 | id objc_msgSendSuper(struct objc_super *super, SEL op, ...)//super 指针调用方法的发送的消息 | 
 其实,这些并不重要,只要掌握objc_msgSend这个就可以了,上面的这些兄弟姐妹只需要了解下就行。
消息发送
 当[obj message]调用时,将会被转化为((void (*)(id,@sel(message))objc_msgSend)(id,@sel(message))。此消息将走如下几个流程
- 检查接收的对象是否为nil,如果是,调用nil处理方案
- 在objc_object结构体中含有cache,首先会在Class的cache中查找IMP(如果没有缓存则会初始化缓存),如果找到就会跳转到对应的函数上执行。
- 如果没有找到就像父类的Class查找,如果还没有没找到就继续向上查找,直到找到根类。
- 如果找到根类还是没有实现方案,这个时候就需要使用_objc_msgForward函数指针替代imp,最后来执行这个imp(动态方法实现)。
消息转发
| 1 | + (BOOL)resolveInstanceMethod:(SEL)aSEL //动态解析方法,给处理不了的sel动态加IMP实现 | 
 理论部分到此告一段落,接下里用实践的方式,来深入的理解下这套东西
实践部分
解归档对象
我们知道oc的对象如果确认了NSCoding协议的话,可以将对象归档为NSData,进而存储在文件中,方便下次使用,通常的做法是将一个对象的属性一个一个的按照类型进行encode,如int型的变量,采用encodeInt:forKey,bool 型的变量,采用encodeBool:forKey。有没有一种方法不管是任何对象都可以进行归档。而不需要我们手动去辨别对象类型,进而调用相应的归档方法呢,runtime的存在对解决这类问题提供了可能。下面以归档一个CYArchieve对象为例,进行阐释。
- 首先第一步,获取当前对象的成员变量个数。 - 1 
 2- unsigned int count; 
 Ivar *ivar = class_copyIvarList([self class], &count);- 此处调用了一个class_copyIvarList()方法,查询runtime,知道这个方法的作用如下 - 1 
 2
 3- // 根据已知的类,获取它的变量列表,outcount是这个变量列表的长度。如果一个类是nil或者没有变量列表,那么将返回NULL,且outCount将为0.注意该对象使用完之后,记得必须要free()!!! 
 OBJC_EXPORT Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
 OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);- 调用这个方法之后,也就是说,可以获取到当前类的所有变量,和变量个数 
- 第二步,解析Ivar,获取变量名称 - 这里还有一个Ivar,这是什么东西?查看其定义知道,这是一个不透明的结构体指针,封装了变量类型。ø - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- /// An opaque type that represents an instance variable. 
 typedef struct objc_ivar *Ivar;
 struct objc_ivar {
 char *ivar_name //变量名字 OBJC2_UNAVAILABLE;
 char *ivar_type //变量类型 OBJC2_UNAVAILABLE;
 int ivar_offset //变量在对象中的偏移量 OBJC2_UNAVAILABLE;
 int space
 OBJC2_UNAVAILABLE;
 }- 它有什么用呢?再来查找资料,苹果runtime现在已经开源,大可去其源码中找答案。 - 翻翻翻,翻到runtime的定义中,找到了它的用途。Ivar有很多用途,这里我们使用到的有下面俩个 - 1 
 2
 3
 4
 5
 6- // 根据获取到的Ivar结构体,获取它的名字 
 OBJC_EXPORT const char *ivar_getName(Ivar v)
 OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
 // 根据获取到的Ivar结构体,获取它的类型
 OBJC_EXPORT const char *ivar_getTypeEncoding(Ivar v)
 OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);- 这俩个方法,可以帮助我们拿到当前变量的名称和变量的类型。名称,我们知道是变量的名称。如_name _title,之类的,可是类型是什么呢?还是得靠查苹果资料,我们找到了官方定义下图是部分定义  
- 第三步,进行归档 - 现在有了变量名称和变量类型,我们基本就可以对对象的变量进行归档了。 - 头文件如下定义 - 1 
 2
 3
 4
 5
 6
 7- @interface CYArchieve : NSObject 
 @property (nonatomic, copy) NSString *userName;
 @property (nonatomic, assign) const void * token;
 @property (nonatomic, assign) int length;
 @property (nonatomic, strong) NSNumber *age;
 @property (nonatomic, assign) float height;
 @end- 对此对象进行归档 - 具体代码如下 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54- - (void)encodeWithCoder:(NSCoder *)aCoder 
 {
 unsigned int count;
 Ivar *ivar = class_copyIvarList([self class], &count);
 for (unsigned int i = 0; i < count; i++) {
 Ivar iva = ivar[i];
 
 // 成员变量名
 const void* name = ivar_getName(iva);
 NSString *ivarName = [NSString stringWithUTF8String:name];
 
 ivarName = [ivarName substringFromIndex:1];
 
 // 获取get方法
 SEL getter = NSSelectorFromString(ivarName);
 
 // 能响应getter方法
 if ([self respondsToSelector:getter]) {
 const void *typeEncoding = ivar_getTypeEncoding(iva);//获取变量类型
 NSString *type = [NSString stringWithUTF8String:typeEncoding];//将c字符串转变为oc字符串
 
 NSLog(@"type = %@",type);
 // const void *
 if ([type isEqualToString:@"r^v"]) {
 const void * valueUTF8 = ((const void *(*)(id ,SEL))(void *)objc_msgSend)((id)self,getter);
 NSString *value = [NSString stringWithUTF8String:valueUTF8];
 [aCoder encodeObject:value forKey:ivarName];
 
 continue;
 }
 else if ([type isEqualToString:@"f"]){
 float fvalue = ((float(*)(id,SEL))(void *)objc_msgSend)((id)self, getter);
 [aCoder encodeObject:@(fvalue) forKey:ivarName];
 continue;
 }
 else if ([type isEqualToString:@"i"]){
 int ivalue = ((int (*)(id, SEL))(void *)objc_msgSend)((id)self,getter);
 [aCoder encodeObject:@(ivalue) forKey:ivarName];
 continue;
 }
 id obj = ((id(*)(id, SEL))(void *)objc_msgSend)((id)self,getter);//如果是OC类型的对象,这么获取
 if (obj && [obj respondsToSelector:@selector(encodeWithCoder:)]) {//判断是否有返回值
 [aCoder encodeObject:obj forKey:ivarName];//之后进行编码
 }
 
 /*//////////////////////////////////////////////////////////////////////////*/
 /* 将(void*)指针强制转为返回值为float类型,参数为id,sel的函数指针。
 ((float(*)(id,SEL))(void *)objc_msgSend)((id)self, getter);
 */
 /*//////////////////////////////////////////////////////////////////////////*/
 }
 }
 free(ivar);
 }
- 解档和归档类似,代码如下 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62- - (id)initWithCoder:(NSCoder *)aDecoder 
 {
 self = [super init];
 if (self) {
 unsigned int count;
 Ivar *ivars = class_copyIvarList([self class], &count);
 
 for (unsigned int i = 0; i < count; i++) {
 Ivar ivar = ivars[i];
 const void * ivarName = ivar_getName(ivar);
 NSString *ivarString = [NSString stringWithUTF8String:ivarName];
 ivarString = [ivarString substringFromIndex:1];
 NSString *setter = ivarString;
 if (![setter hasPrefix:@"_"]) {
 char firstLatter = [setter characterAtIndex:0];
 NSString *firstLetterString = [NSString stringWithFormat:@"%c",firstLatter];
 setter = [setter substringFromIndex:1];
 setter = [NSString stringWithFormat:@"%@%@",[firstLetterString uppercaseString] ,setter];
 }
 
 setter = [NSString stringWithFormat:@"set%@:",setter];
 
 SEL Sel = NSSelectorFromString(setter);
 
 if ([self respondsToSelector:Sel]) {
 const void *typeUTF8 = ivar_getTypeEncoding(ivar);
 NSString *type = [NSString stringWithUTF8String:typeUTF8];
 
 if ([type isEqualToString:@"r^v"]) {
 NSString *value = [aDecoder decodeObjectForKey:ivarString];
 if (value) {
 ((void (*)(id,SEL,const void *))(void *)objc_msgSend)((id)self,Sel,value.UTF8String);
 }
 continue;
 }
 else if ([type isEqualToString:@"i"]){
 NSNumber *number = [aDecoder decodeObjectForKey:ivarString];
 int num = [number intValue];
 ((void(*)(id ,SEL,int))(void *)objc_msgSend)((id)self,Sel,num);
 continue;
 }
 else if ([type isEqualToString:@"f"]){
 NSNumber *number = [aDecoder decodeObjectForKey:ivarString];
 float flo = [number floatValue];
 ((void(*)(id ,SEL,float))(void *)objc_msgSend)((id)self,Sel,flo);
 continue;
 }
 
 id obj = [aDecoder decodeObjectForKey:ivarString];
 if (obj) {
 ((void(*)(id, SEL, id))(void *)objc_msgSend)((id)self,Sel,obj);
 }
 
 }
 }
 
 free(ivars);
 }
 
 return self;
 }
消息转发
这一小节,我来研究下runtime里面的消息转发机制,都知道,objc_msgsend()发送之后,如果目标不能执行相应的SEL,会进入到前文我们介绍的那四个方法中,具体的实践是怎么样的呢,这里我写个demo来演示下,一方面是自己学习,另一方面也做个笔记,方便以后查阅。
- 动态添加方法 - 首先创建了一个CYStudent类,然后在头文件声明了一个study方法 - 1 
 2
 3
 4
 5- @interface CYStudent : NSObject 
 - (void)study;
 @end- 但是,我们在.m文件中不实现此方法,由此来验证,是否会进入消息转发流程,之后在.m文件中实现resolveInstanceMethod:此方法,如下 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14- void play(id obj,SEL sel){ 
 NSLog(@"这个学生不学习,只知道玩耍!");
 }
 @implementation CYStudent
 + (BOOL)resolveInstanceMethod:(SEL)sel
 {
 if([NSStringFromSelector(sel) isEqualToString:@"study"]){
 return class_addMethod(self, sel, (IMP)play,"v@:");
 }
 return [super resolveInstanceMethod:sel];
 }
 @end- 经如下代码测试,发现确实进入了play函数,执行结果如下 - 1 
 2
 3
 4- CYStudent *laosan = [CYStudent new]; 
 [laosan study];
 2017-06-08 22:21:34.997 RuntimeDemo[1381:190411] 这个学生不学习,只知道玩耍!
- 消息转发 - 这里以CYTeacher为例,依然是让CYTeacher执行study方法,但是不给其动态解析的机会,让其将消息发送给CYStudent,CYStudent将继续动态添加方法的流程,具体实现如下 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27- @interface CYTeacher : NSObject 
 @end
 @implementation CYTeacher
 + (BOOL)resolveInstanceMethod:(SEL)sel
 {
 return NO;
 }
 - (id)forwardingTargetForSelector:(SEL)aSelector
 {
 //如果转发的消息是play,老师执行不了,那就让学生对象执行下试试
 if ([NSStringFromSelector(aSelector) isEqualToString:@"study"]) {
 return [CYStudent new];
 }
 
 return [super forwardingTargetForSelector:aSelector];
 }
 @end
 /*/////////////////////////////以下为测试代码///////////////////////////////*/
 CYTeacher *teacher = [CYTeacher new];
 
 [teacher performSelector:@selector(study) withObject:nil afterDelay:0];
 
 /*/////////////////////////////以下为执行结果///////////////////////////////*/
 2017-06-08 22:31:23.541 RuntimeDemo[1457:215285] 这个学生不学习,只知道玩耍!
- 方法签名 - 接下来研究下方法签名和方法调用的问题,以CYDoctor为例,此处还是让CYDoctor对象执行study方法,但是不实现resolveInstanceMethod: 和forwardingTargetForSelector:这俩个方法,而是让其进入方法签名里面,生成一个方法签名,之后再执行forwardInvocation:方法。以下是我做的测试代码 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46- @interface CYDoctor : NSObject 
 @end
 @implementation CYDoctor
 + (BOOL)resolveInstanceMethod:(SEL)sel
 {
 return NO;
 }
 - (id)forwardingTargetForSelector:(SEL)aSelector
 {
 return nil;
 }
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
 {
 if ([NSStringFromSelector(aSelector) isEqualToString:@"study"]) {
 return [NSMethodSignature signatureWithObjCTypes:"v@:"];
 }
 return [super methodSignatureForSelector:aSelector];
 }
 - (void)forwardInvocation:(NSInvocation *)anInvocation
 {
 [anInvocation setSelector:@selector(surgery:)];
 [anInvocation setTarget:self];
 [anInvocation invoke];
 }
 - (void)surgery:(id)sender
 {
 NSLog(@"sender : %@",sender);
 
 NSLog(@"手术中,请勿打扰!!!");
 }
 @end
 /*/////////////////////////////以下为测试代码///////////////////////////////*/
 CYDoctor *doctor = [CYDoctor new];
 
 [doctor performSelector:@selector(study) withObject:nil afterDelay:0];
 
 /*/////////////////////////////以下为执行结果///////////////////////////////*/
 2017-06-08 22:45:39.442 RuntimeDemo[1544:239413] sender : (null)
 2017-06-08 22:45:39.442 RuntimeDemo[1544:239413] 手术中,请勿打扰!!!- 经过这三个小例子,基本对objc_msgSend()的发送流程搞清楚了,接下里准备研究下MethodSwizzling 
MethodSwizzling
以NSMutableArray为例,我们来替换系统的objectAtIndex:,insertObject:AtIndex:,removeObjectAtIndex:等方法。
首先我们给NSobject类添加一个category,用来给任何类进行methodSwizzling。并提供一个方法,用来交换原方法和替换方法。
| 1 | 
 | 
这之后,我们创建一个NSMutableArray的category,用来执行运行时方法替换。
| 1 | 
 | 
接下来是一个例子代码,我们来验证下替换的成果。
| 1 | id nilobjc = nil; | 
打印结果为
| 1 | 2017-06-11 22:43:18.705 RuntimeDemo[38559:4195212] index is bigger than count | 
小结
runtime的学习是持续进行了,我这篇blog也仅仅是抛砖引玉,如何灵活的在项目中使用Runtime才是我们真正应该做的。切记不可为了使用某项技术,硬刚。这是不好滴。😆
