iOS 类的cache_t分析

759 阅读11分钟

开篇

好好学习,不急不躁。每天进步一点点

上篇文章:iOS 类的原理分析(二) 讲述了类的原理,以及分析类中的bits,类的方法,成员变量以及属性等数据的存储;总结了bits的内部结构;

截屏2021-07-10 下午5.39.11.png

可以看到bits是存储在 ro 内存里,并且里面存放了不少信息;

了解了bits,今天我们来了解一下cache;通过名字,我们可以大概知道,cache 即缓存,那么cache里,缓存的是什么数据呢?

cache_t

1.png

通过打印地址信息,查看到 cache_t包含:bucketsAndMaybeMaskmaybeMaskoriginalPreoptCache等数据信息;一个类里,需要缓存的数据,一般是属性,方法,成员变量等等这些数据,但cache_t里的数据,我们没有办法,直观的查看缓存的信息;既然cache是数据的存储,肯定会有数据写入,读取,那么我们深入源码,查找数据写入/读取的业务逻辑;

源码:
......

    static bucket_t *emptyBuckets();
    static bucket_t *allocateBuckets(mask_t newCapacity);
    static bucket_t *emptyBucketsForCapacity(mask_t capacity, bool allocate = true);
    static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
    void bad_cache(id receiver, SEL sel) __attribute__((noreturn, cold));
我们可以明显的看到,这里对 Buckets 进行了一系列的清空与再创建的操作。
......

    void insert(SEL sel, IMP imp, id receiver);

 ......
 这里有 insert,数据插入,我们可以着重看插入的逻辑,在插入底层,可以明显的看出,
 是对bucket_t 进行操作。
 
 再结合上述代码,所以 Buckets 那是重中之重呀。既然重要,那我们就着重看一下,bucket_t 里是什么结构;
 
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

可以看到,bucket_t内部,最重要的就是 imp,sel,一个imp,对应一个sel;

通过源码分析,我们可以总结出,cache_t的结构;

截屏2021-07-13 下午5.40.38.png

bucket_t 数据获取

源码环境,解读cache

了解cache_t结构,和buckets的作用,接下来验证bucket_t内部,是否真有imp,sel的存在。

截屏2021-07-13 下午10.53.32.png

IMP,SEL为空值,是由于方法并没有调用,既然没有调用,就不会有缓存,接下来我们调用一下,再重新输入看看是否有变化

1.png

可能有些人的电脑,直接输出buckets()的时候,得到的值会是空的,如果输出为空,可以参考之前获取属性、方法的get[index] 的方式,直接打印buckets()[index]方式,可以获取数据。

1.png

那么为什么会需要buckets()[index]方式才能看到数据呢?这时因为哈希下标,是乱序的,并不一定从0开始;所以通过这种内存平移的方式,可以获取数据;而我这边能打印出来,可能是因为电脑系统问 题,我是M1电脑。

_bucketsAndMaybeMask

1.png

获取_bucketsAndMaybeMask,打印其中的Value,得出的地址与buckets()的地址一致。意味着_bucketsAndMaybeMask存储的值,是buckets()的首地址。可以使用_bucketsAndMaybeMask,通过地址平移的方式,获取buckets()的数据;

在_bucketsAndMaybeMask存储buckets()的首地址,是系统内存优化的特性,一个bucket内部是se l、imp的结构,而buckets内部,存在多个bucket,如果要把这些bucket全部存入_bucketsAndMaybeMask,那么就会导致cache结构非常庞大臃肿,读取性能也会非常差。所以系统只存首地址,再通过首地址,不断平移,去获取相应的数据;

我们查看buckets()的方式执行源码,源码底层,也是在_bucketsAndMaybeMask中拿到地址后,再使用地址&(与)上mask,最后强转成bucke_t的形式;

buckets.png

而bucketMask在不同的系统上,得到的值是不一样的; bucketMask.png

通过上面的操作,虽然我们看到imp 的 Value内有值,但是,sel里,却没有值,接下来,我们要将sa yHi这个方法,从sel中读取出来;

进入bucket_t内部,可以看到源码中,有获取sel的方法:

