概述
runtime
简称运行时
,就是系统在运行的时候的一些机制,其中最主要的是消息机制。基于 C/C++
语言和汇编语言编写,苹果和 GNU
各自维护一个开源的 runtime
版本,这两个版本之间都在努力的保持一致。
对于 C
语言而言,函数的调用在编译的时候就会决定调用哪个函数,无任何二义性。
而 OC
的方法调用则称为消息发送
,在编译的时候并不能决定真正调用哪个方法,只有在运行的时候才会根据方法的名称找到对应的方法来调用。
Objective-C
程序在三个不同的层次上与运行时系统
交互:
- 通过
Objective-C
源代码进行交互; - 通过
NSObject
类中定义的方法交互; - 通过
直接调用
运行时函数;
支撑OC动态性
OC
是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。
OC
的动态性就是由 runtime
来支撑和实现的,runtime
封装了很多动态性相关的函数。
平时编写的 OC
代码,底层都是转换成了 runtime
API 进行调用。
具体应用
- 利用关联对象(
AssociatedObject
)给分类添加属性; - 交换方法实现(交换系统的方法);
- 遍历一个类中所有的成员变量、属性、方法;
- 利用消息转发机制解决方法找不到的异常问题;
- 字典转换模型;
KVC
、KVO
;- 归档(编码、解码);
NSClassFromString
字符串转类对象;block
;- 类的自我检测;
- .....
isa详解
在 arm64
架构之前,isa
就是一个普通的指针,存储着 Class
、Meta-Class
对象的内存地址。
从 arm64
架构开始,对 isa
进行了优化,变成了一个共用体(union
)结构,还使用位域来存储更多的信息。
#include "isa.h"
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
// 访问该类需要自定义 ptrauth 操作,因此通过将其设为私有来强制客户端通过 setClass/getClass。
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // 结构体的成员,以宏定义方式定义在 isa.h 中,并且区分多个CPU架构
};
bool isDeallocating() {
return extra_rc == 0 && has_sidetable_rc == 0;
}
void setDeallocating() {
extra_rc = 0;
has_sidetable_rc = 0;
}
#endif
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
Class getDecodedClass(bool authenticated);
};
下面是 arm64
架构的 isa
位域宏定义,所有信息加起来刚好是 64
位,也就是 8
字节:
#define ISA_BITFIELD \
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
nonpointer
0
代表普通的指针,存储着 Class
、Meta-Class
对象的内存地址。
1
代表优化过,使用位域存储更多的信息。
has_assoc
是否有设置过关联对象,如果没有,释放时会更快。
has_cxx_dtor
是否有 C++
的析构函数(.cxx_destruct
),如果没有,释放时会更快。
shiftcls
存储着 Class
、Meta-Class
对象的内存地址信息。地址值最后一位永远是0
或8
,因为后3个二进制位是0
。
magic
用于在调试时分辨对象是否未完成初始化。
weakly_referenced
是否有被弱引用指向过,如果没有,释放时会更快。
deallocating
对象是否正在释放。
extra_rc
里面存储的值是引用计数器减1
。
has_sidetable_rc
引用计数器是否过大无法存储在 isa
中。
如果为1
,那么引用计数会存储在一个叫 SideTable
的类的属性中。
如何分析isa
创建一个对象,然后断点后,使用 lldb
指令打印 isa
:
NSObject *obj = [[NSObject alloc] init];
// 打印isa
//(lldb) p/x obj->isa
//(Class) $1 = 0x01000001e8b52331 NSObject
然后根据位域中每个信息的位数和含义去分析即可,我这是用 M1
运行的,应该按照 arm64e
架构的位域信息结构体来分析:
0x01000001e8b52331 转二进制
0b 0000 0001 0000 0000 0000 0000 0000 0001
1110 1000 1011 0101 0010 0011 0011 0001
Class的结构
struct objc_object {
isa_t isa; // isa共用体
};
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用于获取具体的类信息
};
struct class_rw_t {
// ...
explicit_atomic<uintptr_t> ro_or_rw_ext;
// ...
private:
using ro_or_rw_ext_t = objc::PointerUnion<const class_ro_t, class_rw_ext_t, PTRAUTH_STR("class_ro_t"), PTRAUTH_STR("class_rw_ext_t")>;
};
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro; // 指向 class_ro_t
method_array_t methods; // 方法列表,二维数组,存放method_list_t,method_list_t存放method_t
property_array_t properties; // 属性列表,二维数组
protocol_array_t protocols; // 协议列表,二维数组
char *demangledName;
uint32_t version;
};
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; // instance对象占用的内存空间
#ifdef __LP64__
uint32_t reserved;
#endif
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
explicit_atomic<const char *> name; // 类名
// With ptrauth, this is signed if it points to a small list, but
// may be unsigned if it points to a big list.
void *baseMethodList; // 方法列表,一维数组,直接存放method_t
protocol_list_t * baseProtocols; // 协议列表,一维数组
const ivar_list_t * ivars; // 成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; // 属性列表,一维数组
};
method_t
method_t
是对方法/函数的封装,底层结构:
struct method_t {
struct big {
SEL name; // 方法名
const char *types; // 类型编码(返回值类型、参数类型)
MethodListIMP imp; // 指向方法实现的函数的指针(函数地址)
};
};
SEL
可以简单理解为方法名的字符串,通过 @selector()
或者 sel_registerName()
获得。
不同类中相同名字的方法,所对应的方法选择器是相同的。
SEL
是一个类型别名,不过 objc_selector
结构体的源码可能没开源:
typedef struct objc_selector *SEL;
Type Encoding
类型编码,iOS 中提供了一个叫做 @encode
的指令,可以将具体的类型表示成字符串编码。
符号和类型对照表:苹果官方文档
NSLog(@"%s %s %s %s %s", @encode(Person), @encode(int), @encode(void), @encode(SEL), @encode(id));
// {Person=#} i v : @
方法类型编码示例:
// types: i24@0:8i16f20
// i24: 返回值类型int,所有参数占的24字节数
// @0: 第1个参数类型id(8字节),从第0字节开始
// :8: 第2个参数类型SEL(8字节),从第8字节开始
// i16: 第3个参数类型int(4字节),从第16字节开始
// f20: 第4个参数类型float(4字节),从第20字节开始
- (int)testWithArg1:(int)arg1 arg2:(float)arg2;
IMP
指向方法实现的函数的指针(函数地址)。
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
using MethodListIMP = IMP;
方法缓存
Class
内部结构中有个方法缓存(cache_t
),用散列表(哈希表)来缓存曾经调用过
的方法,可以提高方法的查找速度。
cache_t
的结构:
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
uint16_t _flags;
#endif
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
};
如果不使用缓存,则每次调用方法都是要根据类的父子关系,逐层从 类对象
、元类对象
中的 class_ro_t
或 class_rw_ext_t
里的方法数组中遍历查找。