1.Runtime的简介
Runtime是一套C语言的API,封装了很多动态性相关的函数;OC的动态性就是由Runtime来支撑和实现的。
平时编写的OC代码,底层都是转换成了Runtime API进行调用。
具体应用:
- 利用关联对象给分类添加属性
- 遍历类的所有成员属性,动态修成其属性值(例如,修改textFile的占位文字颜色、字典转模型、自动归档解挡)
- 交换方法的实现
- 利用消息转发机制解决方法找不到的异常问题
- kvo和kvc等
2.isa详解
要学习runtime,首先要了解它底层的一些常用数据结构,比如isa指针。在arm64架构之前,isa就是一个普通的指针,存储着class、Meta-Class对象的内存地址;从arm64架构开始,对isa指针进行了优化,变成了一个共用体(union)结构体,还是用位域来存储更多的信息。
(1)通过源码查看arm64架构之前的isa类型,可以看到是class类型的,如下图:
查看arm64结构之后的isa类型,可以看到是isa_t类型的,如下图:
进入isa_t查看里面存放的类型,如下图:
从上图可以看出,如果是arm64的架构,就采用了优化后的isa类型了;并且是通过位域的运算方式来获取值的。至于位域的运算方式可以自行百度。其中isa占用了8个字节,每个字节由8位,所以isa占用了64位,从上图中,我们可以将1+1+33+6+1+1+1+19 算出刚好等于64位。现在,我们需要了解一下优化后的isa位域中每一位都表示了什么含义:
从上个表中,看到如果没有has_assoc和has_cxx_dtor会释放的更快,为什么这么说呢?找到objc的析构函数(也就是销毁的函数)的源码,如下:
/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory.
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
从源码中,我们可以知道,如果有C++的函数就调用object_cxxDestruct()函数来清楚C++的函数;
如果有关联对象就调用_object_remove_assocations()函数来清除关联的对象;
如果都没有,直接调用释放的函数,所以如果没有has_assoc和has_cxx_dtor会对象会释放的更快。
(2)需要注意的一点是, 如果用从isa中取出class的地址信息,怎么取呢?
首先,我们知道优化后的isa是共用体,通过位域运算获取值的,那么如果要通过位域运算获取class的地址信息的话,需要把isa的值&一个ISA_MASK的码,如下图:
方法如下:
其中Class的类型定义如下:
3.Class的结构
先看一张图:
上图主要是从优化后的结构说的!!!
3.1 class_rw_t 和 class_ro_t的结构
从上图中,可以看出objc_class 存放了一个class_data_bits_t的结构体的字段bits,这个字段用于获取具体的类信息。这个字段通过&FAST_DATA_MASK来获取class_rw_t结构体的数据信息,源码如下:
点击查看bits.data()这个方法,源码如下:
点击class_rw_t,查看class_rw_t结构体存放的信息如下:
点击查看class_ro_t的结构体如下:
class_rw_t 里面的methods、properties和protocols 数二维数组,可读可写。点击查看class_rw_t里面的method_array_t的结构体如下:
可以看出method_array_t类型的methods是二维数组,可读可写,包含了类初始化内容和分类的内容,二维数组放的数据结构如下:
从上图可以看出,methods的数组是先存放method_list_t 类型的数组,method_list_t 存放的是method_t结构类型的数组。以此类推properties和protocols存放的数据结构。
而class_ro_t里面的baseMethodList、baseProtocols、ivars、baseProperties是一维数组,是只读的,包含了类的初始内容。
问题来了,既然class_rw_t 里有 methodlist、protocols这些数据,class_ro_t 里面也有baseMethodsList等这些数据,那么这两种有什么区别呢?
先看源码:
通过源码可以看出,当创建实例对象时,是先把类中的属性和方法先存放到class_ro_t里面的,当有分类时,才会把class_ro_t里面的数据复制一份放到class_rw_t里面的methods里面去。
3.2 method_t的了解
从3.class_rw_t和class_ro_t中的源码查看,需要知道的是:method_t是对方法\函数的封装,method_t的结构体通过源码可查看到如下图:
(1)SEL代表方法(函数名),一般叫做选择器,底层结构跟char *类似。
- 可以通过@selector()或者sel_registerName()获得;
- 可以通过sel_getName()和NSStringFromSeletor()转成字符串
- 不同类中相同名字的方法,所对应的方法选择器是相同的
(2)IMP代表了函数的具体实现
typedef id -Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ....)
(3)types 类型编码(types EnCoding):包含了返回值类型和参数的类型
为了便于理解上面的types中的类型编码,请前往这里了解一下类型编码。
4.cache_t的结构体
了解完class_rw_t 的结构体后,就要看看cacht_t的结构体了。Class内部结构中有个方法缓存(cache_t),用散列表来缓存曾经调用过的方法,可以提高方法的查找速度:
这里的cache 是指类的方法缓存。
4.1 为什么要有方法缓存?
我们知道,方法的调用其实从通过isa指针找到类对象的方法列表,在类对象的方法列表中如果没有找到该方法,就通过superClass找父类的方法列表是否有这个方法,如果没有的话,继续往上找,直到根类的方法列表也没有才会结束;但是如果找到了就会直接返回。如下图:
但是这样查看的方式性能略差;假设我的方法就是在根类中方法列表里,那么每次都要通过子类到父类再到父类...再到根类这样找,所以才有了方法缓存的产生。当找到方法时,把方法放到缓存里。所以当调用一个方法时,先通过isa指针找到该类,先从该类的缓存方法列表中查找,如果缓存列表中没有找到,再通过子类到父类再到父类...再到根类这样方式查找。
4.2 cache_t的结构体了解
cache_t内部结构体如下图:
查看bucket_t的结构体如下:
从上两图可以查看,方法缓存是通过以@selector(函数名)为key,以函数的地址为value放到散列表的数组中。
4.3散列表的了解
1.什么是散列表?
散列表就是数组,有两个元素,第一个元素数是序列号(也就是索引),第二个元素就是存放的值。
2. 散列表为什么会非常的高效?
因为散列表通过存储的值&_mask的到一个索引,这个所以就是序列表,通过序列表直接取值,这样就不用一次一次的遍历数组了。因为例如算出来的索引是4,那么4之前的索引可能都是NULL,所以,散列表(哈希表)是以空间换时间 的方法.
3. bucket_t散列表的形式大概如下:
左边是索引, 右边是 bucket_t
结构体
如上图所示, bucket_t
包括 _key
与 IMP, _key
就是SEL
4.iOS arm64
散列表存储原理:
- 初始时, 为对象的
cach_t
分配一个空间, 值为NULL
- 调用方法时, 为对象发送一个
SEL
消息, 如@selector(personTest)
, 将这个方法缓存 - 系统用
SEL
与_mask
作按位与计算:@selector(personTest) & _mask
, 假设其值==2, - 检查索引2 对应的空间是否为
NULL
, 如果为NULL
就将这个bucket_t
缓存在索引2对应空间 - 果不为空, 索引减1, 再检查是否为
NULL
, 依次类推. 如果索引<0, 则使索引 =_mask
- 1, 直至找到索引对应空间为NULL
, 再缓存
5. 对应的查找步骤:
- 调用方法时, 为对象发送一个
SEL
消息, 如@selector(personTest)
- 系统用
SEL
与_mask
作按位与计算:@selector(personTest) & _mask
, 假设其值==2, - 得到索引2 的
bucket_t
, 判断其中的SEL
是否与传过来的SEL
相同, 如果相同, 这个_imp
就是寻找的方法 - 如果不相同, 索引减1, 再比较
SEL
, 依次类推. 如果索引<0, 则使索引 =_mask
- 1, 直至找到_imp
- 源码如下:
查看cache_hash的查找方法如下:
6. 为什么按位&_mask
?
按位与 可保证得到的值 <= _mask
, 这样就不会超出分配的空间.
注: 有的系统是求余 %, 如java, 这样也能保证 <= _mask
7. 为什么有 -1 的算法?
也是因为按位与, 因为不同的值 & _mask
, 可能结果相同. 如果已经被占了, 就-1:
8. 如果空间超出原来的_mask
, 那怎么办?
刚开始为cache_t
分配一定的内存, 系统默认是分配了4, 当内存不够用时, 内存扩大2倍, 依次类推则 _mask *= 2
.
每一次_mask
扩容, 散列表清空,为什么要清空呢?因为_mask的值不一样了,如果还保留之前的列表的话,那么与新的_mask &出来的的值就不一样了,所以就清空了。源码如下:
通过源码可以知道如果lodCapacity没有值的话,就取INIT_CACHE_SIZE: