iOS底层原理探究07- 方法缓存cache

正题开始之前我们先来个开胃小菜巩固一下之前学习的内容

    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];       //
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];     //
    BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]];       //
    BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson 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)[LGPerson alloc] isKindOfClass:[LGPerson class]];       //
    BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]];     //
    NSLog(@" re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);

这段代码输出会是咋样呢


image.png

输出的结果是1 0 0 0 1 1 1 1下面的四个都是1这个应该没什么问题咱们平常开发中经常会对对象做这样的判断,但是上面的四个可能就有点问题了,类的判断不怎么做,那我们怎么理解这个结果呢?当然是看源码了 ,直接上源码

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

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

+ (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;
}

通过源码可以看到出

  • 实例方法的- (BOOL)isMemberOfClass:(Class)cls是直接判断实例的类跟cls相不相同,实例方法的- (BOOL)isKindOfClass:(Class)cls是比较实例的类和它的所有父类跟cls是否相同
  • 类方法的+ (BOOL)isMemberOfClass:(Class)cls是直接判断该类的元类跟cls相不相同,类方法的+ (BOOL)isKindOfClass:(Class)cls是比较类的元类和元类的所有父类跟cls是否相同
    这样来看输出的结果就好理解了。但是这里还有一个问题实际运行时是否真的调用的是isMemberOfClassisKindOfClass,下面我们来看下汇编
    image.png

    通过汇编可以看到isMemberOfClass:是正常调用的,但是isKindOfClass:变成了objc_opt_isKindOfClass这个方法的调用,接下来看下这个方法的源码
// 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);
}

其实objc_opt_isKindOfClass的核心代码也是判断obj的isa指向的类以及isa指向的类的所有父类是否与otherClass相同。

好了开胃小菜结束开始正餐

一、cache的数据结构探索

通过《iOS底层原理探究05》我们已经了解了类的数据结构

struct objc_class : objc_object {
    ...省略无关代码
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    
    ...省略无关代码
}

bits前面的两篇的探索中已经探究过了,今天我们来看下class里的cache的数据结构是怎样的,到底存了些啥?网上很多博客说cache是方法的缓存是不是这样呢?cache的数据结构又是咋样的呢?下面就来探索一下。
先来看下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; 
    };
    ...省略无关代码
   struct bucket_t *buckets() const;
}

主要就是一个联合体的结构下面在控制台打印一下看看实际存储的内容
cache 前面是ISA、superclass这两个指针类型的成员变量所以拿到类的地址往后平移16个字节(一个是8字节)也就是加上0x10


image.png

来尝试输出cache_t里存储的内容

image.png

分别输出 _bucketsAndMaybeMask、_maybeMask、_originalPreoptCache均没打印我们想要的内容,前面我们已经有了打印方法列表和属性列表的经验,一般这些数据结构都需要使用源码里提供的api打印,接下来看看cache_t中是否也有相应的api,通过上面的源码可以看到源码中提供了struct bucket_t *buckets() const;方法来获取buckets下面我们先来看下bucket_t的结构再在lldb中打印一下

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
...省略无关代码
inline SEL sel() const { return _sel.load(memory_order_relaxed); }
inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }
...省略无关代码
}

通过源码可以看到bucket_t中存储的是sel和imp,下面在控制台里打印试试


image.png

通过buckets()方法我们确实打印出了bucket,但是打印出来的数据Value是null,这是为啥呢?这个问题先留这个这儿,这里我们调用的buckets()既然它带个's'那是不是像数组一样存多个bucket呢,我们来试一下


image.png

我们去下标为1的元素,打印出来init方法的sel和imp,由此验证了cache里确实存储着方法的缓存。
但这里我们也产生了一些问题:
  • buckets的数据结构是咋样的
  • 缓存方法的时候开辟多大的空间,是怎么开辟的
  • 方法是怎么被存到缓存里去的
    我们仿照源码的数据结构自己定义一套结构方便我们继续探索
struct sp_bucket_t {//对应bucket_t 
    SEL _sel;
    IMP _imp;
};

struct sp_cache_t {//对应cache_t
    struct sp_bucket_t  *buckets;
    uint32_t            _maybeMask;
    uint16_t            _flags;
    uint16_t            _occupied;
};

struct sp_class_data_bits_t {//对应class_data_bits_t
    
    uintptr_t bits;
};

struct sp_objc_class{//对应objc_class
 
    Class ISA;
    Class superclass;
    sp_cache_t cache;
    sp_class_data_bits_t bits;

};

定义相应的数据结构之后main方法中就可以添加相应的代码数据cache存储的内容了

        struct sp_objc_class *sp_class = (__bridge struct sp_objc_class *)(pClass);
        
        for (uint32_t i = 0; i < sp_class->cache._maybeMask; i++) {
            struct sp_bucket_t bucket = sp_class->cache.buckets[I];
            NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"%hu - %u ",sp_class->cache._occupied,sp_class->cache._maybeMask);
  • for循环打印cache中存储的方法
  • 最后打印出缓存的方法个数和开辟的空间


    image.png
  • 当调用say1、say2两个方法时开辟了三个位置,并缓存了这两个方法,但是0号位置空出来了没有存东西
    在加个方法试试


    image.png
  • 当调用say1、say2、say3时,cache只缓存了say3,cache开辟了7个位置

