OC中类的结构

498 阅读13分钟

Class的结构

研究类肯定得从NSObject说起

NSObject的底层转换成C++

struct NSObject_IMPL {
    Class isa;
};

然而Class是一个指向结构体的指针 typedef struct objc_class *Class;

所以要研究objc_class,就从OC源码中找:源码地址

全局搜索 objc_classruntime.h头文件中发现

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

注意:#if !__OBJC2__ 这句条件判断,条件不成立,所以不是此处!

objc-runtime-new.h头文件中发现struct objc_class,代码行数过多,此处做了删减

struct objc_class : objc_object {
    Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    //此处省略n多个方法。。。
}

objc_class中的isa

我们知道实例对象的isa指针指向类对象,类对象的isa指针指向元类对象;

但是这种说法不够严谨

应该说在arm64架构之前,isa就是一个普通的指针,存储着ClassMeta-Class 对象的内存地址; 但是从arm64之后,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存放跟多的信息。

位域的运用,使得isa能够在同样的内存空间下,存储更多的信息。

isa的结构

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

整理后(将宏替换,删除冗余):

union isa_t {
    uintptr_t bits;
private:
    Class cls;
public:
    struct {
        uintptr_t nonpointer        : 1;                                       
        uintptr_t has_assoc         : 1;                                       
        uintptr_t has_cxx_dtor      : 1;                                       
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ 
        uintptr_t magic             : 6;                                       
        uintptr_t weakly_referenced : 1;                                       
        uintptr_t unused            : 1;                                       
        uintptr_t has_sidetable_rc  : 1;                                       
        uintptr_t extra_rc          : 19
    };
};
  • 1、nonpointer:0,代表普通的指针,存储着Class、Meta-Class对象的内存地址;1,代表优化过,使用位域存储更多的信息
  • 2、has_assoc:是否有设置过关联对象,如果没有,释放时会更快,即使设置之后再清空,该位还是1
  • 3、has_cxx_dtor:是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
  • 4、shiftcls:存储着Class、Meta-Class对象的内存地址信息
  • 5、magic:用于在调试时分辨对象是否未完成初始化
  • 6、weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
  • 7、deallocating:对象是否正在释放
  • 8、extra_rc:里面存储的值是引用计数器
  • 9、has_sidetable_rc:引用计数器是否过大无法存储在isa中;如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

简单在真机上验证部分isa里面存储的信息:

@interface  Cat : NSObject

@end

@implementation Cat

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    Cat *cat = [[Cat alloc]init];
    
    查看isa
    (lldb) p/x cat->isa
    (Class) $0 = 0x000021a1026753f5 Cat
    (lldb) p cat
    (Cat *) $1 = 0x0000000280eaab60
}

增加关联对象,弱用用之后

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    Cat *cat = [[Cat alloc]init];
    
    //添加弱引用
    __weak Cat *weakCat = cat;
    weakCat = nil;
    //添加关联对象
    objc_setAssociatedObject(cat, "like", @"mouse", OBJC_ASSOCIATION_COPY_NONATOMIC);
    NSLog(@"cat = %p",cat);
    
    查看isa
    (lldb) p/x cat->isa
    (Class) $0 = 0x000025a1003d141f Cat
    (lldb) p cat
    (Cat *) $1 = 0x000000028065efc0
}

将最初的isa地址转成二进制:0x000021a1026753f5

00000000 00000000 00100001 10100001 00000010 01100111 01010011 11110101

将添加过关联对象和弱引用的isa地址转出二进制:0x000025a1003d141f

00000000 00000000 00100101 10100001 00000000 00111101 00010100 00011111

比较两个二进制地址的第0,位第1位,第42位:

第0位是1,说明这个指针使用位域优化过;

第1位是1,说明设置了关联对象;

第42位是1,说明有被弱引用指向过;

isa如何找到类对象、元类对象的?

isa结构体中的第一位,

nonpointer,如果为0,代表普通指针,直接存储这ClassMeta-Class对象的地址;

如果为1,代表优化的,使用了位域,想要得到类对象元类对象,需要&上一个ISA_MASK才能得到地址。

下面是ISA_MASK的宏定义,包含了arm64,x86架构的,以及arm64下的模拟器及真机

