1、isa 详解
- isa 在 arm64 架构之前就是一个普通的指针,存储着 Class、Meta-Class 对象的内存地址
- 从 arm64 架构开始,对 isa 进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息
arm64 后:
instance 对象中 isa & ISA_MASK -----> Class 对象地址
class 对象中 isa & ISA_MASK -----> Meta_Class 对象地址
2、对象结构(isa解释)
struct objc_object {
isa_t isa;
}
// 共用体:使用位域
union isa_t {
uintptr_t bits; // bits 的二进制一共 64 位。每一位都代表一个信息,详情看下面的 struct 解释
#define ISA_MASK 0x0000000ffffffff8ULL
// arm64下,bits 中结构如下
struct {
uintptr_t nonpointer : 1 // 第1位:0 代表普通指针,存储着 Class、Meta-Class 对象的内存地址,1 代表优化过,使用位域存储着更多信息
uintptr_t has_assoc : 1 // 第2位:是否设置过关联属性,如果没哟,释放会更快
uintptr_t has_cxx_dtor : 1 // 第3位:是否有 C++ 的析构函数,如果没有,释放会更快
uintptr_t shiftcls : 33 // 第4~36位:类对象\元类对象的地址
uintptr_t magic : 6 // 用于在调试是分辨对象是否为完成初始化
uintptr_t weakly_referenced : 1 // 是否被若引用指向过
uintptr_t deallocating : 1 // 正在释放
uintptr_t has_sidetable_rc : 1 // 计数器首付过大无法存储在 isa 中,如果为 1,那么引用计数会存储在一个叫 SideTable的类的属性中
uintptr_t extra_rc : 19 // 存储引用计数器,具体指为引用计数器-1
}
}
3、Class 结构
类对象中存放着: isa 指针、superclass 指针、属性、对象方法、协议、成员变量
元类对象中存放着: isa 指针、superclass 指针、类方法
元类对象 是一种特殊的 “类对象”
struct objc_class: objc_object {
Class isa // 这里的 isa == isa_t isa
Class superclass;
cache_t chches; // 方法缓存
class_data_bits_t bits; // 用于获取具体的类信息
}
bits & FAST_DATA_MASK 可以得到 class_rw_t 结构体(具体的类信息)
class_rw_t 中保存了方法列表,属性类表,协议类表等
// 运行时会动态创建,包含了类,分类中的信息
struct class_rw_t {
...
const class_to_t *ro; // 编译时确定的中的成员变量方法,属性,协议等
method_list_t *methods; // 在运行时将类中,分类中的方法放到二维数组中
propocity_list_t *propercies; // 属性类表
protocol_list_t *protocols; // 协议列表
...
}
// 编译时就确定的类中的信息,这里的信息在运行时是不可变的
struct class_to_t {
...
const char * name; // 类名
method_list_t *basemMethodList; // 类中的方法列表
propocity_list_t *basePropercies;
protocol_list_t *baseProtocols;
const ivar_list_t *ivars; // 成员变量列表
...
}
类中的 methods 是一个二维数组,查找方法比较耗时,所以 OC 准备了 caches 这个变量,用来存储一些曾经使用过的方法,以便再次调用方法时提高时间效率。
3.1、method
struct method_t {
SEL name; // 方法的名字
const char *types; // 方法的返回值类型,参数类型
IMP imp; // 方法实现的指针,地址
}
IMP代表函数的具体实现
typedef id _Nullable (*IMP) (id _Nonnull, SEL _Nonnull, ...)
-
SEL 代表方法\函数名,一般叫选择器,底层结构类似char *
- 可以通过 @selector() 和 sel_registerName() 获得
- 可以通过 sel_getName() 和 NSSringFromSelector() 转成字符串
- 不同类中相同名字的方法,所对应的方法选择器是相同的
typedef struct objc_selector *SEL
types 包含了函数返回值,参数类型的字符串
[返回值][参数 1]...[参数 n]
3.2、cache
cache 使用了‘散列表’的数据结构存储来存储曾经调用过的方法(cache 中存储的是[method_t, method_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; // 函数的内存地址
}
OC 中散列表的规则:
- 初始化默认容量 _mask, 比如:_mask = 4
- 根据 sel 查找方法
- 结果查到了,判断方法名是否一致
是:取 IMP,调用方法
否:index - 1 且 != 初始 index,再次查找 - 结果没有查到,返回 NULL,这时需要从 objc_rw_t 结构体的 methods 中遍历取方法,调用成功后在添加到 cache 中
计算 index 方法:
index = 方法名 & 容量 // 这里取到 index < 容量
4、objc_msgSend 消息发送
OC 方法调用:消息机制,给方法调用者发送消息
都是转换成了 objc_msgSend 消息发送
objc_msgSend 的执行流程可以分为 3 大阶段:
- 消息发送
- 动态方法解析
- 消息转发
如果三个阶段都没有成功,则报错:unrecognized selector sent to instance
4.1、消息发送
1、判断接受者receiver 是否为nil
, 如果为 nil,直接退出 (所以nil
也可以调用方法),结束
2、如果 receiver 不为nil
,则从 receiverClass 的cache
中进行查找,如果找到了该方法,则返回方法地址,直接调用,结束。
3、如果从 cache 中没有找到该方法,则从 receiverClass 的class_rw_t
中进行查找,如果找到了该方法,返回方法地址,直接调用,并将method_t
缓存到cache
中,结束
- 如果已排序的:二分查找
- 如果没有排序:遍历查找
4、如果仍然没有找到该方法,则从superClass
的cache
中进行查找,如果找到了该方法,返回方法地址,直接调用,并将method_t
缓存到receiver
的cache
中,结束
5、如果仍然没有找到该方法,则从superClass
的class_rw_t
中进行查找,如果找到了该方法,返回方法地址,直接调用,并将method_t
缓存到receiver
的cache
中,结束
6、如果仍然没有找到该方法,则一直向上查找父类,循环第4、5步,直到root
7、如果一直找到了root,仍然没有找到该方法,则进行<<动态方法解析>>
4.2、动态方法解析
是否已经有过动态解析,如果有,则直接进行
<<消息转发>>
如果没有,则进行动态解析。如果业务员实现了
resolveInstanceMethod:
或resolveClassMethod
的方法,并在该方法中动态添加方法,这些方法会添加到class_rw_t
的 methods 中,并标记为已解析
可以使用class_addMethod(...)
方法动态添加,动态解析后,获取动态方法解析方法的返回值(用于打印),会再次回到
<<消息发送>>
阶段,再次查找方法
4.3、消息转发
调用
forwardingTargetForSelector:
方法进行转发,这个方法可以返回能够处理该方法的对象,如果返回了其他对象,则会交给返回对象进行处理,即给返回的对象发送消息objc_msgSend(forwardingTarget, aSelector)
如果没有返回
forwardingTarget
,则调用methodSignatureForSelector:
方法进行方法签名,如果返回签名为nil
,则调用doesNotRecognizeSelector:
方法,抛出异常如果返回了响应的方法签名,则会将签名封装到一个
NSInvocation
的对象aInvocation
中,并将aInvocation
传递到forwordInvocation:
方法中,进行最后处理-
forwordInvocation:
方法中可以为aInvocation
设置方法调用者,方法名,参数等,并执行指定的方法- (void)forwordInvocation:(NSInvocation *)aInvocation { // NSInvocation 封装了一个方法调用,包括:方法签名、方法名、方法参数 aInvocation.target = [Cat new]; // 方法调用者 // aInvocation.selector // 方法名 // 方法参数,index从2开始, 0:receiver, 1:selector // [aInvocation setArgument:NULL atIndex:] // 方法的调用执行 [aInvocation invoke]; }
super
[super message]的底层实现:
消息接受者仍然是子类对象
-
从父类开始查找方法的实现
[super message] struct aaa = { self, // 消息接收者 SuperClass; // 从哪个类开始搜索 } /// aaa 就是 self,调用的是会找到[Student class].superclass objc_msgSendSuper(aaa, sel_registerName("message")); 真是用的是objc_msgSendSuper2, 在 objc_msgSendSuper2 中要求结构体是: struct aaa = { self, // 消息接收者 CurrentClass; // 本身类 } 在 在objc_msgSendSuper2 中会对第二个变量取 superclass, 结果是一样的
面试题
1. 讲一下 OC 的消息机制
OC 中的方法调用其实都是转成了`objc_msgSend`函数的调用,给`receiver`方法调用者)发送了一条消息(selector方法名)
`objc_msgSend`底层有三大阶段:消息发送,动态方法解析,消息转发
(这里可以详细说一下三大流程)
2. 什么是 Runtime?平时项目中有用过吗?
Runtime:
OC 是一种动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行
OC 的动态性就是由 Runtime 来支撑和实现的,Runtime 是一套 C 语言的 api,封装了很多动态性相关的函数
平时编写的 OC 代码,底层都是转换成了 Runtime API 进行调用
具体应用:
利用关联对象给分类添加属性
遍历类的所有成员变量(修改 textfield 的 placeholder 颜色,自动归档解档)
交换方法实现(交换一些系统的方法)
利用消息转发机制,解决方法找不到的异常问题
.....
3. 判断打印结果
/// 父类
@interface Person: NSObject @end
@implemention Person @end
/// 子类
@interface Student: Person @end
@implemention Student
- (instancetype)init {
if (self = [super init]) {
NSLog(@"[self class] = %@", [self class]); // Student
NSLog(@"[self superclass] = %@", [self superclass]); // Person
NSLog(@"[super class] = %@", [super class]); // Student
NSLog(@"[super superclass] = %@", [super superclass]); // Person
}
return self;
}
@end