RunTime底层了解

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类型的,如下图:

RunTime底层了解_第1张图片

查看arm64结构之后的isa类型,可以看到是isa_t类型的,如下图:

RunTime底层了解_第2张图片

进入isa_t查看里面存放的类型,如下图:

RunTime底层了解_第3张图片

从上图可以看出,如果是arm64的架构,就采用了优化后的isa类型了;并且是通过位域的运算方式来获取值的。至于位域的运算方式可以自行百度。其中isa占用了8个字节,每个字节由8位,所以isa占用了64位,从上图中,我们可以将1+1+33+6+1+1+1+19 算出刚好等于64位。现在,我们需要了解一下优化后的isa位域中每一位都表示了什么含义:

RunTime底层了解_第4张图片

 

从上个表中,看到如果没有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的码,如下图:

RunTime底层了解_第5张图片

方法如下:

RunTime底层了解_第6张图片

其中Class的类型定义如下:

RunTime底层了解_第7张图片

3.Class的结构

先看一张图:

RunTime底层了解_第8张图片

上图主要是从优化后的结构说的!!!

3.1 class_rw_t 和 class_ro_t的结构

从上图中,可以看出objc_class 存放了一个class_data_bits_t的结构体的字段bits,这个字段用于获取具体的类信息。这个字段通过&FAST_DATA_MASK来获取class_rw_t结构体的数据信息,源码如下:

RunTime底层了解_第9张图片

点击查看bits.data()这个方法,源码如下:

RunTime底层了解_第10张图片

点击class_rw_t,查看class_rw_t结构体存放的信息如下:

RunTime底层了解_第11张图片

点击查看class_ro_t的结构体如下:

RunTime底层了解_第12张图片

class_rw_t 里面的methods、properties和protocols 数二维数组,可读可写。点击查看class_rw_t里面的method_array_t的结构体如下:

RunTime底层了解_第13张图片

可以看出method_array_t类型的methods是二维数组,可读可写,包含了类初始化内容和分类的内容,二维数组放的数据结构如下:

RunTime底层了解_第14张图片

从上图可以看出,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等这些数据,那么这两种有什么区别呢?

先看源码:

RunTime底层了解_第15张图片

通过源码可以看出,当创建实例对象时,是先把类中的属性和方法先存放到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的结构体通过源码可查看到如下图:

RunTime底层了解_第16张图片

(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),用散列表来缓存曾经调用过的方法,可以提高方法的查找速度:

RunTime底层了解_第17张图片

这里的cache 是指类的方法缓存。

4.1 为什么要有方法缓存?

我们知道,方法的调用其实从通过isa指针找到类对象的方法列表,在类对象的方法列表中如果没有找到该方法,就通过superClass找父类的方法列表是否有这个方法,如果没有的话,继续往上找,直到根类的方法列表也没有才会结束;但是如果找到了就会直接返回。如下图:

RunTime底层了解_第18张图片

但是这样查看的方式性能略差;假设我的方法就是在根类中方法列表里,那么每次都要通过子类到父类再到父类...再到根类这样找,所以才有了方法缓存的产生。当找到方法时,把方法放到缓存里。所以当调用一个方法时,先通过isa指针找到该类,先从该类的缓存方法列表中查找,如果缓存列表中没有找到,再通过子类到父类再到父类...再到根类这样方式查找。

4.2 cache_t的结构体了解

cache_t内部结构体如下图:

RunTime底层了解_第19张图片

查看bucket_t的结构体如下:

RunTime底层了解_第20张图片

从上两图可以查看,方法缓存是通过以@selector(函数名)为key,以函数的地址为value放到散列表的数组中。

4.3散列表的了解

1.什么是散列表?

散列表就是数组,有两个元素,第一个元素数是序列号(也就是索引),第二个元素就是存放的值。

2. 散列表为什么会非常的高效?
因为散列表通过存储的值&_mask的到一个索引,这个所以就是序列表,通过序列表直接取值,这样就不用一次一次的遍历数组了。因为例如算出来的索引是4,那么4之前的索引可能都是NULL,所以,散列表(哈希表)是以空间换时间 的方法.

3. bucket_t散列表的形式大概如下:

RunTime底层了解_第21张图片

左边是索引, 右边是 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
  • 源码如下:

RunTime底层了解_第22张图片

查看cache_hash的查找方法如下:

RunTime底层了解_第23张图片RunTime底层了解_第24张图片

6. 为什么按位&_mask?

按位与 可保证得到的值 <= _mask, 这样就不会超出分配的空间.

注: 有的系统是求余 %, 如java, 这样也能保证 <= _mask

7. 为什么有 -1 的算法?

也是因为按位与, 因为不同的值 & _mask, 可能结果相同. 如果已经被占了, 就-1:

RunTime底层了解_第25张图片

8. 如果空间超出原来的_mask, 那怎么办?

刚开始为cache_t分配一定的内存, 系统默认是分配了4, 当内存不够用时, 内存扩大2倍, 依次类推则 _mask *= 2.

每一次_mask 扩容, 散列表清空,为什么要清空呢?因为_mask的值不一样了,如果还保留之前的列表的话,那么与新的_mask &出来的的值就不一样了,所以就清空了。源码如下:

RunTime底层了解_第26张图片

通过源码可以知道如果lodCapacity没有值的话,就取INIT_CACHE_SIZE:

RunTime底层了解_第27张图片

 

你可能感兴趣的:(OC学习小记)