# if __arm64__
// ARM64 simulators have a larger address space, so use the ARM64e
// scheme even when simulators build for ARM64-not-e.
#   if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#     define ISA_MASK        0x007ffffffffffff8ULL
#   else
#     define ISA_MASK        0x0000000ffffffff8ULL
#   endif
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# else
#   error unknown architecture for packed isa
# endif

验证isa与上ISA_MASK的结果是什么?

 Cat *cat = [[Cat alloc]init];

//打印Cat的类对象地址
 NSLog(@"Cat = %p",[Cat class]); 

打印结果:Cat = 0x102bf93a0

再打印cat的isa指针地址:

p/x cat->isa
(Class) $0 = 0x000021a102bf93a1 Cat

利用计算器编程型,计算

0x000021a102bf93a1 & 0x0000000ffffffff8ULL = 0x102BF93A0

结果正好是Cat类对象的地址,说明isa指针优化后,需要与上ISA_MASK才能得到类对象地址。

objc_class中的 class_data_bits_t

在源码中查看 class_data_bits_t

struct class_data_bits_t {
   
   //由于源码过多,做了部分删减
    
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
 }

isa一样,bits 需要与上FAST_DATA_MASK,得到class_rw_t,这个结构体,存储着类的许多重要信息

#define FAST_DATA_MASK 0x00007ffffffffff8UL

class_rw_t的结构如下:

struct class_rw_t {
	
    //由于源码过多,做了部分删减
    
    uint32_t flags;
    uint16_t witness;
#if SUPPORT_INDEXED_ISA
    uint16_t index;
#endif

    Class firstSubclass;
    Class nextSiblingClass;

public:
    
    // class_ro_t 存放类的初始化信息(如类名、成员变量等)
    const class_ro_t *ro()
	
    //方法列表
    const method_array_t methods() 
    
    //属性列表
    const property_array_t properties() 
    
    //协议列表
    const protocol_array_t protocols()
};

其中class_ro_t的结构如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;

    //instance对象占用的空间
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    // 类名
    explicit_atomic<const char *> name;	
    void *baseMethodList;
    protocol_list_t * baseProtocols;
  
    //成员变量列表
    const ivar_list_t * ivars;
    const uint8_t * weakIvarLayout;
}

从源码可以看出:

  • Class类中有isa指针superClass指针cache方法缓存bits具体的类信息;
  • bits & FAST_DATA_MASK 指向一个新的结构体class_rw_t,里面包含着methods方法列表properties属性列表protocols协议列表class_ro_t类的初始化信息等一些类信息;
  • class_ro_t里面包含类名成员变量列表instance对象占用空间等初始化信息;

总体结构如下图:

补充说明: 事实上,在程序启动和初始化过程中,Class并不是这样的结构,这里的类的结构,是类其他操作(比如合并分类方法等)完成后的状态,可以理解成稳定状态下,Class的内部结构。

class_rw_t

Class_rw_t里面的methods方法列表properties属性列表都是二维数组,是可读可写的,包含类的初始内容分类的内容

方法列表、属性列表、协议列表的结构如下图:

method_t是方法的封装,稍后会解释。

补充一下: methods_array_t里面放着类的所有方法,包括分类的方法,也会合并到一起来。 在有了class_rw_t之后,便会进行category的处理,将Class本身的方法列表category里面的方法列表先后放到class_rw_tmethod_array_t methods里面,Class自身的方法列表会被最先放入其中,并且置于列表的尾部,category方法列表的加入顺序等同与category文件参与编译的顺序,位于class自身方法列表前面,所以分类的方法会先于自身方法调用。

class_ro_t

class_ro_t只读的,是Class本身的固有信息,可以理解成方法@interface@end之间的方法、属性等,最重要的还是存放类的成员变量信息的ivars,而且是被const修饰说明是不可修改的,这也就是为什么Runtime无法动态增加成员变量,底层结构决定的。

结构大致如图:

下面简单介绍下method_t

method_t

method_t是对方法的封装

struct method_t{
    struct big {
        SEL name;	//函数名	
        const char *types;	//编码(返回值类型,参数类型)
        MethodListIMP imp;	//指向函数的指针(函数地址)
    };
}
IMP 代表函数的具体实现

using MethodListIMP = IMP;

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),
第二个参数是方法选择器(selector)
SEL : typedef struct objc_selector *SEL;

