iOS开发:类class的底层实现原理

NSObject底层的实现objc_class,其初始化即对应底层一个objc_class的一个初始化。那么objc_class到底长什么样呢?

注意事项:本部分仅摘录了部分核心代码以节省篇幅。

一、objc_class基础结构

简化代码后如下

typedef struct objc_class *Class;

struct objc_object {
    //实例对象的isa指向类对象;类对象的isa指向元类;元类的isa指向根元类(根元类的isa指向自己)
    Class isa;
};

struct objc_class : objc_object {
    //类对象的superclass指向父类(根类的superclass指向nil);元类的superclass指向父元类(根元类的superclass指向根类)
    Class superclass;
    //缓存等相关信息
    cache_t cache;
    //类对象、元类对象的信息
    class_data_bits_t bits; 
};

因为objc_class继承自objc_object,所以objc_class也至少包括isasuperclasscachebits等4个成员变量。其中:
1.类对象的isa指向元类;元类的isa指向根元类(根元类的isa指向自己)。
2.类对象的superclass指向父类(根类的superclass指向nil);元类的superclass指向父元类(根元类的superclass指向根类)。
3.cache记录缓存等相关信息。
4.bits保存了类对象、元类对象的信息。
对于objc_class的主要结构体关系图如下:

objc_class结构体关系图.png

在开始探索之前,我们定义一个包含属性、成员变量、方法、协议(协议方法)的最小的类,示例代码如下,如下相关探索基本使用这个类展开:

@protocol NXPersonDelegate 
- (void)eat;
@end

@interface NXPerson : NSObject{
    NSInteger _uuid;
}
@property (nonatomic, copy) NSString *name;
- (void)run;
@end

@implementation NXPerson
- (void)run{
    NSLog(@"%s",__func__);
}
- (void)eat{
    NSLog(@"%s",__func__);
}
@end

二、class_data_bits_t、class_rw_t、class_ro_t的探索

简化后的class_data_bits_t如下:

struct class_data_bits_t {
    uintptr_t bits;        //8
}

class_data_bits_t从定义上看包含bits,占用8字节内存。bits&FAST_DATA_MASK得到了class_rw_tclass_rw_ext_tclass_ro_t中有我们熟悉的内容methodsproperties等。class_ro_t即所谓的clean memory(后期是不会发生修改的),存放了编译器就已经确定的属性、方法、协议、变量,里面没有分类的方法。而在运行时会创建class_rw_t即所谓的dirty memory(随着程序的运行会进行一些增删),加载class的时候首先会吧class_ro_t中的属性、方法、协议拷贝到class_class_ext_t中,然后将当前分类中的再拷贝进去,运行时方法、属性的查找也都是查找的class_class_ext_t中的。

如上示例类我们定义了1个成员变量,1个属性,1个普通方法、1个协议方法、1个协议。根据属性=成员变量+set方法+get方法的原则,我们最终要找到2个成员变量(_uuid, _name)、1个属性(name)、4个方法(run, getName,setName:,eat)。

class_ro_t.png

通过如上方式我们获取到类对象的class_ro_t结构体对应的变量$5。接下来可以通过*$5.ivars*$5.baseProperties*$5.baseProtocols*(method_list_t *)$5.baseMethods() 获取到ro中的成员变量、属性、协议、方法。

class_ro_t.ivars.png

$6中可以看到成员变量数量为2,打印得到_uuid_name

class_ro_t.baseProperties.png

$9中可以看到成员变量数量为5,打印得到namehashsuperclassdescriptiondebugDescription,包含了自定义的name

class_ro_t.baseProtocols.png

$11中可以看到协议数量为1,打印得到NXPersonDelegate

class_ro_t.baseMethods.png

$14中可以看到方法数量为4,包括eatrunnamesetName:

同理我们可以通过rw.methods()、rw.properties()、rw.protocols()获取到class_rw_ext_t中存储的methodspropertiesprotocols列表。调试方法与以上类似,在此不再重复。

三、cache_t的探索

简化后的cache_t如下:

struct cache_t {
    explicit_atomic _bucketsAndMaybeMask;//8字节
    union {
        struct {
            explicit_atomic    _maybeMask;//4字节
            uint16_t                   _flags;                          //2字节
            uint16_t                   _occupied;                  //2字节
        };
        explicit_atomic _originalPreoptCache;//8字节
    };
}

cache_t从定义上看包含_bucketsAndMaybeMask_maybeMask+_flags+_occupied_originalPreoptCache构成的联合体,占用16字节的内存。