inline SEL sel() const { return _sel.load(memory_order_relaxed); 

sel.png

我们再来读取一下imp

截屏2021-07-14 下午3.02.47.png

至此,我们都能读取出bucket_t中的imp和sel。

非源码环境,验证bucket_t

通过源码分析,我们可以明确,sel、imp缓存在buckets内部,但是有时候我们不存在源码环境,或者是运用LLDB调试流程繁琐,我们该如何验证呢?

1、首先,我们要明确,我们获取的是cache_t的缓存数据,而cache是保存在class这个结构体中,所以我们需要构造出 class 结构;

截屏2021-07-14 下午10.47.12.png

2、接下来是cache_t结构,cachet_t内,包含了buckets、mask、flags、occupied等其他数据;

截屏2021-07-14 下午10.41.56.png

3、sel、imp是存储在bucket内部,那么也需要一个bucket的结构;

截屏2021-07-14 下午10.48.12.png

4、验证

截屏2021-07-14 下午10.49.24.png

5、结果,有点问题,应该是跟我的电脑系统有关系,我是M1电脑。🤣🤣🤣 !!!!!! 干了个尬

截屏2021-07-14 下午11.15.27.png

cache 数据写入

了解了数据的读取,接下来我们分析数据是如何写入的;前面我们探析源码,发现一个insert方法,那么我们来看看,这里是如何写入数据的;

    void insert(SEL sel, IMP imp, id receiver);
    插入的是sel、imp、receiver(当前消息接收者),相当于把sel、imp,插入给当前的消息接收者;    

cache 存储方式

空白.png cache_t 是一个结构体,所以得通过指针平移去获取 数据,所以要将buckets存入cache_t中,系统存入 的,将会是buckets的指针地址;

buckets 内部,会有很多的bucket,如果直接将buckets 存入cache,那么cache将会非常庞大,苹果为了内存空间, 只将 buckets的指针地址存入,需要数据的时候,再通过地址 去获取;

cache 插入流程

1.png


2.png

init_cache_size.png


3.png

cache 流程图

cache.png

知识补充

哈希

哈希下标,与数组下标排列顺序不同,数组下标是从0开始排序,但是哈希函数不是,它并不一定从0开始排序;

在数据结构中,我们有数组结构,链表结构,哈希结构;

数组结构:是连续的,可以通过下标去获取对应的值,但是在插入和删除方面,就比较麻烦,比如插入,它是顺序插入,相当于遍历所有下标,得到没有值的下标后,才存入该下标;

链表结构:通过指针地址,指向内容;链表结构在插入数据方便,非常便捷,比如,在A、B中间插入E,则将A的指针指向E,再由E指向B,即可,不需要通过下标形式去插入;但由于没有下标概念,链表结构查找数据比较麻烦,需要遍历整个结构的地址来进行对比查找;

哈希链表(拉链法):结合了数组结构和链表结构,既好插入数据,又好查找;哈希链表内有一个哈希函数,通过哈希函数,可以得到一个下标,再通过得到的下标去获取数据;

哈希函数一般是通过 左移 << 或者 >> 右移,或者是取余 % 的方式,得到下标;但是哈希函数取下标的方式,容易产生下标一致,比如说,8%6 == 1, 8%5 == 1;两个下标都为1,但是数据不一样,这就是哈希冲突;解决哈希冲突,需要结合链表的性质进行平移,然后再进行哈希,这样就可以解决哈希冲突;

空白.png

架构

在iOS中,一共存在几种架构体系,真机架构,模拟器架构,mac 架构等;

1、LP64 :Unix和Unix类的系统(Linux,Mac OS X)

2、arm64 :真机架构

3、x86_64:mac架构

4、i386:模拟器架构

面试题补充

BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
BOOL re3 = [(id)[QLYPerson class] isKindOfClass:[QLYPerson class]];       //
BOOL re4 = [(id)[QLYPerson class] isMemberOfClass:[QLYPerson class]];     //
NSLog(@" re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);

BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];       //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];     //
BOOL re7 = [(id)[QLYPerson alloc] isKindOfClass:[QLYPerson class]];       //
BOOL re8 = [(id)[QLYPerson alloc] isMemberOfClass:[QLYPerson class]];     //
NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

做这道面试题,需要通过源码,结合isa走位图

isa流程图.png

源码:

+ (BOOL)isMemberOfClass:(Class)cls { //类方法
    return self->ISA() == cls;
}

- (BOOL)isMemberOfClass:(Class)cls { //对象方法
    return [self class] == cls;
}

/*
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) 解释:
    Class tcls = self->ISA(); 通过 ISA,获取当前self的类,赋值给tcls;
    tcls; tcls是否存在,存在则执行for循环内的逻辑,不存在则执行,tcls = tcls->getSuperclass();
    tcls = tcls->getSuperclass(),不存在 tcls,所以继续往深层获取tcls
*/
+ (BOOL)isKindOfClass:(Class)cls { //类方法
    for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) 
    {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls { //对象方法
    for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

是不是很多人,都直接通过以上的源码,去分析这道面试题?

那么恭喜你,源码看错了。

苹果就是这么顽皮,但你能打他吗?

打死.png

我们通过汇编,来查看实际源码走向:

源码.png

寻找一下这两个API,查看内部源码:

Class
objc_opt_class(id obj)
{
#if __OBJC2__
    if (slowpath(!obj)) return nil;
    Class cls = obj->getIsa();
    if (fastpath(!cls->hasCustomCore())) {
        return cls->isMetaClass() ? obj : cls;
    }
#endif
    return ((Class(*)(id, SEL))objc_msgSend)(obj, @selector(class));
}

// Calls [obj isKindOfClass]
BOOL
objc_opt_isKindOfClass(id obj, Class otherClass)
{
#if __OBJC2__
    if (slowpath(!obj)) return NO;
    Class cls = obj->getIsa();
    if (fastpath(!cls->hasCustomCore())) {
        for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) {
            if (tcls == otherClass) return YES;
        }
        return NO;
    }
#endif
    return ((BOOL(*)(id, SEL, Class))objc_msgSend)(obj, @selector(isKindOfClass:), otherClass);
}

所以,这才是真真真真真真的,底层走的源码,接下来,我们开始分析这几道面试题

第一题: BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]

解析: Class cls = obj->getIsa();通过isa走位,获取(id)[NSObject class]的元类,也就是root class(meta),赋值给cls;

for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) ; cls赋值给tcls,如果tcls存在,则执行for内的逻辑;如果不存在,则执行tcls = tcls->getSuperclass(),重新给tcls赋值;

