[toc]
## 前言
`Runtime`是近年来面试遇到的一个高频方向,也是我们平时开发中或多或少接触的一个领域,那么什么是runtime呢?它又可以用来做什么呢?
**什么是Runtime?平时项目中有用过么?**
- OC是一门`动态性`比较强的编程语言,允许很多操作`推迟`到程序`运行时`再进行
- OC的动态性就是由`Runtime`来支撑和实现的,Runtime是一套`C语言的API`,封装了很多动 态性相关的函数
- 平时编写的OC代码,底层都是转换成了`Runtime API`进行调用
**具体应用**
- 利用关联对象(`AssociatedObject`)给分类添加属性
- 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
- 交换方法实现(交换系统的方法)
- 利用消息转发机制解决方法找不到的异常问题
## 详解isa
我们在研究对象的本质的时候提到过`isa`,当时说的是isa是个`指针`,`存储的`是个类对象或者元类对象的`地址`。
- 实例对象的isa指向类对象
- 类对象的isa指向元类对象
确实,在arm64架构(真机环境)前,isa单纯的就是一个指针,里面存储着类对象或者元类对象地址,但是arm64架构后,系统对isa指针进行了优化,我们在源码中可以探其结构:
![](https://img.kancloud.cn/aa/d2/aad2d7e8d522b5c7ddb198f086892df9_618x158.png)
可以看到,isa是个`isa_t`类型的数据,我们在点进去看一下isa_t是什么数据:
![](https://img.kancloud.cn/fe/0a/fe0a64c58c5485c30b457ef0c43ef95b_716x284.png)
`isa_t`是个`union结构`,里面包含了一个结构体,结构体里面是个宏`ISA_BITFIELD`,我们看看这个宏是什么?
![](https://img.kancloud.cn/84/60/8460f52297f37527eeae69f3b6435c54_1252x1288.png)
也就是这个结构体里面包含很多东西,但是究竟是什么东西要根据系统来确定。
那么在`arm64`架构下,isa指针的真实结构是:
![](https://img.kancloud.cn/ab/19/ab19e14d10b0c568459814e78566d0b4_1354x554.png)
在我们具体分析isa内部各个参数分别代表什么之前,我们需要弄清楚这个`union`是什么呢?我们看着这个union和结构体的结构很像,这两者的区别如下↓↓
- union:共用体,顾名思义,就是多个成员共用一块内存。在编译时会选取成员中长度最长的来声明。`共用体内存=MAX(各变量)`
- struct:结构体,每个成员都是独立的一块内存。 `结构的内存=sizeof(各变量之和)+内存对齐`
也就是说,`union`共用体内所有的变量,都用`同一块内存`,而`struct结构体`内的变量是各个变量有各个变量`自己的内存`,举例说明:
![](https://img.kancloud.cn/9b/ea/9beadfeb38fbf9311334df9eab490dc7_1210x1258.png)
我们分别定义了一个共用体test1和一个结构体test2,里面都各自有八个char变量,打印出来各自占用内存我们发现共用体只占用了1个内存,而结构体占用了8个内存.
其实结构体占用8个内存很好理解,8个char变量,每个char占用一个,所以是8;而union共用体为什么只占用一个呢?这是因为他们共享同一个内存存储东西,他们的内存结构是这样的:
![](https://img.kancloud.cn/aa/ff/aaffe287fa3f1e997346db614e59defd_1276x904.png)
我们看到te就一个内存空间,也就是所有的公用体成员公用一个空间,并且同一时间只能存储其中一个成员变量的值,这一点我们可以打断点或打印进行确认:
![](https://img.kancloud.cn/f2/8c/f28c0bb05499ad4c3e41b190c2ae381d_1488x572.png)
我们发现,第一次打印的时候,bdf这些值都是1的打印出来都是0,这是因为当te.g = '0',执行完后,这个内存存储的是g的值0,所以访问的时候打印结果都是0。第二次打印同理,te.h执行完内存中存储的是1,再访问这块内存那么得到的结果都会是1。所以我们从这也可以看出
> union共用体就是系统分配一个内存供里面的成员共同使用,某一时间只能存储其中某一个变量的值,这样做相比结构体而言可以很大程度的节省内存空间。
既然我们已经知道`isa_t`使用共用体的原因是为了最大限度节省内存空间,那么各个成员后面的数字代表什么呢?这就涉及到了`位域`.
我们看到`union共用体`为了节省空间是不断的进行`值覆盖`操作,也就是新值覆盖旧值,结合位域的话可以更大限度的节约内存空间还不用覆盖旧值。我们都知道**一个字节是8个bit位**,所以位域的作用就是`将字节这个内存单位缩小为bit位来存储东西`。我们把上面这个union共用体加上位域:
![](https://img.kancloud.cn/3e/50/3e508d3ec97d24c13b6017439cef4a61_870x414.png)
上面这段代码的意思就是,abcdefgh这八个char变量不再是不停地覆盖旧值操作了,而是将`一个字节分成8个bit位`,每个变量一个bit位,按照顺序`从右到左`一次排列。
我们都知道char变量占用一个字节,一个字节有8个bit位,也就是char变量有8位,那么te和te2的内存结构如下所示:
![](https://img.kancloud.cn/f5/6c/f56ca905f1ccd95808a89f7e6bb5adbf_1276x904.png)
这个结构我们也可以通过打印来验证:te占用一个字节位置,内存地址对应的值是`0xaa`,转换成二进制正好是`10101010`,也就是`a~h`存储的值。
![](https://img.kancloud.cn/cc/15/cc15a4d7ff330e597dc709c62e2e8613_1726x906.png)
我们可以看到,现在是将一个字节中的8个bit位分别让给8个char变量存储数据,所以这些char变量存储的数据不是0就是1,可以看出来这种方式非常省内存空间,将一个字节分成8个bit位存储东西,物尽其用。
> 所以我们根据isa_t结构体中的所占用bit位加起来=64可以得知isa指针占用8个字节空间。
虽然`位域`极大限度的节省了内存空间,但是现在面临着一个问题,那就是**如何给这些变量赋值或者取值呢**?
普通结构体中因为每个变量都有自己的内存地址,所以直接根据地址读取值即可, 但是`union共用体`中是大家`共用同一个内存地址,只是分布在不同的bit位上`,所以是没有办法通过内存地址读取值的,那么这就用到了`位运算符`,我们需要知道以下几个概念:
- &:按位与,同真为真,其余为假
- |:按位或,有真则真,全假则假
- <<:左移,表示左移动一位 (默认是00000001 那么1<<1 则变成了00000010 1<<2就是00000100)
- ~:按位取反
- 掩码 : 一般把用来进行按位与(&)运算来取出相应的值的值称之为掩码(Mask)。如 #define TallMask 0b00000100 :TallMask就是用来取出右边第三个bit位数据的掩码
好,那么我们来看下这些运算符是怎么可以做到取值赋值的呢?比如说我们上面的te共用体内有8个char,要是我们想出去char b的值怎么取呢?这就用到了&:
![](https://img.kancloud.cn/b0/f0/b0f0a41de1ccd2b8b951f45b90b55369_564x180.png)
按位与`&`上 `1<<1 `就可以取出b位的值了,b是1那么结果就是1,b是0那么结果就是0;
同理,当我们为f设置值的时候,也是类似的操作,就是在改变f的值的同时不影响其他值,这里我们要看赋的值是0还是1,`不同值操作不同`:
![](https://img.kancloud.cn/b1/f1/b1f1ac172d953f7cb36d60eb45aeb9eb_1126x522.png)
所以,这就是共同体中取值赋值的操作流程,那么我们接下来回到isa指针这个结构体中,看一下它里面的各个成员以及怎么取赋值的↓↓
```
/*nonpointer
0,代表普通的指针,存储着Class、Meta-Class对象的内存地址
1,代表优化过,使用位域存储更多的信息
*/
uintptr_t nonpointer : 1; \
/*has_assoc:是否有设置过关联对象,如果没有,释放时会更快*/
uintptr_t has_assoc : 1; \
/*是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快*/
uintptr_t has_cxx_dtor : 1; \
/*存储着Class、Meta-Class对象的内存地址信息*/
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
/*用于在调试时分辨对象是否未完成初始化*/
uintptr_t magic : 6; \
/*是否有被弱引用指向过,如果没有,释放时会更快*/
uintptr_t weakly_referenced : 1; \
/*对象是否正在释放*/
uintptr_t deallocating : 1; \
/*里面存储的值是引用计数器减1*/
uintptr_t has_sidetable_rc : 1; \
/*
引用计数器是否过大无法存储在isa中
如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
*/
uintptr_t extra_rc : 19;
```
我们看到,isa指针确实做了很大的优化,同样是占用8个字节,优化后的共用体**不仅存放这类对象或元类对象地址,还存放了很多额外属性**,接下来我们对这个结构进行验证:需要注意的是因为是arm64架构 所以这个验证需要是ios项目且**需要运行在真机上** 这样才会得出准确的结果
首先,我们来验证这个`shiftcls`是否就是类对象内存地址。
![](https://img.kancloud.cn/ce/b7/ceb79e0c0726ad250ce708fef1f288c2_1188x504.png)
我们定义了一个dog对象,我们打印它的`isa`是`0x000001a102a48de1`
从上面的分析我们得知,要取出`shiftcls`的值需要`isa的值&ISA_MASK`(这个isa_mask在源码中有定义),得出`$1 = 0x000001a102a48de0`
而$1的地址值正是我们上面打印出来`Dog类对象的地址值`,所以这也验证了isa_t的结构。
![](https://img.kancloud.cn/fc/60/fc60bcf1293c70c8641cb88502f5960b_1048x332.png)
我们还可以来看一下其他一些成员,比如说**是否被弱指针指向过**?我们先将上面没有被__weak指向过的数据保存一下,其中红色框中的就是这个属性,`0表示没有被指向过`
![](https://img.kancloud.cn/59/3b/593bdc5080aeab929d15819acb7126c7_782x350.png)
然后我们修改代码,添加弱指针指向dog:
```
__weak Dog *weaKDog = dog;
```
> 注意:只要设置过关联对象或者弱引用引用过对象,has_assoc或weakly_referenced的值就会变成1,不论之后是否将关联对象置为nil或断开弱引用。
![](https://img.kancloud.cn/10/b2/10b27ba52d88fdfe17324d12fc439e9f_810x342.png)
发现确实由0变成了1,所以可以验证isa_t的结构,这个实验要确保程序运行在真机才能出现这个结果。所以arm64后确实对isa指针做了优化处理,不在单纯的存放类对象或者元类对象的内存地址,而是除此之外存储了更多内容。
## class的具体结构
我们之前在讲分类的时候讲到了类的大体结构,如下图所示:
![](https://img.kancloud.cn/36/61/366104521eaf060ac176ac4c5281c77c_2372x1086.png)
就如我们之前讲到的,当我们调用方法的时候是从`bits`中的`methods`中查找方法。
**分类的方法是排在主类方法前面的**,所以调用同名方法是先调用分类的,而且究竟调用哪个分类的方法要取决于`编译的先后顺序`等等:
![](https://img.kancloud.cn/47/f9/47f9d1bde644b43c0c0ac7104da50c48_1794x1140.png)
### rw_t 和ro_t
那么这个`rw_t`中的`methods`和`ro_t`中的`methods`有什么不一样呢?
- 首先,`ro_t中methods,是只包含原始类的方法,不包括分类的`,而rw_t中的methods即包含原始类的也包含分类的;
- 其次,`ro_t中的methods只能读取不能修改`,而rw_t中的methods既可以读取也可以修改,所以我们今后在动态添加方法修改方法的时候是在rw_t中的methods去操作的;
- 然后,ro_t中的methods是个一维数组,里面存放着method_t(对方法/函数的封装,即一个method_t代表一个方法或函数),而rw_t中的methods是个二维数组,里面存放着各个分类和原始类的数组,分类和原始类的数组中存放着method_t。即:
![](https://img.kancloud.cn/28/6a/286ad2c4ab06060f4767c7aab94e9c4e_1916x578.png)
我们也可以在源码中找到rw_t (rw:read and write 读写)和ro_t (ro:read only 只读)的关系:
```
static Class realizeClass(Class cls)
{
runtimeLock.assertLocked();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
// 最开始cls->data是指向ro的
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// rw已经初始化并且分配内存空间
rw = cls->data(); // cls->data指向rw
ro = cls->data()->ro; // cls->data()->ro指向ro 即rw中的ro指向ro
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// 如果rw并不存在,则为rw分配空间
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);// 分配空间
rw->ro = ro;// rw->ro重新指向ro
rw->flags = RW_REALIZED|RW_REALIZING;
// 将rw传入setData函数,等于cls->data()重新指向rw
cls->setData(rw);
}
}
```
首先,`cls->data(即bits)`是指向存储类初始化信息的`ro_t`的,然后在运行时的`运行过程中创建了class_rw_t`,等rw_t分配好内存空间后,开始将`cls->data指向了rw_t`并将rw_t中的ro指向了存储初始化信息的ro_t。
那么ro_t和rw_t中存储的这个`method_t`是个什么结构呢?我们阅读源码发现结构如下,我们发现有三个成员:name、types、imp,我们一一来看:
### method_t
![](https://img.kancloud.cn/01/68/0168fe03f65ba2432a2861227240b153_814x172.png)
- name,表示`方法的名称`,一般叫做选择器,可以通过`@selector()`和`sel_registerName()`获得。
> /*
比如test方法,它的SEL就是@selector(test);或者sel_registerName("test");需要注意的一点就是不同类中的同名方法,它们的方法选择器是相同的,比如A、B两个类中都有test方法,那么这两个test方法的名称都是@selector(test);或者sel_registerName("test");
*/
- types,表示`方法的编码`,即`返回值、参数的类型`,通过字符串拼接的方式将返回值和参数拼接成一个字符串,来代表函数返回值及参数。
> /*
比如ViewDidload方法,我们都知道它的返回值是void,参数转为底层语言后是self和_cmd,即一个id类型和一个方法选择器,那么encode后就是v16@0:8(它所表示的意思是:返回值是void类型,参数一共占用16个字节,第一个参数是@类型,内存空间从0开始,第二个参数是:类型,内存空间从8开始),当然这里的数字可以不写,简写成V@:
*/
关于更多encode规则,可以查看下面这个表:
![](https://img.kancloud.cn/78/1c/781cacc7b61cc9ea7a1251c742ad6b5a_2046x884.png)
当然除了自己手写外,iOS提供了`@encode`的指令,可以将具体的类型转化成字符串编码。
```
NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));
// 打印内容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :
```
- imp,表示指向`函数的指针(函数地址)`,即方法的具体实现,我们调用的方法实际上最后都是通过这个imp去进行最终操作的。
## 方法缓存
我们在分析清楚方法列表和方法的结构后,我们再来看一下方法的调用是怎么一个流程呢?是直接去方法列表里面遍历查找对应的方法吗?
其实不然,我们在分析类的结构的时候,除了`bits`(指向类的具体信息,包括rw_t、ro_t等等一些内容)外,还有一个方法缓存:`cache`,用来缓存曾经调用过的方法
![](https://img.kancloud.cn/d1/d4/d1d430efd8221b33d8efe4c64c28801e_718x238.png)
所以系统查找对应方法**不是通过遍历rw_t这个二维数组来寻找方法的**,这样做太慢,效率太低。
真正的做法是:
> 系统是先从方法缓存中找有没有对应的方法,有的话就直接调用缓存里的方法,根据imp去调用方法,没有的话,就再去方法数组中遍历查找,找到后调用并保存到方法缓存里
流程如下:
![](https://img.kancloud.cn/ea/c7/eac7e004aba998ee6022c316e12fe815_1062x512.png)
那么方法是怎么缓存到`cache`中的呢?系统又是怎么查找缓存中的方法的呢?我们通过源码来看一下cache的结构:
### cache_t
![](https://img.kancloud.cn/3e/42/3e420c3c8694eb7f0f58e95fe09e8d89_794x202.png)
>`散列表(Hash table,也叫哈希表)`,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
我们可以看到,`cache_t`里面就三个成员,后两个代表`长度和数量`,是`int类型`,肯定不是存储方法的地方,所以方法应该是存储在`_buckets`这个散列表中。散列存储的是一个个的`bucket_t的结构体`,那么这个bucket_t又是个什么结构呢?
### bucket_t
![](https://img.kancloud.cn/89/61/8961f7580b4c27966823a901c2b209c6_1104x424.png)
所以`cache_t`底部结构是这样的:
![](https://img.kancloud.cn/d4/fe/d4feed7e8a5633ac713c46f2bb91dff3_1330x858.png)
我们看到,`bucket_t`就两个值,**一个key一个imp**,**key的话就是方法名,也就是SEL,而imp就是Value**,也就是当我们调用一个方法是来到方法缓存中查找,通过比对方法名是不是一致,一致的话就返回对应的imp,也就是`方法地址`,从而可以调用方法,那么这个散列表是怎么查找的呢?难道也是通过遍历吗?
### 方法查找
我们通过阅读源码来一探究竟:
![](https://img.kancloud.cn/01/5b/015bcc57d1d1814d8a7c139574a7defe_1410x1240.png)
--------------------------------------------------------
![](https://img.kancloud.cn/fa/a7/faa78858ce011e07c96572fc99d62f6e_1180x538.png)
通过上面代码的阅读,我们可以知道系统在`cache_t`中查找方法**并不是通过遍历**,而是通过方法名`SEL&mask得到一个索引`,直接去读数组索引中的方法,如果该方法的SEL与我们调用的方法名SEL一致,那么就返回这个方法,否则会判断当前架构,在x86或者i386架构中是向下寻找,在arm64架构中是向上寻找,直到找完为止。
好,既然取值的时候不是遍历,而是直接读的索引,那么讲方法存储到缓存中也肯定是通过这种方式了,直接方法名&mask拿到索引,然后将_key和_imp存储到对应的索引上,这一点我们通过源码也可以确认:
![](https://img.kancloud.cn/a9/42/a942f34124688e2852c52e1a72371db1_2478x1430.png)
我们看到无论是存还是读,都是调用了find函数,查看`SEL&mask`对应的索引的方法,不合适的话再向下寻找直到找到合适的位置。
那么这里有两个疑问,**为什么SEL&mask会出现不是该方法名(读)或者不为空(写)的情况呢?散列表扩容后方法还在吗?**
> 首先,SEL&mask这个问题,是因为不同的方法名&mask可能出现同一个结果,比如test方法的SEL是011,run方法的SEL是010,mask是010,那么无论是test的SEL&mask还是run的SEL&mask 记过都是010,如果大家都存在这个索引里面是会出问题的,所以为了解决这个索引重复的问题需要先做判断,即拿到索引后先判断这个索引对应的值是不是你想要的,是的话你拿走用,不是的话向下继续找,方法缓存也是同样的道理。我们先调用test方法,缓存到010索引,再调用run方法,发现010位置不为空了,那就判断010下面的索引是否为空,为空的话就将run方法缓存到这个位置。
关于散列表扩容后,缓存方法在不在的问题,通过源码就可以知道,旧散列表已经释放掉了,所以是不存在的,再次调用的时候就得重新去rw_t中遍历找方法然后重新缓存到散列表中,比如下面这个例子:
![](https://img.kancloud.cn/39/bd/39bd134db1184d5c852bc7ae688fc7f5_1662x232.png)
到现在我们清楚了,那就是散列表中并不是按照索引依次排序或者遍历索引依次读取,那么就会出现个问题,因为SEL&mask是个小于mask的随机值且散列表存储空间超过3/4的时候就要扩容,那就会导致散列表中有一部分空间始终被限制。确实,**散列表当分配内存后,每个地方最初都是null的,当某个位置的索引被用到时,对应的位置才会存储方法**,其余位置仍处于空闲状态,但是这样做可以极大提高查找速度(比遍历快很多),所以这是一种`空间换时间`的方式。
![](https://img.kancloud.cn/c9/24/c9240d97af3a6bd38e1b4a9c7989a275_889x448.png)
## 方法的传递过程
我们现在已经清楚方法的调用顺序了,**首先从缓存中找没有的话再去rw_t中找,那么在没有的话就去其父类中找**,父类中查找也是如此,先去父类中的cache中查找,没有的话再去父类的rw_t中找,以此类推。如果查找到基类还没有呢?难道就直接报`unrecognized selector sent to instance `这个经典错误吗?
其实不是,方法的传递主要涉及到三个部分,这也是我们平时用得最多以及面试中经常出现的问题:
我们都知道,当我们调用一个方法是,其实底层是将这个方法转换成了`objc_msgSend`函数来进行调用,`objc_msgSend`的执行流程可以分为3大阶段:
> 消息发送->动态方法解析->消息转发
这个流程我们是可以从源码中得到确认,以下是源码:
```
/***********************************************************************
*_class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher
* already tried that.
**********************************************************************/
MP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
//这个函数是方法调用流程的函数 即消息发送->动态方法解析->消息转发
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//先从当前类对象的方法缓存中查看有没有对应方法
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
//没有的话再从类对象的方法列表中寻找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
//遍历所有父类 知道其父类为空
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
//先查找父类的方法缓存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
//再查找父类的方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
//消息发送阶段没找到imp 尝试进行一次动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
//跳转到retry入口 retry入口就在上面,也就是x消息发送过程即找缓存找rw_t
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
//消息发送阶段没找到imp而且执行动态方法解析也没有帮助 那么就执行方法转发
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
```
### 消息发送
首先,消息发送,就是我们刚才提到的系统会先去`cache_t`中查找,有的话调用,没有的话去类对象的`rw_t`中查找,有的话调用并缓存到`cache_t`中,没有的话根据`supperclass`指针去父类中查找。父类查找也是如此,先去父类的cache_t中查找,有的话进行调用并添加到自己的cache_t中而不是父类的cache_t中,没有的话再去父类的rw_t中查找,有的话调用并缓存到自己的cache_t中,没有的话以此类推。流程如下:
![](https://img.kancloud.cn/b3/59/b359a89fc5d8e7c3685e3882fda98fcc_2014x960.png)
当消息发送找到最后一个父类还没有找到对应的方法时,就会来到`动态方法解析`。**动态解析,就是意味着开发者可以在这里动态的往rw_t中添加方法实现**,这样的话系统再次遍历rw_t就会找到对应的方法进行调用了。
### 动态方法解析
动态方法解析的流程示意图如下:
![](https://img.kancloud.cn/53/79/5379a21351832692051d4ce47a32d98d_1080x776.png)
主要涉及到了两个方法:
```
+resolveInstanceMethod://添加对象方法 也就是-开头的方法
+resolveClassMethod://添加类方法 也就是+开头的方法
```
我们在实际项目中进行验证:
![](https://img.kancloud.cn/33/7f/337fcbbbc30819ef847c783fa193a73d_1930x1320.png)
动态添加类方法也是如此,只不过是添加到元类对象中(此时run方法已经改成了个类方法)
![](https://img.kancloud.cn/dc/eb/dceb6b4dbaa2ca2b06ece1c5572b550b_1678x948.png)
而且我们也发现,动态添加方法的话其实无非就是找到方法实现,添加到类对象或元类对象中,至于这个方法实现是什么形式都没有关系,比如说我们再给对象方法添加方法实现时,这个实现方法可以是个类方法,同样给类方法动态添加方法实现时也可以是对象方法。**也就是说系统根本没有区分类方法和对象方法,只要把imp添加到元类对象的rw_t中就是类方法,添加到类对象中就是对象方法。**
![](https://img.kancloud.cn/1a/13/1a135f2146c706fa08c78ad7818deefe_1712x718.png)
### 消息转发
当我们在消息发送和动态消息解析阶段都没有找到对应的imp的时候,系统回来到最后一个`消息转发`阶段。所谓消息转发,就是你这个消息处理不了后可以找其他人或者其他方法来代替,消息转发的流程示意图如下:
![](https://img.kancloud.cn/1c/02/1c025d76c5141e485aeff7ce599dfe88_1872x676.png)
> 即分为两步:
> 第一步是看能不能找其他人代你处理这方法,可以的话直接调用这个人的这个方法, 这一步不行的话就来到第二步
> 这个方法没有的话有没有可以替代的方法,有的话就执行替代方法。我们通过代码来验证:
我们调用dog的run方法是,因为dog本身没有实现这个方法,所以不能处理。正好cat实现了这个方法,所以我们就将这个方法转发给cat处理:
![](https://img.kancloud.cn/6d/f0/6df0cd42a29728e5041aa7a96bb84f7b_1514x612.png)
我们发现,确实调用了小猫run方法,但是只转发方法执行者太局限了,要求接收方法对象必须实现了同样的方法才行,否则还是无法处理,所以实用性不强。这时候,我们可以通过`methodSignatureForSelector`来进行更大限度的转发。
需要注意的是要想来到`methodSignatureForSelector`这一步需要将`forwardingTargetForSelector返回nil(即默认状态)`否则系统找到目标执行者后就不会再往下转发了。
开发者可以在`forwardInvocation:`方法中自定义任何逻辑。
```
////为方法重新转发一个目标执行
//- (id)forwardingTargetForSelector:(SEL)aSelector{
// if (aSelector == @selector(run)) {
// //dog的run方法没有实现 所以我们将此方法转发到cat对象上去实现 也就是相当于将[dog run]转换成[cat run]
// return [[Cat alloc] init];
// }
// return [super forwardingTargetForSelector:aSelector];
//}
//方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
//注意:这里返回的是我们要转发的方法的签名 比如我们现在是转发run方法 那就是返回的就是run方法的签名
//1.可以使用methodSignatureForSelector:方法从实例中请求实例方法签名,或者从类中请求类方法签名。
//2.也可以使用instanceMethodSignatureForSelector:方法从一个类中获取实例方法签名
//这里使用self的话会进入死循环 所以不可以使用 如果其他方法中有同名方法可以将self换成其他类
// return [self methodSignatureForSelector:aSelector];
// return [NSMethodSignature instanceMethodSignatureForSelector:aSelector];
//3.直接输入字符串
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//当返回方法签名后 就会转发到这个方法 所以我们可以在这里做想要实现的功能 可操作空间很大
//这个anInvocation里面有转发方法的信息,比如方法调用者/SEL/types/参数等等信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//这样写不安全 可以导致cat被过早释放掉引发怀内存访问
// anInvocation.target = [[Cat alloc] init];
Cat *ca = [[Cat alloc] init];
//指定target
anInvocation.target = ca;
//对anInvocation做出修改后要执行invoke方法保存修改
[anInvocation invoke];
//或者干脆一行代码搞定
[anInvocation invokeWithTarget:[[Cat alloc] init]];
//上面这段代码相当于- (id)forwardingTargetForSelector:(SEL)aSelector{}中的操作
//当然 转发到这里的话可操作性更大 也可以什么都不写 相当于转发到的这个方法是个空方法 也不会报方法找不到的错误
//也可以在这里将报错信息提交给后台统计 比如说某个方法找不到提交给后台 方便线上错误收集
//...很多用处
}
```
当然我们也可以访问修改anInvocation的参数,比如现在run有个age参数,
```
// 参数顺序:receiver、selector、other arguments
int age;
//索引为2的参数已经放到了&age的内存中,我们可以通过age来访问
[anInvocation getArgument:&age atIndex:2];
NSLog(@"%d", age + 10);
```
我们发现,消息转发有两种情况,一种是`forwardingTargetForSelector`,一种是`methodSignatureForSelector+forwardInvocation:`
> 其实,第一种也称`快速转发`,特点就是简单方便,缺点就是能做的事情有限,只能转发消息调用者;第二种也称标准转发,缺点就是写起来麻烦点,需要写方法签名等信息,但是好处就是可以很大成都的自定义方法的转发,可以在找不到方法imp的时候做任何逻辑。
当然,我们上面的例子都是通过对象方法来演示消息转发的,类方法同样存在消息转发,只不过对应的方法都是类方法,也就是`-变+`
![](https://img.kancloud.cn/61/4b/614bd08de40900ecb05ac9b4a163ad3c_1256x520.png)
所以,以上关于消息传递过程可以用下面这个流程图进一步总结:
![](https://img.kancloud.cn/5c/ae/5caef6207e8d16cbd0f09955e1aff976_1037x263.png)
关于源码阅读指南:
![](https://img.kancloud.cn/99/c0/99c0a89cb8e9dd906a20cd8050a083ab_2052x1000.png)
## super的相关内容
首先我们来看一下这段代码:
![](https://img.kancloud.cn/44/4c/444cf2a2f17d0e6b27dfe8682c1cc6de_900x1064.png)
我们发现最终的打印结果和我们预期的不一样,按我们的思路Super就是指的的Dog的父类Animal,Animal调用class方法应该返回Animal 但是结果却不是这样,这是为什么?首先我们先将这段代码转换成c++底层代码来一探究竟:
```
static instancetype _I_Dog_init(Dog * self, SEL _cmd) {
self = ((Dog *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("init"));
if (self) {
// NSLog(@"%@",[self class]);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
//NSLog(@"%@",[self superclass]);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_1,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));
//NSLog(@"%@",[super class]);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_2,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("class")));
//NSLog(@"%@",[super superclass]);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_3,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("superclass")));
}
return self;
}
```
将上述代码简化后得到下面的结果:
![](https://img.kancloud.cn/9d/a9/9da9e8ea7d375a2875177a7d08f87fe4_1898x196.png)
我们发现,当self调用class方法时,是执行的`objc_msdSend(self,@selector(class))`函数,消息的接收者是当前所在类的实例对象(Dog) , 这个时候就会去self所在类 Dog去查找class方法 , 如果当前类Dog没有class方法会向其父类Animal类找 class 方法, 如果Animal类也没有找到class方法,最终会找到最顶级父类NSObject的class方法, 最终找到NSObject的class方法 ,并调用了object_getClass(self) ,由于消息接收者是 self 当前类实例对象, 所以最终 [self class]输出Dog(class方法是返回方法调用者的类型,superclass方法是返回方法调用者的父类)
[self superclass] 也是同理,找到superclass方法,然后返回调用者的父类,即Animal;
但是当我们调用super的class方法时,底层不是转换成`objc_msdSend`而是变成了`objc_msgSendSuper`函数。这个函数有两个参数,第一个参数是个结构体,结构体中有两个成员:方法调用者和调用者的父类,第二个参数就是方法名,也就是class方法的SEL。
```
[super class] ->
objc_msgSendSuper(
//第一个参数:结构体
{self,//方法调用者
class_getSuperclass(objc_getClass("Dog"))//当前类的父类
},
//第二个参数:方法名
sel_registerName("class")));
```
所以,我们看到`[self class]`和`[super class]`,他们转换成的底层实现都不一致。
objc_msgSendSuper函数的作用是告诉方法调用者去其父类中查找该方法,也就是相比objc_msdSend函数而言少了去自己类中查找方法这一步,而是直接去父类中找class方法,但是方法调用者还是没变,都是Dog。class方法和superclass它们都是返回方法调用者的类型或父类,所以`[super class]`和`[super superclass]`还是返回的Dog的类型和父类,所以打印结果是Dog和Animal,与[self class]和[self superclass]结果一致。
> 所以,总结起来就是,super方法底层会转换为objc_msgSendSuper函数的调用,这个函数的作用是告诉方法调用者去父类中查找方法。
## runtime的常见API与应用案例
```
动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)
注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)
销毁一个类
void objc_disposeClassPair(Class cls)
获取isa指向的Class
Class object_getClass(id obj)
设置isa指向的Class
Class object_setClass(id obj, Class cls)
判断一个OC对象是否为Class
BOOL object_isClass(id obj)
判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)
获取父类
Class class_getSuperclass(Class cls)
获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)
拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)
动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)
获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)
获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)
拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)
获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)
获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)
方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)
拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)
动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)
选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)
用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)
```
- 前言
- WebRTC知识集
- iOS 集成WebRTC各知识点小集
- iOS WebRTC集成时遇到的问题总结
- WebRTC多人音视频聊天架构及实战
- iOS端 使用WebRTC实现1对1音视频实时通话
- iOS 基于WebRTC的点对点音视频通信 总结篇
- WebRTC Native 源码导读 - iOS 相机采集实现分析
- OC 底层原理
- OC runtime 运行时详解
- GCD dispatch_queue_create 创建队列
- iOS底层 Runtime深入理解
- iOS底层 RunLoop深入理解
- iOS底层 Block的本质与使用
- iOS内存泄漏
- iOS中isKindOfClass和isMemberOfClass
- 从预编译的角度理解Swift与Objective-C及混编机制
- 移动支付集成
- iOS 微信支付集成及二次封装
- iOS 支付宝支付 Alipay集成及二次封装
- iOS Paypal 贝宝支付集成及二次封装
- iOS 微信、支付宝、银联、Paypal 支付组件封装
- iOS 微信、支付宝、银联支付组件的进一步设计
- iOS 组件化
- iOS 组件化实施过程
- iOS 组件化的二进制化
- 使用pod package打包framework 实现组件的二进制化
- iOS 自制Framework 获取指定bundle并读取里面的资源
- .podSpec文件相关知识整理
- 开发并上传静态库到CocoaPods
- pod引用第三方库的几种方式
- 如何在.podspec 文件中添加对本地库的依赖
- lipo 命令合并真机与模拟器生成的framework
- iOS多线程
- NSOperation相关知识点
- 自定义NSOperation
- ios多个网络请求之间的并行与串行场景的处理
- iOS动画
- ios animation 动画学习总结
- CABasicAnimation使用总结
- UITableView cell呈现的动效整理
- CoreAnimation动画使用详解
- iOS音视频开发
- iOS 音视频开发之AVCaptureMetadataOutput
- iOS操作本地视频 - 获取,压缩,取第一帧
- 使用 GPUImage 实现一个简单相机
- 直播App架构及思维导图
- 如何快速的开发一个完整的iOS直播app
- iOS视频拖动预览及裁剪
- iOS 直播流程概述
- iOS直播:评论框与粒子系统点赞动画
- iOS音视频开发 - 采集
- 基于AVFoundation实现视频录制的两种方式
- Swift知识集
- Swift 的枚举、结构体和类详解
- Swift 泛型详解
- Swift属性的包装器@PropertyWrapper
- SwiftHub项目 之网络层封装的一点见解
- Moya+RxSwift+HandyJson 实现网络请求及模型转换
- Swift开发小记(含面试题)
- RxSwift 入坑手册 - 基础概念
- 理解 Swift 中的元类型:.Type 与 .self
- Swift HandyJSON库中的类型相互转换的实现
- Swift 中使用嵌套结构体定义一组相关的常量
- Swift Type-Erased(类型擦除)
- Swift中的weak和unowned关键字
- Swift 中的错误处理
- Swift中的Result 类型的简单介绍
- Swift Combine 入门导读
- Swift CustomStringConvertible 协议的使用
- 跨平台
- Cordova跨平台方案 iOS工程创建的步骤
- 使用Cordova 打包WebApp为原生应用详解 (加壳封装)
- RAC响应式编程
- 快速上手ReactiveCocoa之基础篇
- RAC ReactiveCocoa 使用小集
- 优雅的 RACCommand
- 三方库集成及使用
- 融云IM iOS sdk 集成 一篇就够了
- iOS YYTextView使用笔记
- iOS YYLabel使用笔记
- iOS 苹果集成登录及苹果图标的制作要求
- iOS 面向切面编程 Aspects 库的使用
- VKMsgSend库对oc runtime的封装
- OC Protocol协议分发器
- iOS 高德地图实现大头针展示,分级大头针,自定制大头针,在地图上画线,线和点共存,路线规划(驾车路线规划),路线导航,等一些常见的使用场景
- 工作总结
- 自定义UINavigationBar 适配iOS11, iOS15的问题
- SFSafariViewController 加载的网页与原生oc之间的交互
- UICollectionView 设置header的二种方法
- UIPanGestureRecognizer进行视图滑动并处理手势冲突
- OC与Swift混编 注意事项
- UICollectionView 设置水平滑动后,调整每个Item项的排列方式
- oc 下定义字符串枚举
- 高性能iOS应用开发中文版读书笔记
- iOS 图集滑动到最后时添加“显示更多”效果的view组件 实现
- CocoaPods 重装
- WKWebview使用二三事
- IOS电商首页如何布局
- iOS中的投屏方案
- CGAffineTransform 介绍
- 用Block实现链式编程
- iOS 本地化简明指南
- iOS 检查及获取相机、麦克风、相册、位置等权限
- iOS 手势UIGestureRecognizer详解
- ios 编译时报 Could not build module xxx 的解决方法尝试
- iOS 常见编译报错及解决方案汇总(持续更新)
- AVMakeRectWithAspectRatioInsideRect 的使用
- graphhopper-ios 编译过程详解
- 算法
- iOS实现LRU缓存
- 架构
- IOS项目架构
- 其他杂项
- 推荐一个好用的Mac精品软件下载站
- 如何能成为一位合格的职业经理人
- 零基础怎么学习视频剪辑?这篇初剪辑学者指南你一定不要错过
- 免费SSL证书的制作
- 《一部手机拍全景》汇总课
- Linux下JAVA常用命令大全
- 即时通讯
- 通讯协议与即时通讯杂谈
- 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
- 基于实践:一套百万消息量小规模IM系统技术要点总结
- PaddleOCR 文字识别深度学习
- PaddleOCR mac 安装指南
- PaddleOCR 标注工具PPOCRLabel的使用
- PaddleOCR 更换模型
- PaddleOCR 自制模型训练