通过官方源码中给的一段注释:_bucketsAndMaybeMask is a buckets_t pointer, _maybeMask is the buckets mask,我们推断这个指针指向的应该就是bucket_t内容,这个内容通过通过struct bucket_t *buckets() const;获得。

我们通过lldb命令看一下cache_t的信息:

cache_t.png

拿到cache_t后我们通过buckets()获取一下buckets:

buckets.png

根据打印的结果,可以看出$2.buckets()[1]中打印的_sel是有内容的。那么他这个规律是什么样的呢?插入的策略是什么呢?源码中可能跟这个相关的是void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)方法,我们点进去看一下:

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask){
    ...
    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);
    _maybeMask.store(newMask, memory_order_release);
    ...
}

这段代码主要就是如上2行代码,就是把newBuckets和newMask写入_bucketsAndMaybeMask_maybeMask的操作。搜索是哪里调用了setBucketsAndMask(...),最终定位到:

ALWAYS_INLINE void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld){
    bucket_t *oldBuckets = buckets();//获取当前的buckets
    bucket_t *newBuckets = allocateBuckets(newCapacity);//按照新的大小区开辟内存

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);//设置新的buckets和_maybeMask。这里-1!!
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);//回收旧的内存
    }
}

这一段代码主要就是先保存当前的buckets的地址,然后按照新的大小开辟内存,最终将新的内存和大小(newCapacity-1)通过setBucketsAndMask设置给_bucketsAndMaybeMask_maybeMask,如果需要释旧的内存空间则调用collect_free回收旧的内存空间。搜索是哪里调用了reallocate(...),最终定位到:

//本段代码是完整代码
void cache_t::insert(SEL sel, IMP imp, id receiver){
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        return;
    }

    if (isConstantOptimizedCache()) {
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s", cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);
#endif

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

    // 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/*0*/, capacity/*4*/, /* 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(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
}

找到这个方法,有种发现太空奥秘的感觉,void cache_t::insert(SEL sel, IMP imp, id receiver)函数名的意思就是插入sel+imp,是它是它,准没错了!!!!!
我们分析一下他的流程(前置条件的判断这里就省略了):

  • 1.1第一次进来occupied()=0,则newOccupied = 1_maybeMask=0,则oldCapacity=0,capacity=0;
  • 1.2.进入if(isConstantEmptyCache){}分支,capacity=0,则capacity=INIT_CACHE_SIZE=4,接着调用reallocate(oldCapacity/*0*/, capacity/*4*/, /* freeOld */false)进行内存空间的开辟。根据上面的分析此时buckets分配的是4 * bucket_t的大小,_maybeMask = 3
  • 1.3.获取当前的buckets,bucket_t *b = buckets();, b = 4 * bucket_t的大小。mask_t m = capacity - 1;,则m=3mask_t begin = cache_hash(sel, m);是对sel进行hash,这个函数内部计算hash最后一步是value & mask,也就是value&0B11,也就是最终的结果(真是mask=3)只能是0123这4种情况。这里假定是2则begin=2,I=2
  • 1.4.进入do-while循环:检查buckets的索引为2的位置(从0开始)的sel是不是为空,如果为空执行incrementOccupied();_occupied=1,并且执行b[i].set(b, sel, imp, cls());向索引的2的位置写入selimp。第一次进来这个条件肯定是满足的。
  • 2.1 非第一次进来(假定第二次):newOccupied=2,oldCapacity=4capacity = 4
  • 2.2 进入if(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity)){}分支,不做事情
  • 2.3 m=4, 初始的i = begin = selhash值。(i in [0,1,2,3])。
  • 2.4 进入do-while循环:此时如果hash!=2,则按照第一的方式存储进去了(除了2号位置,其他位置没内容),如果找到的位置的sel跟当前的sel相同说明是相同的方法被调用了,不同重复缓存,直接跳出循环。反之则是hash冲突了。则执行cache_next()重新计算hash。(重新计算hash不同平台不一样,__arm64__架构下入锅之前的hash>=1,则进行-1操作,如果之前为0,则直接返回mask;其他则(hash+1)&mask)。找到新的hash赋值给i,然后继续尝试插入。如果不满足条件则继续寻找,直到又一次找到最初的位置,则推出while循环,打印bad_cache()log信息。假定插入成功插在1号位置。
  • 3.1 非第一次进入(假定第三次):newOccupied=3,oldCapacity=4, capacity = 4
  • 3.2 进入else{}分支:capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;也就是在上一次的基础上翻倍了,且会检查capacity不能大于MAX_CACHE_SIZE。(其中MAX_CACHE_SIZE=0B1000 0000 0000 0000=0A32768)。然后调用reallocate(oldCapacity/*4*/, capacity/*8*/, /* freeOld */true)开辟新的内存。这里需要注意的是freeOld=true,也就是要把上一次保留的2个给他干掉了,也就是如果不调用之前的方法则缓存中就暂时没有了,后续如果有调用则会继续加入缓存。
  • 3.3 m=8, 初始的i = begin = selhash值。(i in [0,1,2,3,4,5,6,7])。
  • 3.4 按照上面的规则查找0-7号合适的位置进行插入。
  • ...

