iOS底层原理09:类结构分析——cache属性

2021-06-29  本文已影响0人  黑白森林无间道

在前面的文章中,我们探索了isasuperclassbits属性
iOS底层原理07:类 & 类结构分析
iOS底层原理08:类结构分析——bits属性
本文主要探索cache的结构和底层原理

1、探索cache的数据结构

cache的类型是cache_t结构体

1.1、cache_t结构体

👇来看看objc4-818源码中cache_t结构体

typedef unsigned long           uintptr_t;

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

struct cache_t {
private:
    // explicit_atomic 显示原子性,目的是为了能够 保证 增删改查时 线程的安全性
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //8字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; //4字节
#if __LP64__
            uint16_t                   _flags; //2字节
#endif
            uint16_t                   _occupied;//2字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字节
    };
    
    //下面是一些static属性和方法,并不影响结构体的内存大小,主要是因为static类型的属性 不存在结构体的内存中
    /*
     #if defined(__arm64__) && __LP64__
     #if TARGET_OS_OSX || TARGET_OS_SIMULATOR
     // macOS 或 __arm64__的模拟器
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
     #else
     //__arm64__的真机
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
     #endif
     #elif defined(__arm64__) && !__LP64__
     //32位 真机
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
     #else
     //macOS 模拟器
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
     #endif
     ******  中间是不同的架构之间的判断 主要是用来不同类型 mask 和 buckets 的掩码
    */
        
    // ...省略代码
    // 下面是几个比较重要的方法
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);

    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
    
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    
    void insert(SEL sel, IMP imp, id receiver);
    /// 快速计算对象内存大小,16字节对齐,在对象的alloc中我们已经分析过了
    size_t fastInstanceSize(size_t extra) const {...}
    
    // ...省略代码
}

cache_t是结构体类型,有两个成员变量:_bucketsAndMaybeMask和一个联合体

所以cache_t的大小等于 8+8或者8+4+2+2,即16字节

struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
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;

   //省略代码。。。。
}

1.2、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__  // arm64架构
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else  // 其他架构
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

    // ...省略代码
}

1.3、lldb调试验证

image image image image image

通过源码lldb调试,可以发现 cache存储的 方法缓存

2、根据源码,对类和cache进行仿写

为什么需要进行代码仿写呢?

2.1、准备工作

/*** HTPerson.h ***/
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface HTPerson : NSObject

- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)sayHappy;

@end

NS_ASSUME_NONNULL_END

/*** HTPerson.m ***/
#import "HTPerson.h"

@implementation HTPerson

- (void)say1{
    NSLog(@"%s",__func__);
}
- (void)say2{
    NSLog(@"%s",__func__);
}
- (void)say3{
    NSLog(@"%s",__func__);
}
- (void)say4{
    NSLog(@"%s",__func__);
}
- (void)say5{
    NSLog(@"%s",__func__);
}
- (void)say6{
    NSLog(@"%s",__func__);
}
- (void)say7{
    NSLog(@"%s",__func__);
}

+ (void)sayHappy{
    NSLog(@"%s",__func__);
}

@end
#import <Foundation/Foundation.h>
#import "HTPerson.h"

typedef uint32_t mask_t;
// bucket_t结构体
struct ht_bucket_t {
    SEL _sel;
    IMP _imp;
};

// cache_t结构体
struct ht_cache_t {
    struct ht_bucket_t * _buckets; //8字节
    mask_t _maybeMask; //4字节
    uint16_t _flags; //2字节
    uint16_t _occupied;//2字节
};

// 类结构体
struct ht_objc_class {
    Class isa;  // 8字节
    Class superclass; //8字节
    struct ht_cache_t cache;    //16字节
    uintptr_t  bits;    // 8字节
};


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson *p = [HTPerson alloc];
        Class pClass = p.class;
        
//        [p say1];
//        [p say2];
//        [p say3];
//        [p say4];
//        [p say5];
//        [p say6];
//        [p say1];
//        [p say2];
        
        struct ht_objc_class * ht_class = (__bridge struct ht_objc_class *)(pClass);
        NSLog(@"- %hu - %u", ht_class->cache._occupied, ht_class->cache._maybeMask);

        for (int i = 0; i < ht_class->cache._maybeMask; i++) {
            struct ht_bucket_t bucket = ht_class->cache._buckets[I];
            NSLog(@"%@ - %pf", NSStringFromSelector(bucket._sel), bucket._imp);
        }
    }
    return 0;
}

2.2、对象方法的调用 与 cache值的关系

如果对象方法都没有调用,则cache不会进行方法缓存,此时_occupied_maybeMask的值都为0

image image image

【问题】 这里就产生了几个疑问?

_occupied_maybeMask是什么?在什么地方赋值,只能去objc源码中找答案。我们要缓存方法,首先看怎么把方法插入到bukets中的。带着这些疑问继续探讨cache_t源码

3、cache_t源码探究

image

从这个插入的方法来看,插入的参数有selimp、还有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);
#endif // !DEBUG_TASK_THREADS
}

计算当前所占容量

image

开辟容量

image

reallocate方法探究

image

reallocate 方法主要做三件事

allocateBuckets方法(开辟内存)探究

image

allocateBuckets 方法主要做两件事

setBucketsAndMask 方法探究

image

setBucketsAndMask方法主要用来赋值

collect_free 方法探究

image

二倍扩容

image

方法缓存

image

方法缓存写入方法 set

image

set方法:将impsel写入bucket

insert方法调用流程

前面探究了insert方法的源码实现,接下来我们探究insert方法调用流程,是如何从调用实例方法走到cache里面的insert方法的?

image

从堆栈信息可以看出insert的调用流程:_objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert

【问题】 _objc_msgSend_uncached方法又是何时调用的呢?

image

我们发现:objc_msgSend方法会调用_objc_msgSend_uncached,至此整个流程就串联起来了

上一篇下一篇

猜你喜欢

热点阅读