Runtime之初见
人生若只如初见,何事秋风悲画扇。
依旧是网上很多runtime的资料,依旧是看不懂,,,这里给大家转化一下runtime,使它由隐晦难懂变得通俗易懂。
(虽然截图和语言组织的有些凌乱,但是大家还是一点一点的阅读下去吧,可以新建一个工程,跟着我写的一步一步的自己走一遍,会有帮助的。)
关于runtime理论性的东西可以参考我们同事hah的初识runtime(我俩的文章名字居然差不多)
本文参照:教你快速上手Runtime。谢谢该文作者峥吖峥老师。有兴趣的也可以去峥老师的博客看看去。
另外哪里写的不对,可以给我留言,我会及时改进的,谢谢大家。
在这里我们对runtime进行如下的介绍:
1、runtime的简单介绍
2、runtime的消息发送
3、用runtime交换方法
4、动态添加方法
5、动态添加属性
6、runtime替换KVC,进行字典转换

下边开始我们的通俗易懂的路程:
1、配置环境:新建一个工程,在appdelegate里面将viewcontroller设置为rootcontroller,方便查看打印信息。

创建一个person类,继承自NSObject,下图的PersonViewController无用。
然后再看一下目前的工程文件结构:

按照网上教程我们开始runtime的各个方法的实现。
2、发送消息。举例:实例方法和类方法的调用原理。
我们建立一个继承于NSObject的Person

在.m中分别实现实例方法和类方法:

然后在viewcontroller加入头文件:#import "Person.h"、#import <objc/message.h>
方法的调用有两种,调用实例方法和调用类方法。我们用runtime去观察这两种方法的实质。
1、viewDidLoad里面实现Person的实例化,并用这个实例调用Person里面的实例方法;
2、直接用Preson类名或类对象调用类方法。

此时我们可以看到

但是按照网上教程上图蓝色框框里面的也应该可以放开注释的,可是不知道为什么我的放开注释以后会爆红:

不知道是为什么(如果大家谁知道怎么回事,还烦请留言告诉我,再次谢谢大家了),,,所以这里的runtime并不友好。
哎,终于知道怎么回事了,还是同事hah帮忙解决的:头文件导入错了,应该是导入:#import <objc/runtime.h>

导入#import<objc/messge.h>也可以调用objc_msgSend()这个方法,但是不能传参,而导入#import<objc/runtime.h>,会出现这个方法:objc_msgSend(id, SEL, ...),是可以传参数的.
在这里有的网友提到另外一种解决该问题的方案:谢谢北京-吴露。方案是:引入头文件<objc/objc-runtime>,command+点击进入这个方法,我们会看到下边的

也就是说这个头文件把runtime和message都包含了,不过建议还是单独引入,因为这两个头文件里面的方法很多都类似,容易混淆,区别在于方法是否可以传参数。就像上边我们举例的问题。
打印结果:

其实这里咱们可以看到网上教程的注释:不管是实例方法还是类方法,其本质就是让对象或者类对象发送消息,即我们在OC中调用的实例方法和类方法在runtime运行时的时候实际上是转化成C语言的发送消息的语句。
按照网上教程说的:消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现

其实说的就是:找到对应的实例方法或者类方法。
最后,这里的runtime的使用并不实用。
3、交换方法。

