前言
在前面的文章中iOS底层探究--------类的原理分析(下),我们探索了isa、superclass、bits,那么整个类结构(如下图所示),还剩下cache没有进行探索了。那么今天,就来把这块给补齐。
cache从词面上来看,是缓存的意思。那么这个cache的底层是怎样的了,我们接下来进行探索。
从上篇文章中,我们获取bits是通过内存平移得到的,那么我们获取cache是不是也可以用这个方法,探索cache是什么?他的基本数据结构是什么?
cache的流程图
1、探索cache的数据结构
1.1、通过lldb调试,初探cache内存结构
探索的用的代码还是objc源码,创建一个TestPerson类,然后在main.m里面获取其Class,再通过lldb调试查看,如下图所示:
那么接下来,就是lldb的调试结果了:
类结构里面的包含的内容是:isa(8字节)、superclass(8字节)、cache(16字节)、bits。根据源码,其中,cache是cache_t结构体,进入到cache_t结构体里面(源码代码):
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
//省略代码。。。
//cache的平移
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1;
//省略代码。。。。
//bucket_t的清空操作
static bucket_t *emptyBuckets();
//省略代码。。。。
//插入SEL sel, IMP imp, id receiver的操作
void insert(SEL sel, IMP imp, id receiver);
//省略代码。。。。
}
看到源码,再对比下刚刚lldb调试中,$3所打印的结果,是不是能发现,就是cache里面的内容。到这里,就有疑问了,既然cache是缓存,那么缓存的也应该是变量和方法,应该是与SEL或者IMP相关才对,但是,通过源码,却没有看到与之相关的。根据打印的结果,_bucketsAndMaybeMask和_originalPreoptCache所包含的value,分别都是有值的。
既然都有值,那么就需要对应的去看_bucketsAndMaybeMask和_originalPreoptCache各自的功能方法了。
cache能够进行缓存,那么与之对应的,就应该有增、删、改、查的功能,读写一定少不了。回到源码中,除了_bucketsAndMaybeMask和_originalPreoptCache,还有很多是对cache的地址平移的操作(如:((uintptr_t)1 << (64 - maskShift)) - 1);bucket_t的清空与创建;甚至还有插入的操作 insert(SEL sel, IMP imp, id receiver)。那么我们需要分析出,哪个才是关键点。
1.2、cache里面缓存的方法是在bucket_t里面
既然有插入,那么就有读写,那么进入到插入的操作里面看看具体执行了什么?
void cache_t::insert(SEL sel, IMP imp, id receiver) {
//对各种不符合条件的判断报出错误码
//省略代码。。。。
//通过buckets数组来判断需要插入的内容情况
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
//省略代码。。。。
通过快速浏览,可以知道,插入是需要buckets数组的,而接收者是bucket_t,那么这个关键点,算是找到了。那么就需要查看bucket_t的具体实现了。
struct bucket_t {
private:
x86_64.
#if __arm64__ //架构1 arm64
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else //除了架构1其他的架构
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
//省略代码。。。。
进入到bucket_t里面,就很明了了,其本身就是个数组buckets,里面就是很多组imp和sel的缓存地方,_imp和_sel一一对应,如下如所示:
- 综上分析,我们就能得到一个大概的数据结构关系走势图了:
2、cache底层lldb分析
2.1、lldb深入探究
理清这个结构图之后,我们就需要到代码中去验证了,通过lldb的调试,看看里面是不是存储了方法,通过前面lldb调试到的$3接着往下查找_bucketsAndMaybeMask所储存的内容:
最后却是报了
error的错误。那么说明_bucketsAndMaybeMask查找的路线走不通,接着换_originalPreoptCache查找试试:
发现同样走不通。
我们发现都走不通的时候,就直接去源码中去寻找有没有相关的方法了。再结合刚刚分析的cache的大概数据结构图,就能锁定bucket_t相关的方法了。既然sel和imp的组合是存储在buckets数组里面,那么就到这个数组里面去查找:
//省略代码。。。。
struct bucket_t *buckets() const;
//省略代码。。。
可以查看buckets()存储情况,再次进行lldb调试,还有就是需要先执行TestPerson里面的saySomething方法,才会有缓存,所以在最开始的时候,执行saySomething方法:
这次调试的结果,能够得到_sel和_imp的信息了,虽然他们的value为0,但是方向上是正确的了。但是我们发现在$3中,_occupied的值是为0的,这个是什么原因了?因为方法没有调用,所以没有开始缓存。所以在调试之前,需要先调用。
根据上面的调试结果,发现$5找到的地址,存储的值是空的,因为_sel和_imp对应的值都是0,我们明明是进入到buckets数组里面去取的值。之所以会出现这样的情况,是因为苹果系统在将_sel和_imp组合存入buckets数组时,做了哈希处理。
用了个哈希链表。什么事哈希链表了?先看看接下来lldb调试的步骤,通过 p $3.buckets()[2],就是去buckets数组的第2个角标的值:
这就把方法给找到了。
2.2、Hash链表
为什了是 p $3.buckets()[2]了?
这个就得了解什么是哈希链表了?
- 计算机的存储方式有两种,一种是顺序存储,另一种是链式存储。
哈希链表,是采用数组和链表相结合的数据结构,比如,有一组关键数字
{12,13,25,23,38,34,6,84,91},Hash 表长为14,Hash函数为address(key) = key % 11;还有就是一个array[13]的数组结构。在这组关键数字中,通过哈希函数算出12、23、34的哈希值是一样的,都为1,那么,12、23、34就以链表结构的形式,存储在数组1号位的元素中,如下图所示:
其他的数字处理方法是一样的,根据哈希函数算出的哈希值,再把对应的数字存储进入对应的号位里面。
2.3、验证苹果系统对buckets数组做了Hash处理
那么回归到cache的分析上来,那么方法的存储号位,就不一定存储在0号位,也可能是请他号位。就比如找到的这个方法,就在2号位找到的。
现在我们要想知道我们刚刚找到的这个方法到底是什么?那么就需要到bucket_t里面找是否有相应的方法。看源码:
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
是有个sel()方法输出的。那么,在lldb的调试里面,加上sel(),就能打印出方法名了:
这样就能找到我们需要找的方法了。
那么相应的就要找_imp的输出结果了,那么还是要在源码中找方法:
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)
根据源码,需要传入一个base和cls,由于我们不知道base是什么,所以就传了nil,cls就是TestPerson类了,传pClass,就能得到结果:
接下来,用代码来分析。
3、脱离源码分析cache
3.1、仿照源码,简化版实现类结构体和cache结构体
之所以要用代码分析:
- 第一个原因:假如下载到陌生的代码或者是无法直接运行调试的源码时,会更方便一些;
- 第二个原因:当在使用
lldb时,增减一些属性、方法,就需要再次执行比较多的重复步骤,比较繁琐; - 第三个原因:小规模取样的方式,会让你对底层更加清晰。
我们就以根据源码,自定义一个类似的类的数据结构,来查看cache里面的情况。
那么就建立一个project工程,在工程里面,创建一个TestPerson类,代码如下:
//TestTestPerson类
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TestPerson : NSObject
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)sayHappy;
@end
#import "TestPerson.h"
@implementation TestPerson
- (void)say1{
NSLog(@"TestPerson say : %s",__func__);
}
- (void)say2{
NSLog(@"TestPerson say : %s",__func__);
}
- (void)say3{
NSLog(@"TestPerson say : %s",__func__);
}
- (void)say4{
NSLog(@"TestPerson say : %s",__func__);
}
- (void)say5{
NSLog(@"TestPerson say : %s",__func__);
}
- (void)say6{
NSLog(@"TestPerson say : %s",__func__);
}
- (void)say7{
NSLog(@"TestPerson say : %s",__func__);
}
+ (void)sayHappy{
NSLog(@"TestPerson say : %s",__func__);
}
@end
//main.m文件
#import <Foundation/Foundation.h>
#import "TestPerson.h"
#import <objc/runtime.h>
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits
//bucket_t结构体
struct sc_bucket_t {
SEL _sel;
IMP _imp;
};
//cache_t 结构体
struct sc_cache_t {
//uintptr_t _bucketsAndMaybeMask 换成下面这行(通过源码如何获取bucket(源码): uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);)
struct sc_bucket_t *_bukets; // 8
mask_t _maybeMask; // 4
uint16_t _flags; // 2
uint16_t _occupied; // 2
};
//bits结构体
struct sc_class_data_bits_t {
uintptr_t bits;
};
//类的结构体
struct sc_objc_class {
Class isa;
Class superclass;
struct sc_cache_t cache; // formerly cache pointer and vtable
struct sc_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestPerson *p = [TestPerson alloc];
Class pClass = p.class;
struct sc_objc_class *sc_class = (__bridge struct sc_objc_class *)(pClass);
NSLog(@"%hu - %u",sc_class->cache._occupied,sc_class->cache._maybeMask);
}
return 0;
}
现在,不调用TestPerson类里面的实例方法和类方法,运行下,查看结果:
现在再调用TestPerson的say1方法,[p say1];
接着,我们可以再增加say2、say3、say4、say5、say6、say7等实例方法,看打印结果:
有点当机了,这是啥情况啊?
3.2、类里面的方法不是存在cache里面
3.2.1、类里面的方法存储在另外新开辟内存里
要寻找答案,就需要进入到源码里面去了。
在结构体
cache_t里面,他的其中一个地址存储在_bucketsAndMaybeMask中,而_bucketsAndMaybeMask是由两部分构成:buckets和mask,也就是说里面相当于存储了这么两个数据。那该怎么去获取这两个数据了?
有个切入点,就是我们前面提到的缓存方法的插入。
我们知道cache就是缓存,那么有缓存,就会有插入,也就会有写的过程。那么就能在cache_t的结构体里面,找到插入的方法:
从这个插入的方法来看,插入的参数有sel、imp、还有receiver消息接收者(谁调用了这个消息)。那么在进入这个插入方法里面去看实现:
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
//省略代码。。。。(一些报错误码的判断)
// Use the cache as-is if until we exceed our expected fill ratio.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
#endif
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
}
由源码可知,首先定义一个newOccupied,他的值是occupied() + 1,而occupied()方法里面,就只有一句return _occupied,那么occupied()初始值就为0,所以newOccupied的初始值为1;根据源码,接下来就是有无缓存的判断,当编译第一次进来的时候,缓存是为空的,所以就进入下面这个判断中 :
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
//INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),而INIT_CACHE_SIZE_LOG2 = 2,所以是向左平移2。
//0x001,偏移2位,就是0x100,所以INIT_CACHE_SIZE的值为4,
reallocate(oldCapacity, capacity, /* freeOld */false);
}
一开始没有capacity,所以直接去获取INIT_CACHE_SIZE的值4,那么capacity = 4。紧接着就是reallocate()开辟操作,进入reallocate()实现方法里面:
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
//原有的buckets(),赋值给旧的oldBuckets
bucket_t *oldBuckets = buckets();
//开辟新的buckets
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
//cache_t的第一个地址就是_bucketsAndMaybeMask,此时是第一次编译,所以插入的是一个空的容器进来,给接下来将要被插入的内容一个装填的容器
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
我们先看下在buckets的开辟过程,进入allocateBuckets()方法里面去,查看实现:
size_t cache_t::bytesForCapacity(uint32_t cap)
{
return sizeof(bucket_t) * cap;//bucket_t大小
}
#if CACHE_END_MARKER //macOS 模拟器
bucket_t *cache_t::endMarker(struct bucket_t *b, uint32_t cap)
{
//(首地址+开辟的内存)-1,相当于获取最后一个位置的地址
return (bucket_t *)((uintptr_t)b + bytesForCapacity(cap)) - 1;
}
bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
// Allocate one extra bucket to mark the end of the list.
// This can't overflow mask_t because newCapacity is a power of 2.
bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);//开辟新的bucket_t
bucket_t *end = endMarker(newBuckets, newCapacity);//获取数组中最后一个bucket的地址
#if __arm__
// End marker's sel is 1 and imp points BEFORE the first bucket.
// This saves an instruction in objc_msgSend.
end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
// End marker's sel is 1 and imp points to the first bucket.
//给最后一个位置的bucket赋值 sel = 1,imp = 新创建的bucket地址,
end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
if (PrintCaches) recordNewCache(newCapacity);//记录新的缓存
return newBuckets;
}
到此,就是newBuckets的开辟过程。
接着再进入到setBucketsAndMask()方法的实现中去:
void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
#ifdef __arm__ //
// ensure other threads see buckets contents before buckets pointer
mega_barrier();
//这里开始把newBuckets存储到_bucketsAndMaybeMask里面去
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);
// ensure other threads see new buckets before new mask
//设置内存屏障,防止多个线程同时访问
mega_barrier();
_maybeMask.store(newMask, memory_order_relaxed);
//此时occupied值为0
_occupied = 0;
#elif __x86_64__ || i386 //macOS 和 模拟器
// ensure other threads see buckets contents before buckets pointer
//这里开始把newBuckets存储到_bucketsAndMaybeMask里面去
_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
// ensure other threads see new buckets before new mask
//把_maybeMask写入数据
_maybeMask.store(newMask, memory_order_release);
_occupied = 0;//新开辟的bucket里面,没有缓存任何方法,占位计数为0
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}
3.3.2、cache通过存储的地址到新开辟的空间读写方法
-
通过源码的分析,就知道,在
cache里面,只是存储指针地址,而被调用的方法,却存在另外开辟的一片空间内(源码分析里,所说的容器)。而这个新开辟存储方法的空间,对方法的存储方式是:每一个方法的sel和imp存储在一个bucket里面,而bucket又存储在新开辟的内存里面,这就是buckets数组的来源。当读取方法的时候,就只要通过cache里面的指针地址,到新开辟的空间里面找到对应的bucket来获取方法。 -
这样的好处就是,无论存入多少方法,
cache的内存不会随着方法的数量增多而无限增大,因为cache里面存的只是指针地址,每个指针地址的大小,只有8字节。这就是苹果对内存的优化。下面是图解:
3.3.3、buckets数组存储通过Hash处理
接着再返回到void cache_t::insert(SEL sel, IMP imp, id receiver)这个插入方法的实现里面。当开辟新的存储空间之后:
//获取buckets
bucket_t *b = buckets();
//刚刚在开辟空间时,计算出来,得到的capacity为4,这里再减1,那么m = 3
mask_t m = capacity - 1;
//因为buckets是数组,起始分配的空间是4,那么第一个方法存放的位置,是通过哈希计算出一个地址,就是开始的缓存位置。
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
当计算出mask_t m = 3时,就能解释,之前调用TestPerson类的say1方法,打印出的结果是1-3了,_occupied = 1,_maybeMask = 3。
当得到buckets数组后,开始存储的起始位置,就需要通过哈希函数进行计算了,进入到cache_hash()方法里面:
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
当计算出这哈希值后,就开始进行缓存了。接着就进行一个do-while循环,查找空位置。
do {
if (fastpath(b[i].sel() == 0)) {//当找到空位置
incrementOccupied();//这个方法的实现是:_occupied++;就是占位计数+1,表示这个bucket存储了方法.
b[i].set<Atomic, Encoded>(b, sel, imp, cls());//在buckets数组的i位置,插入对应的参数,把sel和imp写入进来
return;
}
if (b[i].sel() == sel) {//当写入的方法已经存入缓存中,就直接返回
// The entry was added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
return;
}
//直到找到的位置不是开始的位置时,才进行缓存
} while (fastpath((i = cache_next(i, m)) != begin));
存储时,为了防止哈希冲突(就是方法不一样,但是下标一样),所以再次进行哈希一次cache_next(),看下面源码实现,分为不同架构:
#if CACHE_END_MARKER // __arm__ || __x86_64__ || __i386__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
#elif __arm64__ //真机
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
那么这就是第一次执行插入方法的过程了。
3.3.4、根据类里面的方法的增减,会重新开辟内存空间
接下来的方法插入时:
- 当缓存的方法所占容量小于目前总容量
3/4(capacity * 3 / 4),就执行判断:else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))),而cache_fill_ratio(capacity) = capacity * 3 / 4:
static inline mask_t cache_fill_ratio(mask_t capacity) {
return capacity * 3 / 4;
}
- 当满存时。苹果支持容量满存,在满存的情况下,不做任何处理
#if CACHE_ALLOW_FULL_UTILIZATION
else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
// Allow 100% cache utilization for small buckets. Use it as-is.
}
- 当缓存的方法所占容量超出目前总容量的
3/4(capacity * 3 / 4),那么就需要扩容了capacity * 2,执行else判断。刚刚算出来,capacity为4,两倍扩容就是8,但是mask_t m = capacity - 1,所以,m = 7。else判断源码:
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
如何进行扩容的了?就得进入reallocate()方法里面:
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
// Cache's old contents are not propagated.
// This is thought to save cache memory at the cost of extra cache fills.
// fixme re-measure this
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
会重新开辟一个新的空间,因为传入进来的freeOld为true,所以,也会把旧的空间给释放掉,进入collect_free()方法里面:
void cache_t::collect_free(bucket_t *data, mask_t capacity)
{
#if CONFIG_USE_CACHE_LOCK
cacheUpdateLock.assertLocked();
#else
runtimeLock.assertLocked();
#endif
if (PrintCaches) recordDeadCache(capacity);
//把垃圾站进行清空回收
_garbage_make_room ();//创建回收站
garbage_byte_size += cache_t::bytesForCapacity(capacity);//获取开辟的内存大小
garbage_refs[garbage_count++] = data;//将bucket的地址往后移
cache_t::collectNolock(false);//释放回收内存
}
所以,就解释了上面,在工程里面,第二次打印时,为什么只有say7的地址,而其他的6个地址为0的原因,也间接的可以看出,扩容的操作进行了两次,第一次是在4时,进行扩容到8,当8也不能满足的时候,扩容到16,所以,才有1-15这的结果。那为什么say7还在了?
逐步分析下,根据刚刚源码分析的结果,当say7写入进来时,系统发现内存不够了,那么就会进行扩容操作,旧的空间内存是存储的say1到say6的方法,此时扩容,把say7存入新的空间内存里面,然后把旧的空间内存给释放掉,所以,当再去打印say1到say6的地址时,是为0x0的。
- 苹果系统之所以这么做,是因为,原有开辟内存是不能修改的。只能是重新创造一个新的内存来取代原来的内存。同时,数组的平移是十分耗费性能的。当开辟新的内存时,苹果系统会把旧内存里面存储的数据视为不活跃数据,是可以进行剔除的。
如上图所示,在存入
say3时,已经把原有内存释放了,当再次执行say1、say2时:因为哈希函数计算的原因,方法存储之间可能存在空隔。
4、cache在插入方法之前做了什么?
当有方法将要被缓存时,cache执行的流程,但是,是谁发起的缓存消息了?接下来通过实例方法来探究,当TestPerson类调用- (void)saySomething方法时,是怎样执行到insert()上去的。
4.1、探究insert()之前执行流程
既然我们要探究cache在插入方法前做了哪些事情,那么我们是不是的在cache_t结构体里面,插入的方法上,进行跟踪。那么就在insert()方法的实现里面打上断点。如下图:
当运行到断点上时,可以通过
lldb,输入bt指令查看堆栈消息(其实,在工程的左边,也能看到):
根据堆栈消息,我们就知道在执行插入
insert()方法之前,是执行的方法流程:从main()函数的autoreleasepool --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert.
4.2、通过汇编查看信息
刚刚通过堆栈信息,知道了调用实例方法[p saySomething],会有从_objc_msgSend_uncached执行到cache_t::insert这样的部分流程,但是从开始调用方法,到_objc_msgSend_uncached这部分流程,还不知道,那么我们就可以用终极手段--汇编。
执行
[p saySomething]后,是走的objc_msgSend()方法。接着跟踪断点(里面内容太多,分两段截图):
在
objc_msgSend()方法里面,再执行了_objc_msgSend_uncached()方法。到了这里,就与前面的步骤连接上了。
- 调用
insert()方法流程:[p saySomething]底层实现objc_msgSend-->_objc_msgSend_uncached-->lookUpImpOrForward-->log_and_fill_cache-->cache_t::insert
那么insert()的流程图为:
ok,cache的分析,到此全部完成了。