从runtime源码中看到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
class_rw_t *data() {
return bits.data();
}
......
}
// bits.data();
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
分别解释下几个字段
- superclass:指向父类的指针
- cache:调用过的方法缓存
- bits:用于获取具体的类信息
- class_rw_t:类具体信息的结构体,可以看到是由bits & FAST_DATA_MASK得到
接着看看class_rw_t包含哪些信息,class_rw_t结构如下:
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //类的初始信息
method_array_t methods; //方法列表
property_array_t properties; //属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
......
};
class_rw_t中包含了方法、属性、协议等,还有个ro,这个ro指向一个class_ro_t对象,class_ro_t里面包含了类初始的信息,是只读的。
class_ro_t的结构如下:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //instance对象占用的内存空间大小
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name; //类名
method_list_t * baseMethodList; //初始方法列表
protocol_list_t * baseProtocols; //初始协议列表
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; //初始协议列表
method_list_t *baseMethods() const {
return baseMethodList;
}
};
class_rw_t中methods、properties、protocols是二维数组,是可读可写的,包含类初始以及分类的方法、属性、协议。最开始是没有class_rw_t,class_rw_t是在运行时创建的,并将class_ro_t的内容和分类的内容添加进来;
上面的结论可以从下面runtime的源码中看出,删减了部分代码,只保留了上述流程:
/***********************************************************************
* realizeClass
* Performs first-time initialization on class cls,
* including allocating its read-write data.
* Returns the real class structure for the class.
* Locking: runtimeLock must be write-locked by the caller
**********************************************************************/
static Class realizeClass(Class cls)
{
runtimeLock.assertWriting();
const class_ro_t *ro;
class_rw_t *rw;
Class supercls;
Class metacls;
bool isMeta;
if (!cls) return nil;
if (cls->isRealized()) return cls;
assert(cls == remapClass(cls));
ro = (const class_ro_t *)cls->data(); //开始时bits是指向ro的
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw); // 创建完rw将ro赋值给rw的ro,并将rw赋值给cls的bits
}
// Attach categories
methodizeClass(cls);
return cls;
}
从上面源码注释可以看出这个函数是类第一次初始化时执行,最初是没有rw的,class的bits是指向ro的。rw创建完将ro赋值给rw的ro,并将rw赋值给cls的bits,最后从注释可以看出是处理分类的内容;
函数methodizeClass源码如下:
static void methodizeClass(Class cls)
{
runtimeLock.assertWriting();
bool isMeta = cls->isMetaClass();
auto rw = cls->data();
auto ro = rw->ro;
// Methodizing for the first time
if (PrintConnecting) {
_objc_inform("CLASS: methodizing class '%s' %s",
cls->nameForLogging(), isMeta ? "(meta)" : "");
}
// Install methods and properties that the class implements itself.
method_list_t *list = ro->baseMethods();
if (list) {
prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
rw->methods.attachLists(&list, 1);
}
property_list_t *proplist = ro->baseProperties;
if (proplist) {
rw->properties.attachLists(&proplist, 1);
}
protocol_list_t *protolist = ro->baseProtocols;
if (protolist) {
rw->protocols.attachLists(&protolist, 1);
}
// Root classes get bonus method implementations if they don't have
// them already. These apply before category replacements.
if (cls->isRootMetaclass()) {
// root metaclass
addMethod(cls, SEL_initialize, (IMP)&objc_noop_imp, "", NO);
}
// Attach categories.
category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
attachCategories(cls, cats, false /*don't flush caches*/);
}
从上述源码中可以看出,从rw找到ro取出里面的baseMethods、baseProperties、baseProtocols,添加到rw对应的methods、properties、protocols中。最后取出未添加过的分类内容添加进来。关于怎么将分类方法添加进来的,即attachCategories函数的具体实现,请查看About Category。
方法的缓存
方法的调用如果每次都去类、父类、元类中一层层查找效率是比较低,所以runtime中对调用过的方法进行了缓存,放在类的cache中;
讲方法缓存前先来了解下方法的底层结构:
struct method_t {
SEL name; //函数名
const char *types; //编码(返回值类型、参数类型)
IMP imp; //指向函数的指针(函数地址)
};
- imp:函数的具体实现
typedef id (*IMP)(id, SEL, ...);
- SEL:代表方法\函数名,也叫方法选址器,底层结构跟char *类似;
- types:包含返回值和参数编码的字符串
iOS提供了@encode的指令,可将具体的类型表示成字符串编码,见下表(部分code)
code | Meaning |
---|---|
c | A char |
i | An int |
s | A short |
l | A long |
q | A long long |
c | An unsigned char |
I | An unsigned int |
S | An unsigned short |
L | An unsigned long |
Q | An unsigned long long |
f | A float |
d | A double |
B | A C++ bool or a C99 _Bool |
V | A void |
* | A charactor string(char *) |
@ | An object(whether statically typed or typed id) |
: | A method selector(SEL) |
^type | A pointer to type |
举个例子:types = "i20@0:8i16",OC中方法默认会传入id类型的self和SEL ,代表的含义如下
返回值类型 | 参数总长度 | 参数1类型及开始位置 | 参数2类型及开始位置 | 参数3 类型及开始位置 |
---|---|---|---|---|
int类型 | 20 | id 类型 | SEL | int类型 |
先来看下缓存cache_t的结构:
struct cache_t {
struct bucket_t *_buckets; //哈希表,存储调用方法
mask_t _mask; // 哈希表长度 - 1
mask_t _occupied; //已经缓存的数量
}
struct bucket_t {
cache_key_t _key; // SEL作为key
IMP _imp; //函数的内存地址
}
Class的方法缓存(cache_t)是用哈希表实现的,可以提高方法的查找效率
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
一般哈希表是通过目标key &或者%上一个值,得到一个哈希表位置的下标;类方法缓存是用方法SEL作为key & _mask得到一个位置下标,然后将方法地址存入哈希表;哈希表下标从0开始,最大为哈希表长度减一,这也是_mask大小为哈希表长度减一的原因;
下面是方法缓存函数的源码:
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
cacheUpdateLock.assertLocked();
// Never cache before +initialize is done
if (!cls->isInitialized()) return;
// Make sure the entry wasn't added to the cache by some other thread
// before we grabbed the cacheUpdateLock.
if (cache_getImp(cls, sel)) return;
cache_t *cache = getCache(cls);
cache_key_t key = getKey(sel);
// Use the cache as-is if it is less than 3/4 full
mask_t newOccupied = cache->occupied() + 1;
mask_t capacity = cache->capacity();
if (cache->isConstantEmptyCache()) {
// Cache is read-only. Replace it.
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// Cache is less than 3/4 full. Use it as-is.
// 说明当缓存达到散列表的3/4时就会扩容
}
else {
// Cache is too full. Expand it.
cache->expand();
}
// Scan for the first unused slot and insert there.
// There is guaranteed to be an empty slot because the
// minimum size is 4 and we resized at 3/4 full.
//找到第一个可用的位置,插入。表最小长度为4并且在达到3/4容量时扩容,确保表一定有可用的位置
bucket_t *bucket = cache->find(key, receiver);
if (bucket->key() == 0) cache->incrementOccupied();// 表中没存过该方法_occupied自增
bucket->set(key, imp);
}
从上述源码中可以看出散列表最小长度为4并且在达到3/4容量时扩容,确保表一定有可以插入的位置。插入前,查找表中是否已经存储过该方法,没存过_occupied加一。
现在看下查找缓存方法的函数:
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);
bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m); //key & mask得到的下标
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
// arm64架构下的cache_next
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
因为不同key & mask得到的下标可能相同,所以通过key & mask得到初始下标后,拿到散列表下标对于的bucket对象取出key与目标key比较,相等表示就是我们想要的bucket。不同就循环查看i - 1的位置,当i=0时查看mask位置的bucket是否是想要的;如果遍历完之后还没找到正确的,做一些错误处理,具体查找cache_t::bad_cache函数查看。