其实写完下边的代码,在通过理解,你会发现这中所谓的“交换方法”实际上就是系统方法的重写、扩展、再替换回去的过程。
首先我们看一下demo里面viewcontroller的结构(同样的personviewcontroller无用,忽略掉,这里的<objc/message.h>最好是换成#import<objc/runtime.h>):

然后我们看viewDidLoad里面,在这里我们没有做什么操作,只是命名了一个image:这里我们的图片是在工程文件中的,所以走到替换方法里面的时候会判断不为空,所以对应的打印信息“加载空的图片”就不会打印出来。

按照网上教程我们进行步骤一的操作,写一个名字叫做+ (instancetype)imageWithName:(NSString*)name的类方法。这个方法是为了替换的时候准备的。

然后我们进行步骤二,这个步骤二很关键,就是替换的核心所在。

注意:两个方法。前面一个是准备好的方法,后边一个是系统的方法,要用第一个方法换掉第二个方法,这里需要注意替换顺序。
当我们将viewDidLoad里面的image换成一个空的图片的时候

我们看到打印信息

是走了图片为空的if判断里面的。
所以,这个方法可以看成是网络请求一张图片,如果请求失败,可以用默认的图片进行展示,就像下边的方法,网络请求失败的时候会展示一张默认图。


当然,这里不是说runtime这么高大上的方法仅仅能实现这么low的功能,仅做示例而已。
其实这个方法替换可以用在线下替换线上代码里面,“黑魔法”,我还不会,只是听说过。下边继续学习runtime其他的方法。
4、动态添加方法。

动态添加方法,咱们的person类.h里面用到了分类:

在这里拓展一下类扩展的知识:类扩展与分类的区别。谢谢作者:Mitchell孟晨
在这里大家是不是会想到我们的面试题中:

有的还是用英语问的:

类的作用。参考自category解析。如果大家对类别特别感兴趣可以参考深入解析OC中的Category

当我们在类中添加@interface的时候:

会出现上图中的3个。
第一个就是类:继承自谁。

第二个是分类,就是分类。


类扩展与分类的区别中有举例说明:虽然我们在分类中声明属性不会报错,但是@property并没有自动为我们设置的属性生成set、get方法。
第三个就是extension。类扩展。

再看上述中有一句话是这样说的:

类扩展的作用:

person类.m中:

viewcontroller里面:

这里没有实现eat4方法,所以会在viewcontroller中的viewdidload里面调用eat4的时候,会调用这个方法处理,并且把对应的方法列表传过来。用来判断为实现的方法是不是我们想要动态添加的方法。
这里注意的是,峥老师教程里面在调用class_addMethod(self,@selector(eat4), (IMP)eatt,"v@:");这个方法的时候,第三个参数前面并没有添加“(IMP)”,不添加这个IMP,会报黄:

说的什么不清楚,意思就是叫你添加上IMP。
拓展一下performSelector调用和直接调用的区别:

准备好了,我们看打印结果:

这里我们可以看到NSStringFromSelector(sel)调用的就是eat4。
扯了这么多怎么用,到底这个动态添加方法到底是用来干什么的呢,看看众多网友的理解。翻了好多,就下边的两个解释比较靠谱,大家凑合着看吧:

当然还有最一开始咱们引入动态添加的时候的目的:

不忘初心,方得始终。
5、动态添加属性。
想知道原理的请移步OC Associated Objects实现原理
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
看结构,添加了一个NSObject的分类:

分类的.h文件,将set方法和name外漏,方便在viewcontroller里面调用。

分类的.m文件,set方法和name的实现。

最后看viewcontroller里面,引入分类的头文件,然后实例化NSObject,调用objc1的name。

最后的打印结果:

tips:其实呢这个动态添加属性,是在哪里用到的呢?具体的我也说不上来。其实按照我的理解,咱们这里所谓的动态添加属性,是在分类上边添加的属性,那么我就想了,为什么我不能在类自己本身上边添加属性呢?对吧?为什么非要在分类上边添加呢,还整了个动态添加属性,麻烦不?!原因有两个:
1)如果你的类自己本身里面的逻辑或者代码比较多,而需求是新加一个功能,那么就可以在分类上边添加,因为分类和继承还是有区别的。咱们这样理解啊:如果你用的是继承,那么在子类a里面新添加的属性和方法,在父类以及其他继承父类的子类b里面是不能用新添加的属性和方法的,(有点绕)。但是分类就行,你在分类里面新添加了属性和方法,那么在其他用到这个类自己的地方,是可以用新添加的属性和方法的。好像和公有性、私有性有点关系。
2)给系统的类添加属性。像UIView、UIImageView、UILabel、UIButton等等系统的类,你觉得系统的类里面的属性不够用(当然,像我这种小白还是觉得够用的,关键不知道各个类里面除了经常用的还有啥,,,)就可以自己添加属性,当然了,在MJ和SDWebimage里面其实也能看到动态添加属性的影子,只不过是我们大多数人没有进入人家里面去看具体的代码。同事hah在runtime初识里面也提到了这个:

6、字典转模型
峥老师说的“字典转模型的方式一:KVC”这个就是咱们平时JSON解析的时候经常用到的,这里不作进一步解释,看看峥老师说的用runtime字典转模型的方式。

没搞明白。。。
先用KVC吧。
总的来说,即便咱们知道了runtime是怎么用的,但是没有合适的环境使用runtime,也是白搭。所以,咱们现在能做的就是先了解runtime,随时记在心中,以后哪里能够用到的,可以尝试着用runtime的特性和上述几个方法来实现,甚至于用runtime解决bug。
另外这里有很多关于runtime的集合,大家可以参考:runtime专题