lldb调试过程中我们遇到了一些问题

  • 开辟的空间和实际缓存的方法数并不一致
  • 缓存的方法并不是顺序存储的会有空位的情况
  • 当开辟的空间从3变为7的时候之前缓存的方法被清空了只剩了个say3
    带着这些问题我们来看源码

cache的插入方法

//入参是 需要缓存的方法的SEL IMP 和 方法接收者
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();//取出buckets
    mask_t m = capacity - 1;//计算maybemask
    mask_t begin = cache_hash(sel, m);//使用哈希算法计算插入的位置
    mask_t i = begin;//I表示插入位置

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {//如果插入位置是空的 则插入该方法(避免hash冲突)
            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));//如果i位置没有插入成功 通过cache_next找下一个可以插入的位置

    bad_cache(receiver, (SEL)sel);//如果while循环走完都找不到可以插入的位置则缓存失败
#endif // !DEBUG_TASK_THREADS
}

理一下缓存插入的大致的流程,过程中调用的方法在后面附上源码

  • newOccupied = occupied() + 1这里计算插入当前方法后缓存的方法总数 newOccupied是插入当前方法后的总数 首次进来的时候occupied()为0 ,newOccupied = 1
  • oldCapacity = capacity()获取之前缓存的空间大小,capacity = oldCapacity初始化新的空间大小等于老的capacity是新的空间大小
  • isConstantEmptyCache()判断缓存是否为空 首次insert的时候缓存是空的,进走这个流程
  • if (!capacity) capacity = INIT_CACHE_SIZE; 首次缓存capacity(需要开辟的空间)赋值为4 (INIT_CACHE_SIZE = 4 后面附上源码)
  • eallocate(oldCapacity, capacity, /* freeOld */false);开辟空间
  • fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))判断当前开辟的空间是否够用
  • capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;当前开辟的空间不够用的话进行两倍扩容
  • 下面就是插入缓存的流程了
  • bucket_t *b = buckets();先取出buckets
  • m = capacity - 1;mayBeMask设置成开辟的空间减一(这个后面会再看源码)
  • begin = cache_hash(sel, m);通过哈希算法计算插入的位置,这就是方法不是顺序存储的原因,方法存储位置的下标是通过hash算法计算得到的
  • 进入do-while循环把方法插入缓存fastpath(b[i].sel() == 0)判断当时位置是不是为空,为空则调用incrementOccupied()使_occupied + 1,通过b[i].set方法把方法插入缓存
  • b[i].sel() == sel如果之前已经缓存过该方法直接return
  • i = cache_next(i, m)如果前面的两个判断都为假则查找下一个可以插入的位置
  • bad_cache(receiver, (SEL)sel)如果do-while结束也没有找到可以插入的位置则缓存失败

下面来看下插入流程中调用的方法的源码实现

mask_t cache_t::occupied() const
{
    return _occupied;
}
  • 返回_occupied即已经缓存的方法数
unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

mask_t cache_t::mask() const
{
    return _maybeMask.load(memory_order_relaxed);
}
  • mask()返回_maybeMask即开辟的空间大小
  • capacity()如果_maybeMask有值返回_maybeMask + 1否则返回0,因为_maybeMask是等于开辟的空间减一 所以要加回来
bool cache_t::isConstantEmptyCache() const
{
    return
        occupied() == 0  &&
        buckets() == emptyBucketsForCapacity(capacity(), false);
}
  • 判断缓存是否为空
static inline mask_t cache_fill_ratio(mask_t capacity) {
    return capacity * 3 / 4;
}
  • 这个判断是否存满的算法是 判断有没有达到缓存的3/4,达到了就需要对缓存扩容了
struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
  • buckets是通过 _bucketsAndMaybeMask & bucketsMask得到的
  • static constexpr uintptr_t bucketsMask = ~0ul;bucketsMask等于对0取反就是所有二进制位都是1与上_bucketsAndMaybeMask之后得到的还是_bucketsAndMaybeMask本身
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);
}
  • cache_hash算法是通过sel的地址hash得到缓存插入位置的
  • 因为与上了m所以得到的位置不会大于m
  • m是外界传进来的 m = capacity - 1 ,而capacity被初始化成INIT_CACHE_SIZE又通过INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)INIT_CACHE_SIZE_LOG2 = 2得到INIT_CACHE_SIZE = 4m= 3 所以我们在lldb中输出的时候得到1 - 3的打印
    扩容之后因为是双倍扩容capacity变成了8 ,m变成7所以扩容后打印的是1 - 7
void cache_t::incrementOccupied() 
{
    _occupied++;
}
  • 这个方法比较简单就是_occupied自增1
    未完待续
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容