OC底层原理07 - 类结构探索(2)

类结构探索(1)中,对类结构中的isa进行了探索,接下来将对类结构中的其它成员进行探索。

cache_t cache

cache主要是用来缓存方法的,但如何缓存还需要我们去探索,首先来看一下cache_t这个结构体。

struct cache_t {
//表示运行的环境 模拟器 或者 macOS
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    // 是一个结构体指针类型,占8字节
    explicit_atomic<struct bucket_t *> _buckets; 
    //mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    explicit_atomic<mask_t> _mask; 

//表示运行环境是 64位的真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    //指针类型,占8字节
    explicit_atomic<uintptr_t> _maskAndBuckets; 
    //mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
    mask_t _mask_unused; 
    
#if __LP64__
    //uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    uint16_t _flags;  
#endif
    //uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
    uint16_t _occupied; 
}

cache_t结构体在不同的架构中含有的属性个数不同,在真机中对mask和buckets存储进行了优化,将这两个属性存储到一个指针里面。

以下以macOS为例进行说明。

  • _buckets
    它是一个的数组,里面存放了多个bucket_t结构体,而每一个bucket_t结构体中又存放了selimp
struct bucket_t {
private:
#if __arm64__ //真机
    //explicit_atomic 是加了原子性的保护
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else //非真机
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
    //方法等其他部分省略
}
  • _mask
    _mask是指掩码数据,用于在哈希算法或者哈希冲突算法计算哈希下标,其中mask 等于capacity。
  • _occupied
    _occupied表示哈希表sel-imp占用大小(即可以理解为分配的内存中已经存储了sel-imp个数)。

我们通过一个示例来进行探索。

准备工作

  • 定义一个自定义类LGPerson,并在这个类中定义两个属性,若干个实例方法。
@interface LGPerson : NSObject

@property(nonatomic, strong)NSString* name;
@property(nonatomic, strong)NSString* nickName;

- (void)say111;

- (void)say222;

- (void)say333;

- (void)say444;

- (void)say555;

@end

@implementation LGPerson
- (void)say111{
    NSLog(@"%s", __func__);
}

- (void)say222{
    NSLog(@"%s", __func__);
}

- (void)say333{
    NSLog(@"%s", __func__);
}

- (void)say444{
    NSLog(@"%s", __func__);
}

- (void)say555{
    NSLog(@"%s", __func__);
}

@end3
  • 在main.cpp中,定义一个LGPerson的对象,并使用该对象调用其对象方法。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //0x00007ffffffffff8ULL
        LGPerson* person = [LGPerson alloc];
        Class pClass = [LGPerson class];
        [person say111];
        [person say222];
        [person say333];
        [person say444];
        [person say555];
    }
    return 0;
}

开始探索

  • 将程序运行起来,在调用对象方法之前,通过断点停下。通过lldb查看一下,当前catch_t中的内容。
  1. 获取类的首地址
(lldb) p/x pClass
(Class) $0 = 0x0000000100008320 LGPerson
  1. 由于类结构体中的前两个成员为isasuperclass,各占8个字节,因此,将首地址偏移16个字节,即为cache起始地址
(lldb) p/x (cache_t*)0x0000000100008330
(cache_t *) $1 = 0x0000000100008330
  1. 读取cache中的内容
(lldb) p *$1
(cache_t) $2 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = {
      Value = 0x0000000100346460
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = {
      Value = 0
    }
  }
  _flags = 32804
  _occupied = 0
}
  1. 获取buckets中的selimp
(lldb) p $2.buckets()[0].sel()
(SEL) $3 = <no value available>

由此时可以看出,当未调用对象方法时,cache中没有缓存

  1. 调用一次对象方法后,再读取一次buckets中的selimp
(lldb) p *$1
(cache_t) $4 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = {
      Value = 0x000000010070ea30
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = {
      Value = 3
    }
  }
  _flags = 32804
  _occupied = 1
}

(lldb) p $2.buckets()[0].sel()
(SEL) $5 = "say111"

此时可以发现,当调用了一次对象方法后,cache中缓存一次方法
那再调用一次对象方法呢,是不是又会缓存一次?为了验证这个想法,让应用再调用一次对象方法后,再查看一下当前cache中的内容。

(lldb) p *$1
(cache_t) $7 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = {
      Value = 0x000000010070ea30
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = {
      Value = 3
    }
  }
  _flags = 32804
  _occupied = 2
}