所以第一次循环,tcls存在,直接判断 if (tcls == otherClass),tcls是元类,元类 与 NSObject不等,所以执行第二次循环;

第二次循环:tcls = tcls->getSuperclass(),上一次循环,tcls变成元类,这次获取元类的根类,也就是 tcls = root class(class),tcls = NSObject;

执行判断,if (tcls == otherClass) ,NSObject = NSObject,相等;

所以 re1 最后返回 true;


第二题: BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]

解析: re2---> 传入的是NSObject,对比的也是NSObject:

Class cls = obj->getIsa(); 通过isa,获取 传入的类,得到的类为: NSObject

return cls->isMetaClass() ? obj : cls; 三目运算,如果cls指向元类为真,则返回obj,反之则返回cls;

所以:cls 指向的类,为NSObject,不是元类;所以 re2 返回flase


第三题: BOOL re3 = [(id)[QLYPerson class] isKindOfClass:[QLYPerson class]];

解析: Class cls = obj->getIsa();通过isa走位,获取(id)[QLYPerson class]的元类,Subclass (meta),赋值给cls;

for (Class tcls = cls; tcls; tcls = tcls->getSuperclass()) ; cls赋值给tcls,如果tcls存在,则执行for内的逻辑;如果不存在,则执行tcls = tcls->getSuperclass(),重新给tcls赋值;

所以第一次循环,tcls存在,直接判断 if (tcls == otherClass),tcls是元类,元类 与 QLYPerson不等,所以执行第二次循环;

第二次循环:tcls = tcls->getSuperclass(),上一次循环,tcls变成元类,这次获取元类的根类,也就是 tcls = Root class (meta) = NSObject meta,执行判断,if (tcls == otherClass) ,Root class (meta) = QLYPerson,不相等;

第三次循环:tcls = tcls->getSuperclass(),上一次循环,tcls变成根元类,这次获取根元类的父类,也就是 tcls = Root class(class)= NSObject,执行判断,if (tcls == otherClass) ,NSObject = QLYPerson,不相等;

所以:re3 为 false;


第四题: BOOL re4 = [(id)[QLYPerson class] isMemberOfClass:[QLYPerson class]];

解析: re2---> 传入的是QLYPerson,对比的也是QLYPerson:

Class cls = obj->getIsa(); 通过isa,获取 传入的类,得到的类为: QLYPerson

return cls->isMetaClass() ? obj : cls; 三目运算,如果cls指向元类为真,则返回obj,反之则返回cls;

所以:cls 指向的类,为NSObject,不是元类;所以 re2 返回flase


第五题: BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
解析:传入的是一个NSObject对象,对比的是一个NSObject 类,所以是对象与类相比;

Class cls = obj->getIsa(); cls 拿到的是root class(class),也就是拿到了NSObject, 第一次循环:tcls = cls = NSObject,tcls存在,直接判断 if (tcls == otherClass), NSObject = NSObject;相等;

所以:re5 为 true


第六题: BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
解析: 传入NSObject对象,对比的是一个NSObject 类,执行对象方法; 所以,传入对象的类,与需要对比的类,同为NSObject,

所以:re6 为 true


第七题: BOOL re7 = [(id)[QLYPerson alloc] isKindOfClass:[QLYPerson class]]; 解析:传入的是一个QLYPerson对象,对比的是一个 QLYPerson 类,所以是对象与类相比;

Class cls = obj->getIsa(); cls 拿到的是subclass(class),也就是拿到了QLYPerson, 第一次循环:tcls = cls = QLYPerson,tcls存在,直接判断 if (tcls == otherClass), QLYPerson = QLYPerson;相等;

所以:re5 为 true


第八题: BOOL re8 = [(id)[QLYPerson alloc] isMemberOfClass:[QLYPerson class]];
解析:传入QLYPerson对象,对比的是一个QLYPerson 类,执行对象方法; 所以,传入对象的类,与需要对比的类,同为QLYPerson,

所以:re8 为 true