网上其实有很多关于讲述runtime的学习资料,也是站在巨人肩上看的更远。写这篇文章也是对于其他人借鉴和翻阅源码,向源码寻求解释吧。
无意间看到一份关于runtime的博客,感觉写的挺不错的就耐心的看下。看后发现自己对runtime的理解还是比较浅,就决定对自己博客进行重新修改。
随便在这里抛出一个问题:假如我们程序员自己是系统我们怎么执行程序呢?当然换个高级点的说法,如果我们是系统设计者我们想要
runtime怎么运行?
在介绍runtime之前我现在这里给大家梳理一下我们在Objc类中可能见到的内容:
- 成员变量、成员属性:
- 数据成员:
- 协议实现:
runtime简介
Objc被我们成为动态语言,换句话说就是把我们平时看到一些关键方法由编译连接推迟到运行时执行。Objc底层是基于C/C++的编译语言,在C/C++项目运行过程中链接、编译生成可执行文件。而Objc语言是在执行过程中进行编译源码,生成我们熟悉的C/C++后面最终生成机器可以识别的汇编语言。
Objc中有三大自己特色:
- 动态类型
- 动态绑定
- 动态加载
获runtime的源码点击这里
这里我们主要探究runtime.h和message.h两个文件夹中一些使用方法。
下面列出runtime.h文件中代码:
1 | typedef struct objc_method *Method; //代表类定义当中的使用方法 |
其中一种实现方式
其中我们就以类中struct objc_ivar_list *ivars ,为例来看是怎样实现
1 | struct objc_ivar_list { |
可以看出实例变量和属性是存放时一个struct的list的数组中,其中包括list的数目、所占空间和基本储存objc_ivar。
上面objc_ivar在runtime.h中形式:typedef struct objc_ivar Ivar;
objc_ivar中实例变量储存方式:
1 | struct objc_ivar { |
从上面可以看出类的实例变量和属性经过runtime编译后是以struct的储存形式存在,并且单个实例变量保存其名字、类型、偏移量和储存空间。
类中所有实例变量是以list类型进行储存。
通过对
runtime.h文件源码分析我们可以看出,在IOS程序运行过程中把我们类编译为C/C++形式。且编译过程是一个动态过程,所以我们可以通过此方式对我们程序进行动态操作
runtime动态操作
在动态编译过程中无论是属性、实例变量、成员函数、类方法的均可以操作,一下就以比较简单的进行讲解
- 获取类的对象名
- 获取类中成实例变量和属性
- 动态添加方法
获取类的对象名
1 | unsigned int count = 0; |
打印出的内容可能出乎你的意料!
获取类的实例变量和属性
1 | Class clazz = NSClassFromString(@"Graduater"); |
定义一个毕业生类,然后就可以打印出其中的函数实例变量和属性(不多记得要引入<objc/runtime.h>的头文件);
动态的添加方法
1 |
|
然后在函数调用时执行下列:
1 | Graduater *graduate = [[Graduater alloc] init]; |
执行的结果可以条用我们添加的@select(abc)的方法。
runtime使用API的介绍:
1 | const char * class_getName(Class cls) //获取class的名字 |
除去上面我们列举其中极少数的调用实例,如果想了解更多点击这里。
在使用
runtime过程,对于操作对象实行方法改善
- 对于对象进行相关操作一般以
objc_开头 - 对于类的对象进行操作一般以
class_开头 - 对于使用方法对象操作一般以
method_开头 - 对于使用申明属性
(property)一般是以property_开头 - 对于使用类成员变量一般使用
ivar_开头
消息传送机制
在iOS的一个类中我们调用方法使用也是比较多,如
1 | [self method]; |
但是在runtime编译过程中具体实现的方式是什么呢?
首先我们看先message.h中的源码
1 | /* Basic Messaging Primitives |
从上面的解释我们可以的出如果返回类型为float调用objc_msgSend_fpret,如果是数据结构返回使用objc_msgSend_stret。而我们讲解时使用 objc_msgSend(id self, SEL op, ...),就像我们上面的例子就会转化为objc_msgSend(self @select(method))。
但是找到我们所使用的方法具体是怎么实现的?
在此地方我就将添加上面看到的关于objc_msgSend,objc_msgSendSuper,SEL
数据类型:
(1)SEL:
SEL又叫做选择器,表示一个@selector的指针,映射方法的名字。在Objc编译的过程中通过每个方法的名字和参数是生成的唯一的标示(此标示是int的类型),我们把这个标示称作为SEL。SEl的作用是作为IMP(关于IMP的具体说明会在下面展示)的key存在,存储在NSSet的中,这样可以方便我们使用hash的快速查找。SEL使我们查找IMP(指向实现函数的指针)key,如果SEL可以相同的话我们调用函数就会发生错误。
所以SEL是不可以重复的,方法也是不可以重复的。这也就是我们在Objc开发过程中在同一个类中不可以使用同名的函数~~~此处和C++支持重载有很大区别。
1 | typedef struct objc_selector *SEL; |
虽说在runtime.h中没有显示objc_selector结构体类型,但是作为程序员的我们可以找到方法进行验证,使用log打印SEL试下?
(2)IMP:
IMP上面讲到IMP把SEL作为key值,IMP说白啦就是指向我们方法的指针,也就是我们条用具体函数方法的入口。
1 | typedefine id (*IMP)(id, SEL, ...) |
(3)Method
Method:上面我们在类的Struct的结构中可以看出Method是放置我们使用的方法,也是我们使用SEL和IMP的一个具体的绑定。通过使用SEL来找到对应的IMP进而实现我们方法的具体调用。
下面看代码:
1 | typedef struct objc_method *Method; |
下面我们看下在类中方法具体实现的形式:
1 | struct objc_method_list { |
(4)方法缓存
我们在调用方法的过程其实是Lazy调用的过程,方法在第一次调用加载后会放到缓存池中。Objc程序启动后,需要进行类的初始化、调用方法时的cache初始化,再发送消息的时候就直接走缓存。
下面我会对方法缓存做详细的解释。
1 | struct objc_cache { |
消息转发机制具体实现
在动态添加方法过程中,我们使用如下方式:
1)动态解析方法
1 | +(BOOL)resolveInstanceMethod:(SEL)sel; |
具体实例如下:
1 | +(BOOL)resolveInstanceMethod:(SEL)sel{ |
从这里我们就可以看出在函数调用过程中runtime会根据SEl识别子进行寻找,在例子中我们有相应的识别可以查询,如果没有相应的识别子呢?
在系统中如果在查找过程中没有找到相应的选择子,就会调用上面的函数
+(BOOL)resolveInstanceMethod:(SEL)sel;
生成实例方法,如上面例子(添加相应的实例方法可以实现,就跳转方法实现)。把添加方法放入到缓存中,可以供我们进行下次的调用
如果上面我们找到相应的识别子,就会进行第二次对选择子进行相关的处理。
2)备援接收到
1 | -(id)forwardingTargetForSelector:(SEL)aSelector; |
系统会把选择子作为参数,返回给我们一个选择子对象。运行的系统就会对相关方法实例进行查找,如果找到相关方法就实行,找不到就结束转发。
3)完整的消息传递
1 | -(void)forwardInvocation:(NSInvocation *)anInvocation; |
如果上面2)步骤中返回依然是nil,也会执行完整的消息传输消息机制。在该方法中我们可以改变其目标,然后运行系统就会根据改变的目标,在其目标中查询实现方法别调用。当然也可以进行修改选择子。
如果方法调用失败
1 | -(void)doesNotRecognizeSelector:(SEL)aSelector; |
通过对于[self method];方法的实现过程的进行探究我们我们可以了解到在消息传递过程是如何实现的,而且我们可以更具我们需求对其中的方法进行相应的而修改。
runtime是Objc语言特性。
小编通过对于的研究理解是:
1)通过runtime我们可以等到我们类中所有元素的资料信息,可以使用期同工的API对类进行我们想到达到的目的;
2)在我们调试过程中也可以通过打印来简化我们调试信息;
3)最棒的就是我们可以了解程序运行过程可以在开发中使我们更加得心应手。