(lldb) p $7.buckets()[0].sel()
(SEL) $8 = "say111"
(lldb) p $7.buckets()[1].sel()
(SEL) $9 = "say222"

总结:

  • 未调用对象方法之前,_occupied为0,cache中没有缓存。
  • 调用一次对象方法,_occupied为1,cachebuckets中可以读取到当前被调用的对象方法sel和imp
  • 调用两次对象方法,_occupied为2,cachebuckets中可以读取到这两个对象方法的sel和imp

那调用对象方法时,是如何将方法存入cache中?

由于每一次调用,会对_occupied值进行加1,那就先从这个值着手。

  • 查看源码,对_occupied值进行加1的操作是在incrementOccupied函数中完成
void cache_t::incrementOccupied() 
{
    _occupied++;
}
  • 继续查找调用incrementOccupied这个函数的地方。发现只在cache_tinsert方法有调用。
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endif

    ASSERT(sel != 0 && cls->isInitialized());

    // Use the cache as-is if it is less than 3/4 full
    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 <= capacity / 4 * 3)) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    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 because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(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));

    cache_t::bad_cache(receiver, (SEL)sel, cls);
}

该方法就是cache插入,即向cache中插入sel、imp
接下来我们来分析一下这个方法。

  • 根据occupied的值计算出当前的缓存占用量,当属性未赋值或方法未调用时,occupied()为0。而newOccupied=occupied()+1,即newOccupied为1。
    当对属性进行操作时,会隐式的调用属性的set/get方法,occupied也会增加。
    当对方法进行调用时,occupied也会增加。
    当对对象的父类方法进行调用时,occupied也会增加。

  • 根据缓存占用量判断执行的操作。
    如果是第一次创建,则默认开辟4个

if (slowpath(isConstantEmptyCache())) { //小概率发生的 即当 occupied() = 0时,即创建缓存,创建属于小概率事件
    // Cache is read-only. Replace it.
    if (!capacity) capacity = INIT_CACHE_SIZE; //初始化时,capacity = 4(1<<2 -- 100)
    reallocate(oldCapacity, capacity, /* freeOld */false); //开辟空间
    //到目前为止,if的流程的操作都是初始化创建
}

如果缓存占用量小于等于3/4,则不作任何处理

else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { 
    // Cache is less than 3/4 full. Use it as-is.
}

如果缓存占用量超过3/4,则需要进行两倍扩容以及重新开辟空间

else {//如果超出了3/4,则需要扩容(两倍扩容)
    //扩容算法: 有cap时,扩容两倍,没有cap就初始化为4
    capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 2*4 = 8
    if (capacity > MAX_CACHE_SIZE) {
        capacity = MAX_CACHE_SIZE;
    }
    // 走到这里表示 曾经有,但是已经满了,需要重新梳理
    reallocate(oldCapacity, capacity, true);
    // 内存 扩容完毕
}
  • 针对需要存储的bucket进行内部impsel赋值
    这部分主要是根据cache_hash方法,即哈希算法 ,计算sel-imp存储哈希下标,分为以下三种情况
    • 如果哈希下标的位置未存储sel,即该下标位置获取sel等于0,此时将sel-imp存储进去,并将occupied占用大小加1。
    • 如果当前哈希下标存储的sel 等于即将插入的sel,则直接返回
    • 如果当前哈希下标存储的sel 不等于即将插入的sel,则重新经过cache_next方法即哈希冲突算法,重新进行哈希计算,得到新的下标,再去对比进行存储。

到此,cache_t的原理基本分析完成了。

接下来有几个问题为重点面试问题:

  1. bucket数据为什么会有丢失的情况?
    答:原因是在扩容时,是将原有的内存全部清除了,再重新申请了内存导致的。

  2. 为什么随着方法调用的增多,其打印的occupied 和 mask会变化?
    答:因为在cache初始化时,分配的空间是4个,随着方法调用的增多,当存储的sel-imp个数,即newOccupied + CACHE_END_MARKER的和超过总容量的3/4,例如有4个时,当occupied等于2时,就需要对cache的内存进行两倍扩容。

  3. say333、say444的打印顺序为什么是say444先打印,say333后打印,且还是挨着的,即顺序有问题?
    答:因为sel-imp的存储是通过哈希算法计算下标的,其计算的下标有可能已经存储了sel,所以又需要通过哈希冲突算法重新计算哈希下标,所以导致下标随机的,并不是固定的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354