根据上面的规律,我们大概可以总结出:
1.申请开辟的内存是4、8、16... 等等 2的N次方,重新申请的是上一次的2倍空间,并且最大不能大于0A32768。源码中有标注Initial cache bucket count. INIT_CACHE_SIZE must be a power of two.
2.每次开辟内存空间的时候会把地址给_bucketsAndMaybeMask,把mask=newCapacity-1_maybeMask。并且每次重新开辟内存后也会把旧的buckets的空间回收。
3.cache只会缓存方法,缓存方法时会对方法进行hash,如果相应的位置上可以插入则直接插入,如果这个位置上有相同的方法则不再重入插入,如果插入的位置有内容(hash冲突),则通过cache_next重新计算hash,满足则插入,如果知道重新计算的hash跟最初的hash想等了则跳出循环。那么该方法就暂时缓存不了。

在源码环境做这些数据的调试很不方便,很多api是私有的我们我们只能通过地址区读取,中间只要一步错了,就需要从头再来,如果我们把一个类对应的结构体转换成我们自己定义的结构体是否可行呢?
第一步:仿照源码结构重新定义objc_class结构

struct NX_bucket {
    SEL _sel;
    IMP _imp;
};
struct NX_cache {
    struct NX_bucket *_buckets;
    uint32_t    _maybeMask;
    uint16_t    _flags;
    uint16_t    _occupied;
};
struct NX_bits {
    uintptr_t bits;
};
struct NX_objc_class {
    Class isa;
    Class superclass;
    struct NX_cache cache;
    struct NX_bits bits;
};

第二步:在原有的NXPerson的基础上增加了func1-func7共计7个方法:

@protocol NXPersonDelegate 
- (void)eat;
@end
@interface NXPerson : NSObject {
NSInteger _uuid;
}
@property (nonatomic, copy) NSString *name;
- (void)run;
- (void)func1;
- (void)func2;
- (void)func3;
- (void)func4;
- (void)func5;
- (void)func6;
- (void)func7;
@end
@implementation NXPerson
- (void)eat{ NSLog(@"%s", __func__);}
- (void)run{NSLog(@"%s", __func__);}
- (void)func1{NSLog(@"%s", __func__);}
- (void)func2{NSLog(@"%s", __func__);}
- (void)func3{NSLog(@"%s", __func__);}
- (void)func4{NSLog(@"%s", __func__);}
- (void)func5{NSLog(@"%s", __func__);}
- (void)func6{NSLog(@"%s", __func__);}
- (void)func7{NSLog(@"%s", __func__);}
@end

第三部:编写测试代码:

- (void)test{
    //将类对象强制转换成我们自定义的结构体。
    struct NX_objc_class *__class = (__bridge  struct NX_objc_class *)[NXPerson class];
    //创建一个实例对象,后面会使用该实例对象调用实例方法。这里没有执行init方法,是为了排除init方法的干扰,init也会进行缓存。
    NXPerson *p = [NXPerson alloc];
    //顺序调用如下方法,每调用一个方法都打印一下当前的_maybeMask(当前开辟的内存容量大小)和_occupied(已占用的个数)
    NSArray *funcs = @[@"func1", @"func2", @"func3", @"func4", @"func5", @"func6", @"func7", @"func8"];
    for(int i = 0; i < funcs.count; i++){
        NSString *func = funcs[i];
        //调用方法。这里没有使用performSelector方法,是为了排除performSelector的干扰,performSelector也会进行缓存。
        //这里需要把Build Settings中Enable Strict Checking of objc_msgSend Calls 设置为NO。
        objc_msgSend(p, NSSelectorFromString(func));
        //打印_maybeMask、_occupied
        NSLog(@"\n\nfunc=%@;maybeMask=%d;occupied=%d", func, __class->cache._maybeMask, __class->cache._occupied);
        //一次打印_buckets中的信息
        for (int i = 0; i < __class->cache._maybeMask; i++){
            struct NX_bucket bucket = __class->cache._buckets[i];
            NSLog(@"index:%d,sel=%@,imp=%p", i,NSStringFromSelector(bucket._sel), bucket._imp);
        }
    }
}

