11fb24fd1c79a6e2410a766e18b53d5c
iOS 高级之美(八)—— 类结构分析中篇

一、前提条件

知晓对象,类对象,元类(只要知道概念就好)
知晓类是struct(同样有概念就好)

本文所使用的runtime版本为756.2

二、类的结构

我们平时使用的对象都是 id 类型定义的,都是objc_object这样的结构体,而后我们可以把id强转成其他任意的 class类型,是因为class本质是objc_class 结构体。而 objc_class 是继承 objc_object 的。也就是里式替换原则

里氏替换原则(Liskov Substitution Principle LSP) 面向对象设计的基本原则之一。 里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

2.1、objc_object

那么objc_object 里都放了些什么呢?objc_object结构体内部可以分为五个部分,如图。

可以看出objc_object结构体主要提供了管理自身对象的一些接口,其中比较重要的就是isa_t结构了。

2.2、isa_t

下面来看一下 isa_t的结构,其本质为联合体。以64位为🌰

union isa_t {
//isa 的两种初始化方法
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
//两种互斥类型的isa。
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

//64位CPU架构 bits 的定义如下
uintptr_t nonpointer        : 1;     //是否是指针类型
uintptr_t has_assoc         : 1;     //是否有关联对象
uintptr_t has_cxx_dtor      : 1;     //是否有C++相关的析构方法
uintptr_t shiftcls          : 44;    //class 的地址
uintptr_t magic             : 6;     //系统架构标识
uintptr_t weakly_referenced : 1;     //是否有弱引用
uintptr_t deallocating      : 1;     //是否正在释放
uintptr_t has_sidetable_rc  : 1;     //是否使用 SideTable 管理引用计数。当extra_rc 放不下时(2^8)使用
uintptr_t extra_rc          : 8      //引用计数
  • 联合体在公用内存的同时,其内部的成员也是内存互斥的,也就是说,如果 cls 存在,则 bits 不存在,反之相同。
  • 因此 isa_t 有两种表现形式。一种为 classcls 指针,另种为 bits 。其区别如下图


关于何时使用 指针 形式的 isa_t ,可以通过两个方面控制

  • 通过 Environment Variables 设置变量 OBJC_DISABLE_NONPOINTER_ISAYESNO
  • 通过 classflag 设置。(下文会说明)

2.3、 实验

下面我们通过 lldb 来证明下上面的结果。首先我们设置 OBJC_DISABLE_NONPOINTER_ISANO


我们先来获取 person 对象的 class。分成如下几个步骤

  • 1. 获取实例的 isa
  • 2. 获取 isa 的二进制形式
  • 3. 根据 bits 的定义和 isa 的二进制,我们要取其中的44位(3~47)的值。

实际上在定义 bits 的地方同样定义了一些宏,称为 mask ,我们可以通过 & 来获取相应的值。如图。

接下来我们把 OBJC_DISABLE_NONPOINTER_ISAYES ,在进行测试 - 通过 LLDB 分析情况

2.4、其他

在最新的系统下( iOS13mac10.15 下)对 isa 直接使用了指针类型,并未启用 指针优化,即使设置了 OBJC_DISABLE_NONPOINTER_ISA 也无效。

2.5、 objc_class

objc_class 作为 objc_object 的子类,实现了把各个类(NSObject)串联(继承)到了一起,并为 runtime 提供了基石。

通过 objc_object 基类提供的接口我们发现,其实对象(实例)是没有能力来“调用”方法的,因为在 objc_object 的定义中并没有方法相关的结构。是 objc_class 赋予了对象调用方法的能力。也是由 objc_classruntime 使得 Objective-C 具有了面向对象的能力
objc_object 结构体内部可以分为四个部分,如图。

其中主要的结构为 cache_tclass_data_bits_t

三、cache_t

cache_t 是一种用来快速查找执行函数的一种机制。我们知道 Objective-C 为了实现其动态性,将函数地址的调用,包装成了 SEL 寻找 IMP 的过程 ,随之带来的负面影响就是降低了方法调用的效率,为了解决这一问题。 Objc 采用了方法缓存的机制来提高调用效率。

cache_t 的特点如下。

  • 用于快速查找执行函数

  • 是可增量扩容的哈希表结构

  • 局部性原理的应用

    cache_t 结构体的定义如下。

struct cache_t {
    struct bucket_t *_buckets; //缓存数组
    mask_t _mask;              //当前数组的需要扩容的临界值
    mask_t _occupied;          //数组的中被使用的容量
    ...
};

3.1、初步验证

下面我们来借助 lldb 还原下上面的场景。

我们将上面的代码执行 step over

通过上面的例子我们能验证缓存确实存在 bucket_t 中,还有 occupied 代码缓存已使用的数量,但是,我们也发现并不是在数组的第一位按顺序存储的,而且数组的大小及存储规则还不清楚,下面我们结合源码和 lldb 来验证。

  • 1. 缓存如何命中。
  • 2. buckets如何创建。
  • 3. buckets如何管理缓存。

3.2、cache_fill_nolock

cache_fill_nolock 是缓存体系提供给外部使用的api,主要服务于 msgSend

通过调用方法,来触发 cache_fill_nolock ,此函数的作用为,查找,并填充缓存。

```c
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();

// Never cache before +initialize is done
if (!cls->isInitialized()) return;

// Make sure the entry wasn't added to the cache by some other thread 
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;

cache_t *cache = getCache(cls);
//SEL强转为 uintptr_t 类型的 无符号整形数字。
cache_key_t key = getKey(sel);

// Use the cache as-is if it is less than 3/4 full
//在已经使用数量的基础上+1,获得将要更新缓存之后的已使用数量
mask_t newOccupied = cache->occupied() + 1;
//获取当前缓存的总容量 capacity = mask + 1
mask_t capacity = cache->capacity();
//如果是空的缓存(occupied==0 &&  capacity==0)
top Created with Sketch.