Class的结构
研究类肯定得从NSObject
说起
NSObject
的底层转换成C++
时
struct NSObject_IMPL {
Class isa;
};
然而Class是一个指向结构体的指针
typedef struct objc_class *Class;
所以要研究objc_class
,就从OC源码中找:源码地址
全局搜索 objc_class
在runtime.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就是一个普通的指针,存储着Class
、 Meta-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,代表普通指针,直接存储这Class
、Meta-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_t
的method_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
其中加载的数字其实是跟所占字节有关
1、24 总共占有多少字节
2、@0 是id 类型的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);
};
其实主要就是:sel
和imp
,缓存的是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指针
存储着更多的信息,而优化前仅保存class
或meta-class
的地址; bits
存储着class_rw_t
,class_rw_t
里面有方法列表
、属性列表
、协议列表
和class_ro_t
,class_ro_t
里面存储着类名
、成员变量列表
等;cache
里面利用散列表存储着曾经被调用过的方法;
以上若有错误,欢迎指正。转载请注明出处。