OC底层原理 学习大纲
梳理下
调用方法
的流程,避免大家迷路。
- 我们
对象
(实例对象或类)调用方法
,都是执行objc_msgSend
:- step1: 进入
汇编
语言,在cache
中快速查找
,找到了返回imp
,没找到走step2
- step2: 进入
c/c++底层
,在methodList
中查找,(会将方法写入缓存,保障后续调用时,能直接在第一步就获取到imp
),找到了的返回imp
,没找到走step3
- step3: 走
最后
的处理机制
(三重防护,这个后面详细介绍),没找到走step4
- step4: 执行默认的
imp
,报错提示,crash
。
上一节已了解方法在cache
中的高速查找
流程,当cache
中找不到imp
时,我们进入step2
:
方法慢速查找流程
请跟着我,一步步细致理解
。我相信你会收货满满的干货
- 了解高速
cache
转低速c/c++底层
过程 - 初始值forward_imp
- 慢速前的cache读取
- 检查类是否合法
- 类的初始化(双向链表)
- 当前类和父类循环查找imp
- 最后的防线
- 查找结果
1. 了解高速cache
转低速c/c++底层
过程
我们选择
arm64真机环境
,在objc4
源码中进行探索。上一节我们在
缓存
中找不到imp
后,就来到了__objc_msgSend_uncached
->MethodTableLookup
方法列表查找 ->_lookUpImpOrForward
。objc4源码
中搜索_lookUpImpOrForward
,发现没有具体实现
。 取消_
,搜索lookUpImpOrForward
:
- 发现
lookUpImpOrForward
不再是汇编语言,而是oc底层语言。这也是为什么说从这开始,就是从快速搜索(汇编)
回到慢速搜索(c/c++)
代码检验流程
main.m
中加入测试代码。 在[p sayHello];
加入断点@interface HTPerson : NSObject - (void)sayHello; @end @implementation HTPerson - (void)sayHello{ NSLog(@"%s",__func__); } @end int main(int argc, const char * argv[]) { @autoreleasepool { HTPerson *p = [HTPerson alloc]; [p sayHello]; } return 0; }
-
lookUpImpOrForward
的代码在OC底层原理十: 类的深入理解中有讲到,这里我们做更细致的分析。
2. 初始值forward_imp
lookUpImpOrForward
函数第一步,设置初始变量
(forward_imp、imp、curClass)
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
我们先弄清楚forward_imp
是什么?
- 全局搜索
_objc_msgForward_impcache
注:
1、C/C++
调用汇编
,去汇编
中查找时: 在方法前
多加
一个下划线
2、汇编
调用C/C++
方法,去C/C++
中查找时: 在方法
前减
一个下划线
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
结论: forward_imp
的值就是未找到imp
的函数实现
3. 慢速前的cache读取
在进入lookUpImpOrForward
函数前,我们在汇编MethodTableLookup宏
中了解behavior
的值为3
lookUpImpOrForward
函数继续往下看:
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
因为LOOKUP_CACHE
= 4
, behavior
= 3
,二进制得出 behavior & LOOKUP_CACHE
为0
。
-
if
判断条件不成立
。
初次进入lookUpImpOrForward
是从缓存
进入的,所以没必要再在缓存中
进行查询,但是后续再次进入时,控制入参behavior
的值,会触发cache_getImp
缓存查询
我们看看cache_getImp
,发现又回到了高速
的汇编
层,全局搜索_cache_getImp
:
发现调用了CacheLookup
,继续在cls
的cache
中高速查找
当前sel
对应的imp
。
CacheLookup高速查找
流程在上一节已详细介绍
主要目的:
可能
多个线程
都在执行objc_msgSend
任务,在你第一次
高速查找未找到
时,可能其他线程
已将cls的sel和imp写入
了cls的缓存
。高速查找(汇编)
比低速查找(c/c++)
性能快
太多了,我们进入慢速查找
前,我们再走一次高速缓存
查找。 (缓存读取
不到再过来lookUpImpOrForward
的,默认走了一遍高速缓存了,就没再进入了)如果获取到
imp
,就跳转到done_nolock
(下面一起分析), 如果没有,进入慢速查找
流程。
4. 检查类是否合法
checkIsKnownClass(cls);
确保cls
在已知类列表
中,避免传入一个非类
的二进制
文件,进行CFI攻击
。
5. 类的初始化(双向链表)
确保cls已存在
,将cls
赋值给curClass
- 如果
不存在
,就现在创建类
( 因为swift
和OC
类的结构不一样,所以需要单独判断
是否存在。)
// 如果类没有实现或内部信息不完整
if (slowpath(!cls->isRealized())) {
// 实现类(swift和oc) 递归实现完整的继承链和isa指向链
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
// 如果类没有初始化
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
// 完成类的初始化
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
// 此时运行时锁一定是锁定状态
runtimeLock.assertLocked();
// 拿到了cls类 (oc类或swift类)
curClass = cls;
5.1 类的实现(完善继承链和isa链,双向绑定)
此处目的是cls的所有内容赋值
,完成继承链
和isa指向链
的完整绑定
。便于后续沿着继承链搜索sel对应的imp
。
5.2 类的初始化
检查本类
及继承链
上的类是否都初始化
,确保后续可以对类进行操作。
6.当前类和父类循环查找imp
// unreasonableClassCountt为 353056。一个足够大的值,只要比继承链类的个数大,就循环有效)
for (unsigned attempts = unreasonableClassCount();;) {
// 在curClass的函数列表中搜索sel方法
Method meth = getMethodNoSuper_nolock(curClass, sel);
// 如果存在,将imp赋值为sel对应的imp。跳到下面done继续操作
if (meth) {
imp = meth->imp;
goto done;
}
// 如果没有找到, curClass赋值为curClass的父类,判断父类是否存在
if (slowpath((curClass = curClass->superclass) == nil)) {
// imp取初始值forward_imp(错误提示),跳出for操作。
imp = forward_imp;
break;
}
// 防止无限循环
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 【进入循环】
// cache_getImp走汇编,循环从父类缓存中高速寻找sel对应的Imp
imp = cache_getImp(curClass, sel);
// 如果imp是初始值forward_imp(错误提示),就跳出循环
if (slowpath(imp == forward_imp)) {
break;
}
// 在父类中找到imp后,跳到下面done继续操作
if (fastpath(imp)) {
goto done;
}
}
7.最后的防线
我们看到,上一步操作
中,如果没找到imp
,都会break
跳出for
操作,进入下面的动态尝试
。
- 这是
慢速查找最后的倔强
,也是系统给予sel方法
的最后一次补救机会。
// behavior 默认为3, LOOKUP_RESOLVER为2
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER; // behavior取LOOKUP_RESOLVER的反
return resolveMethod_locked(inst, sel, cls, behavior);
}
这段代码
只执行一次
- 首次进入:
behavior
=3,LOOKUP_RESOLVER
= 2,behavior & LOOKUP_RESOLVER
= 2 (二进制计算), if条件成立。
behavior
取LOOKUP_RESOLVER
的反
,下一次behavior
和LOOKUP_RESOLVER
进行与运算
时,结果一定为0
。
(比如001
取反是110
, 他们每个位
都相反
,所以与运算
的结果为000
)所以这个补救机会,只会进入一次。
- 这一步包括
动态方法决议
等三步挽救法
,我们在下一节详细讲解。
本节我们只需要知道,如果最后挽救失败
,程序就会抛出forward_imp
异常内容。并crash了。
8.查找结果
当我们找到
imp
时,就会直接进入done
流程,将cls
的sel
和imp
写入缓存中(便于下次同样的方法,可以在cache汇编快速查询到
)。done
结束后,我们会进入done_nolock
流程, 如果imp
是forward_imp
,就返回nil,否则,返回正常的imp
。
done:
// 从将sel和imp写入cls的缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 运行时解锁
runtimeLock.unlock();
done_nolock:
//如果不需要找了(LOOKUP_NIL),并且imp等于forward_imp,就返回nil。
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
// 返回当前获取的imp
return imp;
下一节,OC底层原理十四: objc_msgSend (消息转发 + 汇总图),我们会探究最后的防线
,并梳理objc_msgSend
的完整流程
。