iOS Runtime 初识与应用

1,147 阅读7分钟

什么是 Runtime

什么是运行时呢?从字面意思来看,就是一个程序在其运行的过程中所做的一些事情。而苹果在 object—C 中提供了一套纯 c 语言的 api,这套 api 即为 runtime。

在 iOS 开发的过程中,正式因为runtime 的特性,让 object-C 具有了吸引人的魅力。使得我们可以真正做到玩语言,做出高逼格的花样,快乐就完了~

要了解运行时,我们得先了解 object-C 的消息机制,可以看下面的流程

1、编译器会先将代码 [obj doSomeThing] 转化为 objc_msgSend(obj, @selector (doSomeThing)) 函数去执行。

2、在 objc_msgSend() 函数中,首先通过 obj 的 isa 指针找到 obj 对应的 class。

3、在 class 中会先去 cache 中 通过 SEL 查找对应函数 doSomeThing(cache 中method 列表是以 SEL 为 key 通过 hash 表来存储的,这样能提高函数查找速度),若 cache 中未找到。再去 class 中的消息列表 methodList 中查找,若 methodList 中未找到,则取 superClass 中查找。若能找到,则将 doSomeThing 加入到 cache中,以方便下次查找,并通过 method 中的函数指针跳转到对应的函数中去执行。

看完上面的流程,可能会迷惑里面提到的一下字端是什么意思,那我们来看一下一个 OC 类中都包含了什么?

可以先看类结构体 struct objc_class。虽然该结构体中有许多变量,但是从变量名中我们可以大概理解其含义,结构体里包含了 指像父类的指针、类名、版本等等信息,里面有些变量会在下面的应用中进行用到。

下面就让我们来看看 runtime 的具体应用吧~

Runtime 的应用

Runtime 的功能非常强大,这也是魅力所在,它能够在程序运行的时候,获取一个类中的所有信息,并且能够根据开发者意愿去修改。脑洞多大,变化就能多大~

接下来,本文主要就下面几点进行讲解:

(1)使用 class_addMethod 函数在运行时对函数进行动态增加新函数

(2)消息转发

(3)使用 class_copyPropertyList 及 property_getName 获取类的属性列表及每个属性的名称

(4) 使用 class_copyMethodList 获取类的所有方法列表

(5) Method Swizzling

动态添加方法 - class_addMethod

首先来看一下 API 文档解释

先看下函数中的各个参数的含义:

Class _Nullable cls:传入的是一个类,也就是你想要在哪个类里进行添加方法

SEL _Nollnull name:想要添加的方法名字

IMP _Nonnull imp:imp 是 Implement 缩写,表示指向方法的实现地址

const char * _Nonnull types:对于参数与返回值的描述。举个例子:"v@:",表示的是返回值为 void 并且没有参数。

下面看一下具体的调用:

这里要给 HJXPerson 类动态添加一个 sayHello 的方法。首先要拿到添加的方法 sayHello 的 IMP 指针,然后调用相应的接口去添加方法。

(注:当要执行动态添加方法的时候,需要用 performSelector 来进行调用,因为 performSelector 是运行时系统负责去找方法的,在编译时候不做任何校验;如果直接调用编译是会自动校验)

消息转发

文章开头介绍了 Objective-C 中调用一个方法的流程,但如果最后都没有找到对应方法,我们的程序都会 crash 并抛出信息没有找到对应的方法。其实在 crash 之前还会进行一套流程,那就是消息转发。转发流程图如下:

下面将分别结合代码事例进行介绍这几种方法的处理:

  • resolveInstanceMethod

此为消息转发的先导,也叫动态决议机制。见下面例子:

在 HJXPerson 类中并没有声明 eat 这个函数,所以在实例调用方法的时候会进入到这个回调之中。

其实,objective-c 的方法就是至少带有两个参数(self 和 cmd)的普通的 C 函数。因此在代码中提供这样一个 C 函数 dynamicMethodIMPEat,让它来充当对象方法 eat 这个 selector 的动态实现。

因为 eat 是被对象所调用,所以它被认为是一个对象方法,因而应该在 resolveInstanceMethod 方法中为其提供实现。

  • forwardingTargetForSelector

这个方法只能让我们把消息转发到另一个能处理这个消息的对象,但是无法处理消息的内容,比如参数和返回值。例子如下:

该类为 HJXCat 类,里面声明了 “useTool” 这个方法但并没有实现,所以在未添加 resolveInstanceMethod 处理的时候,会进入到 forwardingTargetForSelector 方法中,然后可以根据传入的 aSelector,来转交给其他类去处理相应的方法。

(注:返回的为想要把方法交给的类的一个实例对象)

  • forwardInvocation

先看下工程代码

forwardInvocation 的整体流程和 forwardingTargetForSelector 基本上是差不多的。通过 aSelector 来进行判断是否要进行转发,然后进行手动签名。然后在 anInvocation 中可以获取到函数传里过程中所有信息。

forwardingTargetForSelector 和 forwardInvocation 区别

快速转发:forwardingTargetForSelector 仅支持一个对象的返回,也就是说消息只能被转发给一个对象、无法处理消息的内容,比如参数和返回值。

普通转发:forwardInvocation 可以将消息同时转发给任意多个对象。

消息转发总结

  • 首先看是否为该 selector 提供了动态方法决议机制,如果提供了则转到 2;如果没有提供则转到 3;

  • 如果动态方法决议真正为该 selector 提供了实现,那么就调用该实现,完成消息发送流程,消息转发就不会进行了;如果没有提供,则转到 3;

  • 其次看是否为该 selector 提供了消息转发机制,如果提供了消息了则进行消息转发,此时,无论消息转发是怎样实现的,程序均不会 crash。(因为消息调用的控制权完全交给消息转发机制处理,即使消息转发并没有做任何事情,运行也不会有错误,编译器更不会有错误提示。);如果没提供消息转发机制,则转到 4;

  • 运行报错:无法识别的 selector,程序 crash

property_getName 及 class_copyMethodList

这两个函数总从字面意思上,可以看出分别是获取一个类中的所有属性名称及方法名。这两个方法可以算是基础,让你可以知道一个类内的所有属性及方法,以便你接下来可以随心所欲的对于其修改,修改哪里。

下面为调用方法来获取 HJXCat 中的属性及方法:

(上面的各个属性都是声明在 .m 文件中的)

iOS 黑魔法 - Method Swizzling

首先让我们来看下方法交换的原理:

  • 在 Objective-C 中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是 selector 的名字。利用 Objective-C 的动态特性,可以实现在运行时偷换 selector 对应的方法实现,达到给方法挂钩的目的。

  • 每个类都有一个方法列表,存放着 selector 的名字和方法实现的映射关系。IMP 有点类似函数指针,指向具体的 Method 实现。

归根结底,都是偷换了 selector 的 IMP

跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

下面我们来看一下代码部分:

就这么简简单单几行,就能够实现方法的交换。原有的 eat 方法在调用 hasEatenFull 方法后,就与 play 方法进行了交换,再次执行 eat 方法,其实就是调用了 play 方法。

虽然 Swizzling 可以任你喜欢的去弄,想要玩起来,anyway,都可以。但是开发工程中,我们需要先好好的去想一想,能不能利用良好的代码和架构设计来实现,或者是深入语言的特性来实现。好用,但是不能滥用。

后记

Runtime 在代码运行的时候,最大提供给我们操作性,可以帮助我们理解代码的运行,窥探我们看不到的代码,扩展更多更有趣的功能。

代码玩起来,快乐就完啦~用好,不滥用~

Article by 夏风_Me