引言:笔者在接触runtime之前,认为runtime是一个可用可不用的,因为在我们的实际开发中毕竟用的少。但是通过对runtime的相关学习和整体之后,就觉得在代码中用的确实不是很多,但是runtime能在关键的时候或者用OC(Swift)解决问题棘手的时候,帮助我们化险为夷。之前笔者也是在了解和学习runtime之前,不停的在心里问:runtime究竟能在哪里用到?就举个简单的例子:我们在设置button的点击事件的时候,一般是这样设置的:
但是这样,我们的Button声明和方法实现已经分开了,那我们可不可以设置成这样呢:
要是想实现以上的方法,就需要用到runtime了,现在就把自己学习runtime的心得分享给大家,如果有错误的地方,欢迎大家指出。
一、runtime(运行时)的简介
众所周知,Objective-C是一门动态语言,它的函数调用(消息调用)在运行时才会执行。简单的来说,runtime是属于C语言的API,Objective-C的幕后工作者。点击这里可以看到runtime的开源代码以及点击这里有对Objective-C Runtime的介绍文档。
二、runtime的消息机制
其实Objective-C中的方法调用,实质上就是发送消息。例如我们可以将一个.m文件转化成.cpp文件。
上述是我们在开发中调用的普通方法,但是我们把.m文件转化成.cpp文件会看到什么呢?(转化方法是使用clang重写命令:clang -rewrite-objc Students.m.假如在重写命令的过程中,出现“./ViewController.h:9:9: fatal error: 'UIKit/UIKit.h' file not found”类似这样的错误,可以参考该大神的文章Objective-C编译成C++代码报错)。
红箭头指向的就是消息发送函数,在Xcode中看到的objc_msgSend是这样:
下面就先介绍下objc_msgSend中出现的id和SEL吧:
id:(还记得我们一开始生成的.cpp文件吗,对,点击打开文件就可以看到这些信息了)
我们在结构体中看到了objc_object,那么它又是什么呢,我们可以点击这里,看到objc_object的真身,也是结构体!
我们可以看到objc_object这个结构体中,包含了一个private的isa指针,它的类型为isa_t。根据isa可以找到对象所对应的类,另外Objective-C的引用技术原理也跟这个isa_t相关,以后再研究。
SEL:在Objective-C中的selector在objc中是以SEL表示的。SEL是一个方法选择器,个人理解它就是实现哪个方法的标识,通过Objective-C中的@selector方法或者runtime中的sel_registerName(const char *str)都可以获得一个SEL的方法选择器,当然更直接一点,SEL sel = NSSelectorFromString(@"1111");也可以。
三、runtime发送消息的流程
Class:另外,我们在objc_object的结构体中,看到了Class ISA();,那么Class又是什么呢?
在Xcode中,我们可以看到结构体objc_class的真身:
我在网上看大神们的介绍,objc_class是继承于objc_object,但是在源码中我只看到截图所示的介绍。从objc_class结构体中,我们可以看到它包含了isa(指向自身类的指针),super_class指向父类的指针,name(类名),ivars(成员变量),methodLists(方法列表),cache(方法缓存),protocols(协议)。
Cache:主要是为了方法的缓存。一般来说,当方法调用的时候(消息发送),不会直接在isa所指向的类中查找能够响应的消息的方法,而是到cache中去查找。假如在cache中没有找到能够能够响应的消息的方法,那么它才会去methodLists(方法列表)中查找,并且runtime会把已经调用过得方法缓存到cache中,方便下次调用,也是为了优化性能。更多的关于缓存的实现细节,可以戳这里看去,反正我已经看的眼花缭乱了。
runtime发送消息的流程:就以[self StudetsRun];这个方法为例。首先,Xcode会将[self StudetsRun];方法转化为runtime中的objc_msgSend,并将方法调用者self和方法名字StudetsRun作为参数,以消息的方式传输出去。objc_class中的isa指针,会找到对象所对应的类。在类中,首先去cache中,通过SEL来查找相对应的Method,如果有的话,消息就发送成功了。假如没有,就去isa指向的类中的methodLists(方法列表)中查找,并且runtime会把已经调用过得方法缓存到cache中。如果isa指向的类中没有该方法,就去该类的父类中去查找,一直找到根类NSObject中。根类中也没有的话,要么就报类似这样的crash信息:unrecognized selector sent to instance 0x7fd0a141afd0。说明没有找到该方法,或者我们采用消息的转发,可以避免crash。
四、runtime动态添加属性
我们知道,分类是没有办法添加成员变量的,但是可以用runtime动态添加属性,还可以达到偷天换日的效果,也为我们解决问题提供了新的思路和更好的办法。
1.objc_setAssociateObject:主要是为属性动态添加setter方法,第一个参数是id类型的,即你想要在哪个属性上添加set方法,就需要传入哪个属性。第二个参数key,属性上面的值和键是要一一对应,键的类型不限(int,char都可以),但是一般会用char,因为它节省内存。第三个参数就是要添加在属性上的值了,注意这个属性的值必须是id类型的(那int,BOOL类型的怎么办呢,稍后再说),最后一个policy是描述值的类型,系统给我们枚举了一下几种:
2.objc_getAssociateObject:该方法主要是为了动态生成getter方法,第一个属性是你要从哪个属性上面取值,第二个属性是传入一个键。
3.objc_removeAssociateObjects:从哪个属性中移除。
4.举个例子:就为NSObject添加分类,并添加一个属性name。
在.h文件中声明一个name属性,方便点语法的调用。
在.m文件中我们需要实现getter,setter方法:
5.动态添加基础数据类型属性(int,BOOL等):
在.h中的属性声明:
.m文件实现getter,setter方法:
6.动态添加属性的应用:
关于runtime动态添加的属性的应用,我举的例子是为button动态添加一个block,让button的点击事件在block中运行,关于Demo,请点击我的Demo下载吧!
五、Category为什么不能添加成员变量
我们都知道Category(分类)是不能添加成员变量的,那其中的原因又是什么呢?
在runtime中category是指向结构体objc_category的指针,其中的原因,我们还需要在这个objc_category结构体中下手。
而我们的类在创建的时候要用到objc_class属性,我们就拿结构体objc_class和objc_category做下对比:
通过两个结构体的对比我们就会发现:相比于objc_class,objc_category缺少isa、super_class、ivars(成员列表)、cache等属性。在分类中,我们可以用@property来声明属性,同时也声明了setter和getter方法。但是因为objc_category中缺少ivars(成员列表属性),所以我们无法生成成员变量(想深入了解@property的小伙伴请点击iOS @property探究(二): 深入理解)。但是objc_category中有instace_method和class_method两个属性,所以分类是可以添加方法的。
六、block的实现原理
block在我们的开发中也是比较常用的,那么它的实现原理是什么呢?
首先看下我们平时写的普通block代码:
当我们用clang重写命令重写该部分的时候,发现会转化成如下形式:
我们的block的底层实现其实也是runtime,在.cpp文件中,我们可以看到__block_impl,那么它又是什么呢?
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
block的实现就暂时给大家提到这里,更多细节请参考大神的文章。
七、runtime拦截系统方法:
runtime拦截系统的方法要用到三个方法:
例如,苹果出的iPhoneX,有些全屏图片(例如App的引导页)需要UI切两套图,那么我们可以用拦截系统的[UIImage imageNamed]方法,实现两套图片的互切。
点击这里,可以下载Demo。
八、runtime动态添加方法
Objective-C是一门动态语言,基本上我们用到的控件、 数组、数据等都是以懒加载的形式加载的。在程序运行的时候才回去添加方法,动态添加的方法的作用就是去处理未实现的实例方法或者是类方法。只要我们调用了一个不存在的方法时,它就会动态添加方法。
动态添加属性需要用到class_addMethod函数,在该函数中需要我们传入cls(类名)、name(方法)、imp(implementation,实现新方法的函数,它至少要传入两个参数:self和_cmd)、types(类型)。不懂得请进官方文档传送门。
动态添加方法一般应用于:当我们想用一个类拥有一种新的方法,但是不知道类的内部实现的时候,我们可以添加分类,动态添加方法的功能实现。例如我给一个Game类动态添加方法。
我们在函数调用:
但是我们在应用的过程中会发现报了一个警告:PerformSelector may cause a leak because its selector is unknown。
消除警告的办法,将performSelector方法换成这种写法:((void (*)(id, SEL, NSString *))[game methodForSelector:sel])(game, sel, @"11212");
关于这种问题地详细原因以及解决办法,请参照大神博客。我的动态添加方法的Demo地址,请看点击这里下载吧。
runtime这部分就先分享到这里,有关后续的归档解档,字典转模型,Method Swizzling,消息的转发,KVO的底层,Objective-C的引用计数实现等等后续再更新,如果笔者有理解或者书写错误的地方,欢迎指出,蟹蟹!!
非常感谢网上各位大神的文章: