浅谈runtime的几个应用场景

前言

写这篇文章也是因为我一个朋友觉得学runtime没啥用处,平时开发也用不到,学了也就是为了应付面试而已,我不这么觉得,所以今天讨论一下runtime这个黑科技能搞点啥东西出来,如有错误希望大家积极指正,我们一起进步。

搞IOS的同学肯定都知道有这个东西,但又仅仅知道有这个东西的存在,oc的运行都得靠他,黑科技可远观而不可亵玩焉。说白了runtime就是oc运行的一些机制,oc是一门动态语言,它的动态特性由runtime体现出来。今天主要研究四个问题。本文的demo稍后上传。

1.给分类添加属性

2.消息转发机制

3.动态交换方法的实现

4.手动实现多继承(oc本身是不支持多继承的)

在研究runtime之前先了解一下两个经常出现在runtime里的知识点

IMP

这是一个函数指针,顾名思义它保存的是一个函数的内存地址,通过IMP系统就可以找到这个函数然后去执行它,也可以说说它指向一个方法的实现。

SEL

这个类型我们经常遇到比如[self performSelector:@selector(click) withObject:nil];这个方法里的:@selector(click)就是SEL类型。SEL是方法编号,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID,只要方法名称相同,那么它们的ID就是相同的。是对方法的一个包装,它本身并不指向方法的实现,系统在内部通过SEL找到对应的IMP,然后去执行这个方法。

Method

在objc.h中, Method 的定义如下:

/// An opaque type that represents a method in a class definition.typedefstructobjc_method*Method;structobjc_method{SEL method_name                                          OBJC2_UNAVAILABLE;char*method_types                                      OBJC2_UNAVAILABLE;    IMP method_imp                                          OBJC2_UNAVAILABLE;}

Method = SEL + IMP + method_types,相当于在SEL和IMP之间建立了一个映射。

接下来进入正题,我们都知道Category(分类)里面是不能添加属性和成员变量的,为什么?比如你在Category里申明了一个属性@property(nonatomic,copy)NSString * userName;这个时候你看.m文件会有一个警告,告诉我们要实现它的set方法,这就是原因所在,在分类里面添加属性,系统只会申明set和get方法,并不会实现他们,并且也不会生成一个带下划线的成员变量,也就是说在内部我们没办法操作这个属性。既然已经知道问题了,那就可以开始解决的问题了,我们只要能在它的内部自己实现set和get方法,然后可以操作这个属性问题不就解决了,这个时候我们只要用到objc_setAssociatedObject和objc_getAssociatedObject这两个函数就可以了。首先我们创建一个UIView的分类Test。然后在.m里实现set和get方法,代码如下。


这里面用到的知识点就是动态的关联属性

objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,

                         id _Nullable value, objc_AssociationPolicy policy)

可以看到这个函数需要传入四个函数分别是被绑定的对象,键,绑定的值和策略,这个策略有几个选项

OBJC_ASSOCIATION_ASSIGN = 0,

OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,

OBJC_ASSOCIATION_COPY_NONATOMIC = 3, 

OBJC_ASSOCIATION_RETAIN = 01401,     

OBJC_ASSOCIATION_COPY = 01403 类似于assign,retain,copy。

而这个键一般用静态字符常量,用来保存和获取所关联的值。这个函数相当于我们的set方法。同理objc_getAssociatedObject这个函数相当于我们的get方法。通过动态的关联属性,我们就可以变相的操作这个userName属性了。

接下来我们来看看消息转发是怎么玩的

首先我们定义一个类叫MessageForward,然后申明一个方法-(void)sendMessage;

//创建一个对象

MessageForward * message = [[MessageForward alloc]init];

[message sendMessage];//调用方法

接下来会发生什么呢?

编译器将代码[message sendMessage];转化为objc_msgSend(object, @selector (makeText));,在objc_msgSend函数中。首先通过message的isa指针找到message对应的class。在Class中先去cache中 通过SEL(方法编号)查找对应的method,若 cache中未找到。再去methodList中查找,若methodlist中未找到,则去superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。这是正常流程,如果经常上面一系列骚操作都没有找到这个方法的实现,那么。。。。。。。crash,game over。然后报一个无法识别方法的错误。如果不想crash,那么我们需要去实现整个消息转发流程。

第一步动 态方法解析

查找接收者所属的类,看其是否能动态添加方法,以处理这个“未知的方法”。我们需要实现如下方法

这个方法是NSObject里面,留给我们来动态的解析方法

根据sel得到对应的方法的名字,然后进行判断,如果methodNam为 sendMessage,那么调用

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,

                const char * _Nullable types)函数,添加方法,需要四个参数,第一个是谁添加到哪个类,第二个要添加方法的编号,第三个参数是一个函数指针,方法的实现,第四个参数用来描述这个方法的类型,比如返回值类型,各个参数的类型

’v‘ :void类型,第一个字符代表返回值类型

’@‘ : 一个id类型的对象,第一个参数类型

’:‘ : 对应SEL,第二个参数类型

下来及就是实现这个方法


这里面有两个隐藏参数 self和_cmd需要我们写上,如果有别的自定义参数可以跟着后面写。这个时候再运行就不会蹦了,而是输出我最帅三个字。如果没有动态添加方法那么将会进入下一步找备用。