打印结果较多,我们对打印的结果进行整理如下:

调用函数 _maybeMask和_occupied _buckets 结果分析
func1 _maybeMask=3;_occupied=1 index:0,_sel=(null),_imp=0x0
index:1,_sel=(null),_imp=0x0
index:2,_sel=func1,_imp=0xcfc0
新开内存为4,func1缓存在2号位置
func2 _maybeMask=3;_occupied=2 index:0,_sel=func2,_imp=0xcf90
index:1,_sel=(null),_imp=0x0
index:2,_sel=func1,_imp=0xcfc0
func1缓存在1号位置
func3 _maybeMask=7;_occupied=1 index:0,_sel=(null),_imp=0x0
index:1,_sel=(null),_imp=0x0
index:2,_sel=(null),_imp=0x0
index:3,_sel=(null),_imp=0x0
index:4,_sel=(null),_imp=0x0
index:5,_sel=(null),_imp=0x0
index:6,_sel=func3,_imp=0xcf60
2+1+1>4*3/4=>新开内存为8,func3缓存在6号位置
func4 _maybeMask=7;_occupied=2 index:0,_sel=(null),_imp=0x0
index:1,_sel=(null),_imp=0x0
index:2,_sel=(null),_imp=0x0
index:3,_sel=(null),_imp=0x0
index:4,_sel=func4,_imp=0xcf30
index:5,_sel=(null),_imp=0x0
index:6,_sel=func3,_imp=0xcf60
func4缓存在4号位置
func5 _maybeMask=7;_occupied=3 index:0,_sel=(null),_imp=0x0
index:1,_sel=(null),_imp=0x0
index:2,_sel=func5,_imp=0xcf00
index:3,_sel=(null),_imp=0x0
index:4,_sel=func4,_imp=0xcf30
index:5,_sel=(null),_imp=0x0
index:6,_sel=func3,_imp=0xcf60
func5缓存在2号位置
func6 _maybeMask=7;_occupied=4 index:0,_sel=func6,_imp=0xced0
index:1,_sel=(null),_imp=0x0
index:2,_sel=func5,_imp=0xcf00
index:3,_sel=(null),_imp=0x0
index:4,_sel=func4,_imp=0xcf30
index:5,_sel=(null),_imp=0x0
index:6,_sel=func3,_imp=0xcf60
func6缓存在0号位置
func7 _maybeMask=7;_occupied=5 index:0,_sel=func6,_imp=0xced0
index:1,_sel=func7,_imp=0xcea0
index:2,_sel=func5,_imp=0xcf00
index:3,_sel=(null),_imp=0x0
index:4,_sel=func4,_imp=0xcf30
index:5,_sel=(null),_imp=0x0
index:6,_sel=func3,_imp=0xcf60
func7缓存在1号位置
func8 _maybeMask=15;_occupied=1 index:0,_sel=(null),_imp=0x0
index:1,_sel=(null),_imp=0x0
index:2,_sel=(null),_imp=0x0
index:3,_sel=(null),_imp=0x0
index:4,_sel=func8,_imp=0xce70
index:5,_sel=(null),_imp=0x0
index:6,_sel=(null),_imp=0x0
index:7,_sel=(null),_imp=0x0
index:8,_sel=(null),_imp=0x0
index:9,_sel=(null),_imp=0x0
index:10,_sel=(null),_imp=0x0
index:11,_sel=(null),_imp=0x0
index:12,_sel=(null),_imp=0x0
index:13,_sel=(null),_imp=0x0
index:14,_sel=(null),_imp=0x0
5+1+1>8*3/4=>新开内存为16,func8缓存在4号位置

通过对测试数据的整理,我们更加深刻的明白底层扩容的实在容量即将达到3/4的时候进行扩容的,扩容后在新的容器中插入缓存数据,老得数据就丢弃了,这样也使得使用频率高的方法更容易被缓存下来,也是一种很好的淘汰机制啊!!

你可能感兴趣的:(iOS开发:类class的底层实现原理)