目录
动态性
Objective-C
是一门动态性比较强的编程语言,跟C
、C++
等语言有着很大的不同,其动态性是由Runtime API
来支撑的
Runtime API
提供的接口基本都是C语言
的,源码由C\C++\汇编语言
编写
什么是Runtime?
OC是一门动态性很强的编程语言,不像C/C++等编译型语言,程序运行结果就是编译后的结果,OC允许程序在运行时动态地去修改一些东西,比如动态添加方法、动态替换方法的实现、动态地去修改实例对象的类型,也就是允许这些操作推迟到运行时,这种动态性就是由Runtime来支撑和提供的
Runtime就是一套C语言API,封装了很多动态性相关的函数,平时编写的OC中动态相关的代码底层都会转换成Runtime API进行调用
底层相关数据结构
isa结构
在ARM64
架构之前,isa
只是一个普通的指针,存储着class
、meta-class
对象的地址;之后,对isa
进行了优化,其结构采用了union
联合体,将一个64位
的内存数据bits
分开存储了许多数据,其中的33位
才是存储class或meta-class的地址值
优化过的isa
其实就是位域,进一步了解isa
结构见这篇文章:【iOS】OC类与对象的本质分析
Class结构
class_rw_t
在Class
对象的底层结构objc_class
中,我们知道通过bits & FAST_DATA_MASK
就可以得到class_rw_t
类型的表结构
class_rw_t
里面的methods
、properties
、protocols
都是二维数组,是可读可写
的,包含了类的初始内容、分类的内容
以method_array_t
举例,里面的元素都是method_list_t
类型的二维数组,每一个二维数组又是method_t
类型的元素,表示每一个方法类型
class_ro_t
class_ro_t
里面的baseMethodList、baseProtocols、ivars、baseProperties
是一维数组,是只读的,包含了类的初始内容
在objc
源码里的头文件objc-runtime-new.mm
,通过查看函数realizeClassWithoutSwift
的实现,程序运行时会将class_ro_t
里面的数据和分类里面的数据信息全部合并到一起放到class_rw_t
里面来
method_t
method_t
是对函数的封装,里面包含了函数名
、编码信息
以及函数地址
IMP
代表函数的具体实现,指向着该函数的地址
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
SEL
代表函数名,一般叫做选择器,底层结构跟char *
类似,可以通过@selector()
和sel_registerName()
获得
typedef struct objc_selector *SEL;
// 不同类中相同名字的方法,所对应的方法选择器是相同的,可以通过sel_getName()和NSStringFromSelector()转成字符串
types
包含了函数返回值、参数编码的字符串,排列顺序如下
iOS中提供了一个叫做@encode
的指令,可以将具体的类型表示成字符串编码
一个函数默认会带有两个参数id _Nonnull
和SEL _Nonnull
,之后才是写入的参数
下面举例说明,函数的types是多少
- (int)test:(int)age height:(float)height
{
NSLog(@"%s", __func__);
return 0;
}
// 该函数types为i24@0:8i16f20
// i 返回值int类型
// 24 几个返回值类型占据的大小总和(8 + 8 + 4 + 4)
// @ id类型
// 0 表示从第0位开始
// : SEL类型
// 8 从第8位开始
// i 参数int类型
// 16 从第16位开始
// f 参数float类型
// 20 从第20位开始
// 可以通过输出日志验证
NSLog(@"%s %s %s", @encode(unsigned short), @encode(Class), @encode(SEL));
方法缓存cache_t
Class
内部结构中有个方法缓存cache_t
,用散列表(哈希表) 来缓存曾经调用过的方法,可以提高方法的查找速度
在objc-cache.mm
文件里可以查看cache_t::insert
函数,是通过一套哈希算法计算出索引,然后根据索引在散列表数组里直接插入数据进行缓存
void cache_t::insert(SEL sel, IMP imp, id receiver) {
....
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
// ....
else {
// 如果空间已满,那么就进行扩容,乘以2倍
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
// 将旧的缓存释放,清空缓存,然后设置最新的mask值
reallocate(oldCapacity, capacity, true);
}
bucket_t *b = buckets();
mask_t m = capacity - 1;
// 通过 sel&mask 计算出索引(哈希算法)
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
do {
// 通过索引找到的该SEL为空,那么就插入bucket_t
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
// 用索引从bucket里面取sel和传进来的sel做比较,如果一样证明已经存有,直接返回
if (b[i].sel() == sel) {
return;
}
// 从散列表里查找,如果上述条件不成立(索引冲突),那么通过cache_next计算出新的索引再查找插入
} while (fastpath((i = cache_next(i, m)) != begin));
bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
清空缓存cache_t::reallocate
当存储空间已满时,会进行扩容,并且将旧的缓存全部释放清空,然后设置最新的mask
值,mask
值是散列表的存储容量-1
,也正好对应散列表的索引值
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld) {
bucket_t *oldBuckets = buckets();
bucket_t *newBuckets = allocateBuckets(newCapacity);
ASSERT(newCapacity > 0);
ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
setBucketsAndMask(newBuckets, newCapacity - 1);
// 将旧的缓存和mask释放
if (freeOld) {
collect_free(oldBuckets, oldCapacity);
}
}
哈希算法cache_hash
static inline mask_t cache_hash(SEL sel, mask_t mask) {
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
// 与mask按位与,有时也可对mask取余%
}
如果计算出的索引在散列表中已经有了缓存数据,那么就通过cache_next
更新下索引值,再去对应的位置插入缓存数据
更新索引值cache_next
通过源码可以看到计算方式如下:
#if CACHE_END