找备用接受者(也就是备胎,专门用来接锅的,心疼一分钟)

首先我们先准备一个接锅侠,创建一个类Reserve

申明sendMessage方法,并实现它(申不申明都行)


根据aSelector得到对应的方法的名字,然后进行判断,如果methodNam为 sendMessage,那么调用 [[Reserve alloc]init];然后将这个对象返回,也就是让它去响应这个消息,这个时候我们运行,程序会走到Reserve这个类当中的sendMessage方法,这样找备胎的就完成了。备胎这个词真的很让人心痛。。。。。。。。。。

如果上面两步骚操作都失效了,那就只能用最后的大招了“消息转发”,平时玩游戏的小伙伴都知道大招和普通的技能相比除了更牛x以外还有一个令人头疼的后遗症,CD时间太长,在这里也就对应的程序的消耗更大,越往后面走消耗越大。看代码。


首先调用methodSignatureForSelector这个方法,对方法进行签名,获取到方法的类型信息,比如返回值类型,各个参数类型等。然后调用-(void)forwardInvocation:(NSInvocation *)anInvocation,将消息进行转发给Reserve的对象r,让这个r去响应这个消息,这个时候再运行,依然完美的的解决了问题。

但是。。。。。。。。。。这个连大招都失灵了会怎么样,一样crash,这就很让人尴尬,我一顿操作猛如虎,一看战绩0-5,为了避免这种尴尬的情况我们还有最后一手底牌,请看代码

-(void)doesNotRecognizeSelector:(SEL)aSelector{

    NSLog(@"消息无法处理");

};

这个方法就是最后的最后都失效了调用的,至少保证程序不会crash啊,也不枉撸了那么多代码。消息转发机制到这里就算结束了。

接下来我们就要动态交换方法的实现

首先我们创建一个UIView的分类Exchange然后如下代码


所谓交换方法实际上交换两个方法的实现,比如有a,b两个方法,将他们交换,那么调用a方法会去执行b方法的实现,反之调用b方法会去执行a方法的实现。这个很简单,我的demo里有,可以下载玩一下。

接下类就是重头戏了,多继承(oc本身是不支持多继承的)

记得在很久以前我去面试,面试官问我oc支持多继承吗,当时虽然我是一个刚毕业的菜鸟,但这点常识还是有的,立刻回答了没有(毕竟我也是背过不少面试题的,我猜测接下来他会问,那oc协议可以多继承吗,哈哈,都是套路,辛亏我早有准备),正当我胸有成竹的时候,他说现在我就想实现多继承你说怎么办。窝草,这么傲娇,你想实现就实现,苹果不要面子的啊,对于这个问题当时我觉得不可能啊,从来没听说过,所以就回答了不知道,果然这场面试就这样gg了。回去之后百度了一下,窝草,居然真的有这种骚操作,今天就和大家分享一下。

我们先聊聊大致的思路

一个类既继承A类,又继承B类,也就说这两个类方法它都可以用,有什么办法可以同时调用两个类的方法呢,答案其实上面已经说了,协议,oc是支持协议的多继承的,我们把方法的申明写在协议里,然后让这个类同时继承A类和B类的协议,这样就可以调用这两个类的方法了,调用的时候编译器不会报错,但运行时还是会crash,因为在这个类内部找不到方法的实现,方法的实现是在A类和B类里,那么怎么办,其实答案也在上面了,我们可以通过消息转发机智,将消息转发到A类和B类里,这样就解决得了找不到方法实现的问题,首先创建两个类Man和Women,定义各自的协议代码如下。


然后创建一个Children类,同时集成上面两个协议。不过这个Children类的基类不是NSObject而是NSProxy,这是一个虚类,没有构造方法,需要自己手动实现init方法。相当于轻量级的NSObject,主要实现-(nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel和-(void)forwardInvocation:(NSInvocation *)invocation两个方法进行消息转发,上面已经说了这两个方法。首先我们先写个init方法。


第一步创建一个字典然后调用-(void)registerMethodsWithTarget:(id)target方法


这个方法是干嘛的,首先获取到调用者所在类的所有实例方法,然后进行遍历,然后以调用者为值,方法名作为键,添加到_methodsMap这个字典里,后边会用的,先不说。接下来进行消息转发


首先调用-(nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel这个方法,返回方法签名。

然后调用-(void)forwardInvocation:(NSInvocation *)invocation进行消息转发,首先获取到sel,然后根据sel获取到methodName,然后真个methodName到_methodsMap这个字典去取值,得到目标target,就是我们再init方法里创建一个Man对象和一个Women对象,然后进行判断如果target响应这个方法,将消息转发给这个target,结束。

这些就是runtime的几个简单应用,还有好多好多骚操作,不过需要我们我们慢慢地去发现,runtime对于新手来说还是不太理解和操作的,用不好就是天大的bug,建议大家用的时候一定要小心一点,因为它是运行时候的机制,所以在编译阶段编译器是发现不了错误的,出bug了也很难定位,双刃剑吧,用好的吊炸天,用不好,估计就要上天台了。

推荐阅读更多精彩内容