目录
1.Runtime简介
2.isa的结构及详解
3.Class的结构
4.方法缓存实现
1.Runtime简介
- Objective-C 语言是一门动态语言。它把一些决策从编译阶段、链接阶段推迟到运行时阶段,实现该机制的基础就是 runtime(又叫作运行时)。
静态语言:在编译阶段就已确定所有变量的数据类型,同时也确定要调用的函数,以及函数的实现。常见的静态语言,如:swift , C/C++、Java 等。
动态语言:程序在运行时可以改变其结构。也就是说在运行时检查变量数据类型,同时在运行时才会根据函数名查找要调用的具体函数。如 Objective-C。
- Runtime是什么?
Runtime 提供的接口基本都是 C 语言,源码由 C\C++\汇编语言编写。Runtime API 为 Objective-C 语言的动态属性提供支持,充当一种用于 Objective-C 语言的操作系统,使得该语言正常运转工作。
2.isa详解
要想彻底熟悉 Runtime,那么必须要Runtime底层常用的数据结构,比如 isa 指针,Class 的结构等,下面我们先来看看isa;
之前写的一篇文章也有提到isa指针的概念和作用 iOS 探究 OC对象、isa指针及KVO实现原理
现在我们深入探讨一下isa内部的结构:
- 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址 (作为了解)
- 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。
从Runtime源码下载地址下载最新的Runtime源码,
在objc_private.h
文件中,我们可以找到OC对象的结构体定义和isa的定义:
精简主要代码如下
//对象结构体
struct objc_object {
private:
isa_t isa; //isa指针,大小为8个字节
public:
对象的相关方法
....
}
union isa_t {
uintptr_t bits;
private:
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
复制代码
由上面在isa.h
中找到ISA_BITFIELD
在arm64中的定义
通过上面代码块的分析我们取出ISA_BITFIELD真机部分,将union isa_t的结构整理如下:
union isa_t
{
Class cls;
uintptr_t bits;
struct{
//1,代表优化过,使用位域存储更多的信息;0,代表普通的指针,存储着类/元类对象的内存地址
uintptr_t nonpointer : 1;
//是否有设置过关联对象,如果没有,释放时会更快,
//如果有的话销毁前会调用 _object_remove_assocations 函数根据关联策略循环释放每个关联对象
uintptr_t has_assoc : 1;
//是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
//如果有的话对象销毁前会调用 object_cxxDestruct 函数去执行该类的析构函数
uintptr_t has_cxx_dtor : 1;
//存储着Class、Meta-Class对象的内存地址信息
//isa & ISA_MASK 得出该实例对象所属的的类的地址
uintptr_t shiftcls : 33;
//用于在调试时分辨对象是否未完成初始化
uintptr_t magic : 6;
//是否有被弱引用指向过,如果没有,释放时会更快
// 如果有的话对象销毁前会调用 weak_clear_no_lock 函数把该对象的弱引用置为 nil,
// 并调用 weak_entry_remove 把对象的 entry 从 weak_table 中移除
uintptr_t weakly_referenced : 1;
//占位符 无用
uintptr_t unused : 1;
//引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
uintptr_t has_sidetable_rc : 1;
//里面存储的值是引用计数器减1
uintptr_t extra_rc : 19;
}
}
复制代码
上面union isa_t表示 isa_t为一个共用体,表示里面的所有变量共用一块内存,同时使用了位域来存储更多的信息(即struct中的参数,长度总和刚好是64位,即8个字节),通过位运算存取相关参数值。
3.Class的结构及方法缓存实现
在runtime源码中的objc-runtime-new.h文件中,找到objc_class
结构体,即为class的实现,实现方法太长,下面为部分代码截图:
精简如下:
struct objc_class{
Class isa; //
Class superclass;
cache_t chahe; //方法缓存
class_data_bits_t bits;//用于获取具体类的信息(&FAST_DATA_MASK获取)
}
复制代码
通过class_data_bits_t bits
可以获取到类的相关信息,在class_data_bits_t结构体中,我们可以看到如下方法:
可以看到通过bits & FAST_DATA_MASK
返回了一个calss_rw_t的结构体,即类的信息。
- 那么calss_rw_t里面包含什么呢?
这里点进去,把源码精简完后主要内容如下:
struct class_rw_t{
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //ro即read only 里面包含的信息都是只读的
method_array_t methods //方法列表 二维数组里面是method_list_t
property_array_t properties //属性列表 二维数组里面是property_list_t
protocol_array_t protocols //协议列表 二维数组里面是protocol_list_t
Class firstSubclass;
Class nextSiblingClass;
}
复制代码
从class_rw_t里我们主要分析两个主要部分class_ro_t和methods、properties、protocols:
- class_ro_t 里面包含类的初始化的一些参数和方法,都是只读不可修改的,把class_ro_t源码精简如下:
struct class_ro_t{
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const char * name; //类名
method_list_t *baseMethodList //方法列表 (一维数组)
const ivar_list_t * ivars; //成员变量列表 (一维数组)
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; // 属性列表 (一维数组)
protocol_list_t * baseProtocols; // 协议列表 (一维数组)
}
复制代码
可以看出来都是类的基本信息和初始化的方法列表、属性列表、成员变量列表等。
- methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容,结构如下,每个methods数组包含若干个method_list_t,每个method_list_t数组里面又有若干个method_t结构体,可参考下图
接下来再看看method_t是什么,在objc-runtime-new.h文件中找到struct method_t
,主要参数如下:
struct method_t{
SEL name; //函数名
const char *types; //编码(返回值类型、参数类型)
IMP imp; // 指向函数的指针(函数地址)
}
复制代码
我们可以把method_t理解为是对方法\函数的封装。
- IMP代表函数的具体实现,每一个方法都默认带有两个隐式参数 self : 方法调用者 _cmd : 调用方法的标号 ,可以写也可以不写。
- SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似
1.可以通过@selector()和sel_registerName()获得
2.可以通过sel_getName()和NSStringFromSelector()转成字符串
3.不同类中相同名字的方法,所对应的方法选择器是相同的
- types包含了函数返回值、参数编码的字符串
iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码
下面写两个例子总结一下:
-(void)testMethods;
//每个方法其实都是有两个隐式参数self和_cmd,所以上面的方法等价于下面的方法
-(void)testMethods:(id)self _cmd:(SEL)_cmd;
复制代码
通过断点分析可以看到testMethods的types是:v16@0:8
参考上面encode指令表格,v16@0:8
每个字符含义如下:
我们再写一个复杂一些的函数:
- (int)test:(int)age height:(float)height;
转换如下:
- (int)test:(id)self _cmd:(SEL)_cmd (int)age height:(float)height;
复制代码
上面函数的types是i24@0:8i16f20
:
i 表示返回值是int类型
24 表示所有参数占字节总和 id(8字节)+ SEL(8字节)+ int(4字节) + float(4字节) = 24
@ 表示对象类型
0 表示参数self是从0字节开始
: 表示SEL类型
8 表示参数_cmd是在参数self后面开始,即8字节后
i 表示int类型
16 表示age是在self和_cmd后面,即8+8 = 16
f 表示float类型
20 表示height是在self、_cmd、age后面,即8+8+4 = 20
总结
通过上面对class结构的内容分析,我们可以用下面一张图来表示各结构体之间的关系:
4.方法缓存实现
上面我们主要探究了class的结构以及他的详细信息,现在我们继续看看class中的方法缓存cache_t chahe
struct objc_class{
Class isa; //
Class superclass;
cache_t chahe; //方法缓存
class_data_bits_t bits;//用于获取具体类的信息(&FAST_DATA_MASK获取)
}
复制代码
- 当我们调用方法时,大概的轨迹是在当前class找,没有找到就去,父类里面找,如果找到就返回,没找到会一层一层去父类找,一直到基类。最终找到的方法就会被Class内部结构中的cache_t,用散列表(哈希表)将方法缓存起来,下次调用直接去cache_t里面拿,从而提高方法查找速度。
详细的对象/类方法调用轨迹可参考之前的文章 iOS 探究 OC对象、isa指针及KVO实现原理
- cache_t是如何对方法进行缓存的呢?下面先看下他的内部结构
struct cache_t {
struct bucket_t *_buckets; // 散列表 数组
mask_t _mask; // 散列表的长度 -1
mask_t _occupied; // 已经缓存的方法数量
};
复制代码
bucket_t
是以数组的方式存储方法列表的,看一下bucket_t内部结构
struct bucket_t {
private:
cache_key_t _key; // SEL作为Key
IMP _imp; // 函数的内存地址
};
复制代码
从源码中可以看出bucket_t中存储着SEL和_imp,通过key->value的形式,以SEL为key,函数实现的内存地址 _imp为value来存储方法。
为了方便理解,我们还是通过下面图示来表达他们的关系:
总结一下方法被缓存的流程:
1.第一次调用某方法,会根据当前类->父类->基类的轨迹找方法,找到了则把这个方法的信息(SEL,imp)保存进当前类的缓存列表中,并使用方法的SEL(方法名)作为key,函数实现的内存地址 imp为value来存储方法。
2.后面调用某方法,会先在类的缓存列表中查找,通过传进来的方法key,查找到这个方法的信息(类似字典),直接返回。