SEL代表方法名,一般叫做选择器,底层结构跟char *类似

  • 可以通过@selector()sel_registerName()获得
  • 可以通过sel_getName()NSStringFromSelector()转成字符串
  • 不同类中相同名字的方法,所对应的方法的选择器是相同的
  • 具体实现typedef struct objc_selector *SEL
types

types包含了函数返回值,参数编码的字符串

结构为:返回值 参数1 参数2...参数N

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码

 "i24@0:8i16f20"

 0id 8SEL 16int 20float  == 24

- (int)test:(int)age height:(float)height

每一个方法都有两个默认参数self和_msg 我们可以查到id类型为@,SEL类型为:

1、第一个参数i返回值
2、第二个参数@ 是id 类型的self
3、第三个参数:是SEL 类型的_msg
4、第四个参数i 是Int age
5、第五个参数f 是float height

其中加载的数字其实是跟所占字节有关
124 总共占有多少字节
2、@0id 类型的self的起始位置为0
3、:8 是因为id 类型的self占字节为8,所以SEL 类型的_msg的起始位置为8

objc_class中的 cache_t

简单来说,他是用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度。

散列表

就从散列表来说起,首先散列表本质上就是一个数组,在往散列表里面添加成员的时候,首先需要借助key计算出一个index,然后再将元素插入散列表的index位置,取值就很方便了,根据一个key,计算出index,然后到散列表对应位置将值取出,即:f(key) = index

但是如何根据key来计算一个index,这种算法就很多,常用的有取余、平方等,还有散列表如何均匀分布节省空间,怎么扩容,这里不做细说。

大致如下图:

cache_t的结构如下:

由于源码中宏定义比较多,提前说明一下:

#if defined(__arm64__) && __LP64__     // 真机 并且是 64位
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elif defined(__arm64__) && !__LP64__     // 真机 并且是 不是64位
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED // 余下模拟器
#endif

//arm64 且 iOS 且非模拟器  且非Mac
#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST 
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif

#define CACHE_MASK_STORAGE_OUTLINED 1 模拟器或者macOS
#define CACHE_MASK_STORAGE_HIGH_16 2  真机64位
#define CACHE_MASK_STORAGE_LOW_4 3  非64位真机
#define CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 4

struct cache_t

struct cache_t {
  
//此处删减了部分源码

private:
    bool isConstantEmptyCache() const;
    bool canBeFreed() const;
    mask_t mask() const;	//这个值 = 散列表长度 - 1

    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);
    
public:
    unsigned capacity() const;
    struct bucket_t *buckets() const;   // 用来缓存方法的散列表
    Class cls() const;
    
    mask_t occupied() const;	     //表示已经缓存的方法的数量
    void initializeToEmpty();
    
    void insert(SEL sel, IMP imp, id receiver);    //核心方法,往散列表中插入缓存、缓存扩容等
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
    void destroy();
    void eraseNolock(const char *func);

    static void init();
    static void collectNolock(bool collectALot);
    static size_t bytesForCapacity(uint32_t cap);
};

容量capacity = mask + 1

unsigned cache_t::capacity() const
{
    return mask() ? mask()+1 : 0; 
}

再来看看 bucket_t是什么结构:

//此处删减了部分源码

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64. IMP-first对arm64比较友好
    // SEL-first is better for armv7* and i386 and x86_64.	SEL-first对armv7比较友好
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;

public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    // 获取sel
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }
    // 获取IMP
    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)
    
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

其实主要就是:selimp,缓存的是IMP

iOS中,索引值即数组的下标就是通过@selector(方法名)&_mask来求得,具体每一个数组的元素是一个结构体,里面包含两个元素_imp@selector(方法名)作为的key

// Class points to cache. SEL is key. Cache buckets store SEL+IMP. //SEL是key。缓存buckets存储SEL+IMP。
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
    return (mask_t)(value & mask);
}
  • 随着方法的增加,方法数量超过_mask值了怎么办 ?

    随着方法的增多,方法数量肯定会超过_mask,这个时候会清空缓存散列表,然后_mask*2,再满还是增加两倍,最后有个最大值2^16;

  • 如果两个值&_mask的值相同了怎么办 ?

    如果两个值&_mask的值相同时,第二个&减一,直到找到空值,如果减到0还没有找到空位置,那就放在最大位置;

  • 在没有存放cach_t的数组位置怎么处理?

    在没有占用时,会在空位置的值为NULL

