OC - 类的cache_t分析

647 阅读8分钟

前言

通过前两篇文章 OC - 方法的本质(上)OC - 方法的本质(下),我们对类有了一定的了解。而且我们也分别对里面的 isasuperclassbits进行了分析探究,还有 cache 没有进行分析。今天我们就对cache 进行分析和探究。

cache_t 数据结构

通过前两篇文章,我们已经知道如何还原 bits 中的数据了。接下来我们试着还原 cache 中的数据。

(lldb) p/x TPerson.class
(Class) $0 = 0x0000000100008708 TPerson
(lldb) p (cache_t *)0x0000000100008718
(cache_t *) $1 = 0x0000000100008718
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515392
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32820
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000803400000000
      }
    }
  }
}
// 然后读取里面的值。
(lldb) p $2._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $3 = {
  std::__1::atomic<unsigned long> = {
    Value = 4298515392
  }
}
(lldb) p $3.Value
error: <user expression 4>:1:4: no member named 'Value' in 'explicit_atomic<unsigned long>'
$3.Value
~~ ^
(lldb) p $2._maybeMask
(explicit_atomic<unsigned int>) $4 = {
  std::__1::atomic<unsigned int> = {
    Value = 0
  }
}
(lldb) p $4.Value
error: <user expression 6>:1:4: no member named 'Value' in 'explicit_atomic<unsigned int>'
$4.Value
~~ ^
(lldb) p $2._originalPreoptCache
(explicit_atomic<preopt_cache_t *>) $5 = {
  std::__1::atomic<preopt_cache_t *> = {
    Value = 0x0000803400000000
  }
}
(lldb) p $5.Value
error: <user expression 8>:1:4: no member named 'Value' in 'explicit_atomic<preopt_cache_t *>'
$5.Value
~~ ^
(lldb) 

可以看到在cache_t中有 _bucketsAndMaybeMask_maybeMask_flags_occupied_originalPreoptCache 数据,但是都无法打印它们的Value。那么这些数据分别代表什么意思呢?接下来我们就一起看看底层源码( 苹果objc源码objc源码编译 ) 的描述和处理。

image.png

cache_t 中,我可以清晰的开到 _bucketsAndMaybeMask_maybeMask_flags_occupied_originalPreoptCache 的结构。

image.png

那么 cache_t 的缓存在哪里呢?IMPSEl 在哪里呢?接下来我们带着这些疑问继续查看源码。那么应该怎么去分析源码呢?既然 cache_t 是用来缓存数据的,那么它一定会有 的方法。我们可以通过查看 cache_t 中提供的 方法

image.png 看到这里,出现了一个 bucket_temptyBuckets() 清空 bucketsallocateBuckets() 开辟 bucketsemptyBucketsForCapacity()endMarker()bad_cache()

继续往下看

image.png 发现这里有一个 insert 插入方法。接下来我们就看下这个 insert 方法是如何实现的。

image.png 发现在 insert 方法中,对 bucket_t 进行了操作。得出一个结论:cache_t 的核心就是 bucket_t

接下来我们就看看 bucket_t

image.png 当我们点击进入 bucket_t 源码时,第一眼就看到了我们一直在寻找的 IMPSEl。就得到如下这张 cache_t 的数据结构图。

image.png

cache_t 分析

我们对 cache_t 数据结构有了一定了解。接下来我们就一起分析一下 cache_t

1. 通过 lldb 分析

根据我们之前对 bits 的分析思路。而且我们也已经在上文中拿到了 cache_t,那么如果拿到 IMPSEl呢?结合 cache_t 的数据结构图,我开始对 cache_t 进行分析。

(lldb) p [tp formatPerson]
2021-06-23 14:13:32.768517+0800 KCObjcBuild[5296:231526]  --- TPerson -- formatPerson ---
(lldb) p/x tClass
(Class) $0 = 0x0000000100008780 TPerson
(lldb) p (cache_t *)(0x0000000100008780+0x10)
(cache_t *) $1 = 0x0000000100008790
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4326470912
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32816
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001803000000007
      }
    }
  }
}
(lldb) p $2.buckets()
(bucket_t *) $3 = 0x0000000101e0b500
(lldb) p *$3
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 48368
    }
  }
}

我们已经分析得到了 bucket_t 了, 那如果获取 bucket_t 中的 IMPSEl 呢?继续查 bucket_t 的源码。

image.png

发现 bucket_t 提供了 sel()imp()rawImp() 方法。接下来我们依次验证。