下面介绍核心方法:insert

/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
    FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
    FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {//类要初始化完毕,否则直接返回
        return;
    }
    
    // Use the cache as-is if until we exceed our expected fill ratio.
    
    mask_t newOccupied = occupied() + 1;//已存储方法数量+1 
    unsigned oldCapacity = capacity(), capacity = oldCapacity;//旧容量
    if (slowpath(isConstantEmptyCache())) { //小几率执行 判断如果是创建 进行初始化
        // Cache is read-only. Replace it.
         // 设置初始容量,INIT_CACHE_SIZE = (1 << 2) (1左移两位)即初始为4
        if (!capacity) capacity = INIT_CACHE_SIZE;
        //reallocate方法会创建新的散列表,并释放旧的(如果有的话)
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    
    //判断是否需要扩容
    // CACHE_END_MARKER = 0 ???
    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.
        //缓存的容量小于3/4 或 7/8,不用做处理(扩容)
    }
    
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
        // 对于小bucket,允许100%的缓存利用率时,不用做处理
    }
    else {//直接扩容
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE; 
        //如果有空间就空间大小乘以2,没有空间就是INIT_CACHE_SIZE,1左移两位 = 4,
        if (capacity > MAX_CACHE_SIZE) {//最大值为 2^16 
            capacity = MAX_CACHE_SIZE;
        }
        //重新分配空间 ,此方法第三个参数就是是否释放原来空间
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets(); //新建一个bucket_t
    mask_t m = capacity - 1; // 数组的最大值 = 容量 - 1 (因为数组的下标从0开始)
    mask_t begin = cache_hash(sel, m);	//计算出首个的索引值
    mask_t i = begin; 

    //循环遍历,重新将所有的IMP放到散列表
    do {
        if (fastpath(b[i].sel() == 0)) { //通过下标去判断buckets里对应的位置是否有值,没值
            incrementOccupied(); //增加已缓存IMP的数量
            b[i].set<Atomic, Encoded>(b, sel, imp, cls()); //将imp缓存进散列表
            return;
        }
        if (b[i].sel() == 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));
    // 都不满足,重新hash计算,获取新的下标i
    //循环条件,i是否等于首个索引值,不是i=i-1,继续遍历,如果i=0 了,i = 数组的最大值m
## }

// 判断如果
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
缓存查找

既然散列表已经缓存了被调用过的方法,那么如何查找呢?

之前版本的源码中有find方法,但最新的源码中没有找到find 找到了一个相关的,cache_getImp,可惜是用汇编写的,不做介绍了

/********************************************************************
 * IMP cache_getImp(Class cls, SEL sel)
 *
 * On entry:    r0 = class whose cache is to be searched
 *              r1 = selector to search for
 *
 * If found, returns method implementation.
 * If not found, returns NULL.
 ********************************************************************/

	STATIC_ENTRY _cache_getImp

	mov	r9, r0
	CacheLookup NORMAL, _cache_getImp
	// cache hit, IMP in r12
	mov	r0, r12
	bx	lr			// return imp
	
	CacheLookup2 GETIMP, _cache_getImp
	// cache miss, return nil
	mov	r0, #0
	bx	lr

	END_ENTRY _cache_getImp

我们知道,当对象或类调用方法的时候,发送objc_msgSend消息,首先会去缓存找方法,调用cache_getImp,如果找到,就能得到imp,就可以直接调用了,同时会把该方法存储到自己类对象或元类对象的散列表里面;

总结一下:

  • Class的底层是一个指向结构体的指针,struct objc_class *;
  • objc_class包含isa指针superclass指针类的相关信息bits被调用过的方法缓存cache;
  • 优化后的isa指针存储着更多的信息,而优化前仅保存classmeta-class的地址;
  • bits存储着class_rw_t,class_rw_t里面有方法列表属性列表协议列表class_ro_t, class_ro_t里面存储着类名成员变量列表等;
  • cache里面利用散列表存储着曾经被调用过的方法;

以上若有错误,欢迎指正。转载请注明出处。