(lldb) p $4.sel()
(SEL) $15 = "formatPerson"
(lldb) p $4.imp(nil, TPerson.class)
(IMP) $16 = 0x0000000100003b70 (KCObjcBuild`-[TPerson formatPerson])

这里有一个点需要注意:imp 需要传入一个 UNUSED_WITHOUT_PTRAUTH bucket_t *base 的参数。查看宏定义得知这个参数可以设置为 nilimage.png 这里有一个点需要注意:p $2.buckets() 时,有时候是无法获取到值的。是因为 buckets() 是一个集合 哈希函数,因为 哈希函数 内部元素的位置是随机的。可以通过 p $2.buckets()[1] 索引方式去取。这里笔者为什么通过p $2.buckets()就把值获取出来呢?也许是因为在一开始的时候就已经执行了 p [tp formatPerson] 方法。或者当输出如下格式的时候,就找到了 bucket_t 的值。笔者也曾在p $2.buckets()[6] 的时候才获取到值。所以大家要有耐心和细心!

(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 48368
    }
  }
}

2. 通过 脱离源码 分析

当我们在没有源码的情况下,或者有源码但是并不能调试的情况下,那么我们应该怎么进行分析呢?既然源码没法进行编译,那么我们就在自己的工程中,仿照源码写一份能进行编译的代码。

我们已经知道 objc_class 的数据结构,那么我们就自定义一个tcd_objc_class

image.png 这里有一个需要注意的点:objc_class 中有一个隐藏参数 isa,这里需要一一对应。如果没有添加会导致后续输出 cache 数据不对。

image.png

objc_class 中有一个 cache_t,我们也自定义一个 tcd_cache_t

image.png

objc_class 中有一个 class_data_bits_t,我们也自定义一个 tcd_class_data_bits_t

image.png

cache_t 中有一个 bucket_t,我们也自定义一个 tcd_bucket_t

image.png

objc_class 中需要的,我们都已经自定义完成。完整代码如下:

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct tcd_bucket_t {
    SEL _sel;
    IMP _imp;
};

struct tcd_cache_t {
    uint16_t _bucjetsAndMaybeMask; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

struct tcd_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct tcd_objc_class {
    Class isa;
    Class superclass;
    struct tcd_cache_t cache;             // formerly cache pointer and vtable
    struct tcd_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
};

所有需要的东西我们已经全部准备好了,就下来我们就一起看看怎么使用吧!

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        TPerson * tp = [TPerson alloc];
        Class tpClass = tp.class;
        [tp formatPerson0];        
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

定义好了 tp ,得到了 tpClass。然后我们就需要把 tpClassobjc_class类型)的强转成我们定义的 tcd_objc_class类型。

struct tcd_objc_class *tcd_class = (__bridge struct tcd_objc_class *)(tpClass);

接下来我们就打印一下 tcd_classcache

输出:
NSLog(@"--- TPerson -- formatPerson --- : %@",tcd_class->cache);

打印:
--- TPerson -- formatPerson --- : -[TPerson formatPerson0]

接着我们输出 _maybeMask_occupied

输出:
NSLog(@"-- _maybeMask : %u -- _occupied : %u ", tcd_class->cache._maybeMask, tcd_class->cache._occupied);

打印:
-- _maybeMask : 3 -- _occupied : 1

发现 _occupied 的值为 1_maybeMask 的值为 33 号位置。这样我们通过 lldb 分析打印的结果很相似了。

image.png

我们在 lldb 分析的时候得知数据是存储在 buckets 的,所以这里我将 tcd_cache_t 的结构调整一下。tcd_bucket_t 替换 _bucjetsAndMaybeMasktcd_bucket_t代码调整如下:

struct tcd_cache_t {
    struct tcd_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

我们知道 buckets 是一个哈希函数的集合,所有接下来我们就通过 for 循环打印一下结果。

TPerson * tp = [TPerson alloc];
Class tpClass = tp.class;
    
[tp formatPerson0];
struct tcd_objc_class *tcd_class = (__bridge struct tcd_objc_class *)(tpClass);
        
NSLog(@"-- _maybeMask : %u -- _occupied : %u ", tcd_class->cache._maybeMask, tcd_class->cache._occupied);
        
for (mask_t i = 0; i < tcd_class->cache._occupied; i++) {
    struct tcd_bucket_t bucket = tcd_class->cache._bukets[i];
    NSLog(@"-- %@ -- %p",NSStringFromSelector(bucket._sel), bucket._imp);
}

image.png 这里并没有得到我们想要的数据,那我们再换个方式打印。

for (mask_t i = 0; i < tcd_class->cache._maybeMask; i++) {
    struct tcd_bucket_t bucket = tcd_class->cache._bukets[i];
    NSLog(@"-- %@ -- %pf",NSStringFromSelector(bucket._sel), bucket._imp);
}

image.png 发现在最后一个打印了我们想要的数据。那么是为什么呢?首先occupied 表示里面存储了几个,我们这里只有1个,maybemask表示总共开辟了多少内存,我们这里开辟了3个内存。其次buckets 是一个哈希函数 集合,所以值并不一定在第一个。这里笔者打印时,数据就在最后一个。

接下来我们继续尝试调用多个方法。

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        TPerson * tp = [TPerson alloc];
        Class tpClass = tp.class;
        [tp formatPerson0];
        [tp formatPerson1];
        [tp formatPerson2];
        [tp formatPerson3];
        
        struct tcd_objc_class *tcd_class = (__bridge struct tcd_objc_class *)(tpClass);
        
        NSLog(@"-- _maybeMask : %u -- _occupied : %u ", tcd_class->cache._maybeMask, tcd_class->cache._occupied);
        
        
        for (mask_t i = 0; i < tcd_class->cache._maybeMask; i++) {
            struct tcd_bucket_t bucket = tcd_class->cache._bukets[i];
            NSLog(@"-- %@ -- %pf",NSStringFromSelector(bucket._sel), bucket._imp);
        }
        
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

在这个地方调用了 formatPerson0~3 四个方法。我们来看看打印结果呢?

image.png 根据打印结果我们可以得知:开辟了 7 个内存,但是只存储了 2 个。打印了方法 formatPerson3formatPerson2formatPerson0formatPerson1 方法去哪了呢?接下来我们就在 底层原理分析 中一探究竟。

3. 底层原理分析

首先我们先在cache_t 源码中找到 install 方法。

image.png

方法 void insert(SEL sel, IMP imp, id receiver); 中有我们比较熟悉的参数:SEL selIMP imp还有一个消息接收者id receiver。接下来我们就看看insert方法是怎么处理的。

1. 计算容量

image.png occupied() 初始化:第一次进入是 occupied()没有值为 0,插入 1newOccupied = 1

2. 开辟容量

image.png

INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)

即:capacity = 4;但是我们再上述分析过程中 得知 capacity = 3 啊。接下来我们看 reallocate 里做了什么操作。

image.pngreallocate 中做了三件事:1. allocateBuckets() 开辟 buckets 内存;2.通过 setBucketsAndMask 设置 bucketsmask 的值;3. 由freeOld 控制是否 collect_free 释放内存。接下来我们依次查看它们分别都做了什么。

allocateBuckets()

image.pngallocateBuckets() 中主要做了两件事:1. bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1); 开辟大小;2.end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);SELIMP 赋值。

setBucketsAndMask

image.pngsetBucketsAndMask中主要根据不同的架构系统向_bucketsAndMaybeMask_maybeMask 写入数据。

collect_free image.pngcollect_free中,主要是清空数据,回收内存。

3. 哈希存值

image.png buckets 通过 cache_hash(sel, m) 的开始位置,然后 do while 循环查找,为了防止哈希冲突 进行了 cache_next(i, m)哈希

4. 3/4 容积

超过75%进行扩容,低于75% 正常插入。

image.png

image.png 超出75%,进行capacity * 2 2倍扩容。扩容的最大容量不会超过mask 最大值的 2^15

4. insert 调用流程

insert 处,添加断点。

image.png

insert 调用栈:_objc_msgSend_uncached -> lookUpImpOrForward -> log_and_fill_cache -> insert

但是我们并不知道[tp formatPerson]_objc_msgSend_uncached是一个什么样的过程。接下来我们可以通过 汇编 的方式进行查看。

image.png

汇编 代码中,我们可以看到 [tp formatPerson] 的底层实现是通过 objc_msgSend 方法实现的。 这里我们就知道了 insert 方法的整个流程。[tp formatPerson] -> objc_msgSend -> _objc_msgSend_uncached -> lookUpImpOrForward -> log_and_fill_cache -> insert

附件:cache_t 流程图

未命名文件.png

补充:

1. 处理器适配架构

真机:ARM64、模拟器:i386、电脑:x86_64

image.png

总结

通过 lldb 结合源码调试,我们知道方式是存储在 cache 中,方法的 IMPSEL 存储在 bucket 中。然后我们又了解了仿照objc_class源码,自定义tcd_objc_class 的方法进行调试。发现这种方法调用简单,操作方便。接下来我们有对cache_t源码进行了分析,了解的方法的存储过程。知道了 [tp formatPerson] 在底层代码中是怎样调用的。