iOS 底层探索 文章汇总
一、前言
上一篇文章iOS cache_t原理分析中我们分析了cache_t
的底层代码以及部分原理,在Cache
的流程中发现方法的快速查找流程为objc_msgSend
,那么这篇文章我们就一起分析objc_msgSend
流程是怎样的。
二、iOS 方法调用流程分析
在前面的文章iOS 对象的本质我们使用clang
将OC
代码编译为cpp
文件,编译后的代码如下:
//OC中方法的调用
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];
//clang编译后的底层实现
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
从OC
代码编译后可以发现方法的本质就是objc_msgSend消息发送
objc_msgSend 流程图
objc_msgSend 汇编源码
在objc4-781源码中,搜索objc_msgSend
。这篇文章主要分析真机中objc_msgSend
源码实现,基于效率考方法的快速查找方法objc_msgSend
使用的是汇编实现,而汇编的文件以.s结尾,所以需要在arm64.s后缀
的文件中查找objc_msgSend
源码实现。
Tips:检索文件时如果很多文件展开会影响查看下面的文件:按住Commend 点击文件前的收起三角可收起所有文件
_objc_msgSend
源码实现如下:
//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
//---- 无窗口
UNWIND _objc_msgSend, NoFrame
//---- p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receiver
cmp p0, #0 // nil check and tagged pointer check
//---- le小于 --支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//---- p0 等于 0 时,直接返回 空
b.eq LReturnZero
#endif
//---- p0即receiver 肯定存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器
ldr p13, [x0] // p13 = isa
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//---- 如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//---- 等于空,返回空
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
ret
END_ENTRY _objc_msgSend
objc_msgSend
主要步骤分析:
- 【第一步】判断
objc_msgSend
方法的第一个参数receiver
是否为空
I. 如果支持tagged pointer
,跳转至LNilOrTagged
i. 如果小对象
为空,则直接返回空,即LReturnZero
ii. 如果小对象
不为空,则处理小对象的isa
,走到【第二步】
II 如果即不是小对象,receiver
也不为空,有以下两步
i. 从receiver
中取出isa
存入p13
寄存器,
Ii. 通过GetClassFromIsa_p16
中,arm64
架构下通过isa & ISA_MASK
获取shiftcls
位域的类信息,即class,GetClassFromIsa_p16
的汇编实现如下,然后走到【第二步】
.macro GetClassFromIsa_p16 /* src */
//---- 此处用于watchOS
#if SUPPORT_INDEXED_ISA
// Indexed isa
//---- 将isa的值存入p16寄存器
mov p16, $0 // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa -- 判断是否是 nonapointer isa
// isa in p16 is indexed
//---- 将_objc_indexed_classes所在的页的基址 读入x10寄存器
adrp x10, _objc_indexed_classes@PAGE
//---- x10 = x10 + _objc_indexed_classes(page中的偏移量) --x10基址 根据 偏移量 进行 内存偏移
add x10, x10, _objc_indexed_classes@PAGEOFF
//---- 从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
//--用于64位系统
#elif __LP64__
// 64-bit packed isa
//---- p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa ---- 用于32位系统
mov p16, $0
#endif
.endmacro
- 【第二步】获取isa完毕,进入慢速查找流程CacheLookup NORMAL
CacheLookup 缓存查找汇编源码
//!!!!!!!!!重点!!!!!!!!!!!!
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
LLookupStart$1:
//---- p1 = SEL, p16 = isa --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 从x16(即isa)中平移16字节,取出cache 存入p11寄存器 -- isa距离cache 正好16字节:isa(8字节)-superClass(8字节)-cache(mask高16位 + buckets低48位)
ldr p11, [x16, #CACHE]
//---- 64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//--- p11(cache)右移48位,得到mask(即p11 存储mask),mask & p1(msgSend的第二个参数 cmd-sel) ,得到sel-imp的下标index(即搜索下标) 存入p12(cache insert写入时的哈希下标计算是 通过 sel & mask,读取时也需要通过这种方式)
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
//--- 非64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
//--- p12是下标 p10是buckets数组首地址,下标 * 1<<4(即16) 得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT是3
//--- 从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--- 比较 sel 与 p1(传入的参数cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不相等,即没有找到,请跳转至 2f
b.ne 2f // scan more
//--- 如果相等 即cacheHit 缓存命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncached
CheckMiss $0 // miss if bucket->sel == 0
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素,),如果等于,则跳转至第3步
cmp p12, p10 // wrap if bucket == buckets
//--- 定位到最后一个元素(即第一个bucket)
b.eq 3f
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳转至第1步,继续对比 sel 与 cmd
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人为设置到最后一个元素
//--- p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
//--- 再查找一遍缓存()
//--- 拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
//--- 比较 sel 与 p1(传入的参数cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不相等,即走到第二步
b.ne 2f // scan more
//--- 如果相等 即命中,直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
//--- 如果一直找不到,则CheckMiss
CheckMiss $0 // miss if bucket->sel == 0
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f //如果等于,跳转至第3步
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳转至第1步,继续对比 sel 与 cmd
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncached
JumpMiss $0
.endmacro
//以下是最后跳转的汇编函数
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
//--- 如果为GETIMP ,则跳转至 LGetImpMiss
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncached
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
//--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncached
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
CacheLookup
主要步骤分析:
【第一步】通过
cache
首地址平移16字节
(因为在objc_class中,首地址距离cache
正好16字节,即isa
占8字节,superClass
占8字节),获取cahce
,cache
中高16位存mask
,低48位存buckets
,即p11 = cache
-
【第二步】从
cache
中分别取出buckets
和mas
k,并由mask
根据哈希算法计算出哈希下标
I. 通过cache
和掩码(即0x0000ffffffffffff)的& 运算
,将高16位mask
抹零,得到buckets
指针地址,即p10 = buckets
II. 将
cache
右移48位,得到mas
k,即p11 = mask
III. 将
objc_msgSend
的参数p1(即第二个参数_cmd)& msak
,通过哈希算法,得到需要查找存储sel-imp
的bucket
下标index
,即p12 = index = _cmd & mask,
为什么通过这种方式呢?因为在存储sel-imp
时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取,如下所示
-
【第三步】根据所得的哈希下标
index
和buckets首地址
,取出哈希下标对应的bucket
I. 其中PTRSHIFT
等于3,左移4位(即2^4 = 16字节)的目的是计算出一个bucket
实际占用的大小,结构体bucket_t
中sel
占8字节,imp
占8字节II. 根据计算的哈希下标
index
乘以 单个bucket
占用的内存大小,得到buckets
首地址在实际内存中的偏移量III. 通过
首地址 + 实际偏移量
,获取哈希下标index
对应的bucket
4.【第四步】根据获取的bucket
,取出其中的sel
存入p17
,即p17 = sel,取出imp
存入p9
,即p9 = imp
-
【第五步】第一次递归循环
I. 比较获取的
bucket
中sel
与objc_msgSend
的第二个参数的_cmd
(即p1)是否相等II. 如果相等,则直接跳转至
CacheHit
,即缓存命中,返回imp
III. 如果不相等,有以下两种情况
i. 如果一直都找不到,直接跳转至CheckMiss
,因为$0
是normal
,会跳转至__objc_msgSend_uncached
,即进入慢速查找流程
ii. 如果根据index
获取的bucket
等于buckets
的第一个元素,则人为的将当前bucket
设置为buckets
的最后一个元素(通过buckets首地址+mask右移44位
(等同于左移4位)直接定位到bucker
的最后一个元素),然后继续向前查找,进行递归循环 【第六步】第二次递归循环:重复【第五步】的操作,与【第五步】中唯一区别是,如果当前的
bucket
等于buckets
的第一个元素,则直接跳转至JumpMiss
,此时的$0是normal
,也是直接跳转至__objc_msgSend_uncached
,即进入慢速查找流程
objc_msgSend
慢速查找流程分析
- 在快速查找流程中,如果没有找到方法实现,无论是走到
CheckMiss
还是JumpMiss
,最终都会走到__objc_msgSend_uncached
汇编函数
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup // 开始查询方法列表
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
- 搜索
MethodTableLookup
的汇编实现,其中的核心是_lookUpImpOrForward
,汇编源码实现如下
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward //核心源码
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
lookUpImpOrForward
方法:
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//--- 定义的消息转发
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
//--- 快速查找,如果找到则直接返回imp
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
//--- 加锁,目的是保证读取的线程安全
runtimeLock.lock();
//--- 判断是否是一个已知的类
checkIsKnownClass(cls);
//--- 判断类是否实例化,如果没有,需要先实例化
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
//--- 判断类是否初始化,如果没有,需要先初始化
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
//--- 类 -- 父类 -- 根类 -- nil
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
//--- 当前类方法列表,如果找到,则返回
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
//--- 未找到方法实现,方法解析器也不行,使用转发
imp = forward_imp;
break;
}
// Halt if there is a cycle in the superclass chain.
//--- 如果父类链中存在周期,则停止
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
//--- 父类缓存
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
//--- 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器
break;
}
if (fastpath(imp)) {
//--- 如果在父类中,找到了此方法,将其缓存在类中
goto done;
}
}
// No implementation found. Try method resolver once.
//--- 没有找到方法实现,尝试一次方法解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
//--- 存储到缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
//--- 解锁
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
慢速查找流程如下
主要步骤为:
【第一步】
cache
缓存中查找,即快速查找
,找到则直接返回imp
,反之,则进入【第二步】-
【第二步】判断
类
I. 是否是已知类
,如果不是,则报错
II. 是否
实例
化,如果没有,则实例化
III. 是否
初始化
,如果没有,则初始化
-
【第三步】
for循环
,按照类-父类-根类-nil
的顺序查找
I. 当前类的方法列表
中查找方法,如果找到,则存储到cache
,并返回imp
II. 当前类被赋值为
父类
,如果父类等于nil
,则使用消息转发
,并结束循环III. 如果
父类链
中存在循环,则报错
,终止循环IIII.
父类缓存
中查找方法
i. 如果父类
中找到了消息转发
,则结束循环,进入【第四步】
i. 如果在父类缓存
中,找到了方法实现
,则将其缓存在类中
,并返回imp
i. 如果都没有找到,则继续循环,查找父类
的方法列表
,以此类推 【第四步】未找到方法实现,尝试一次
方法解析
以上就是方法的慢速查找流程
,类中无法找到实现时,会去父类中查找,直到nil
为止,如果还是没有找到,则进行一次方法解析
,如果方法解析还是不行,则使用消息转发
方法解析流程
在未找到方法实现时,会尝试一次方法解析,其流程如下
其源码实现如下:
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) { //类不是元类,调用对象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {//如果是元类,调用类的解析方法 resolveClassMethod
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//如果方法解析中将其实现指向其他方法,则继续走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
主要步骤为:
- 判断类是否是元类
- 如果是
类
,执行对象方法解析函数resolveInstanceMethod
- 如果是
元类
,执行类方法解析函数resolveClassMethod
,如果在元类中没有找到
或者为空
,则在元类的对象方法解析函数resolveInstanceMethod
中查找
- 如果是
- 如果方法解析函数中,将其实现指向了其他方法,则继续走方法查找流程
lookUpImpOrForward
小结:
iOS 方法的查找流程可简单描述为:
对象 -> ISA
-> 方法(类) -> cache_t
-> methodlist
方法会封装成消息
OC
方法 -> 消息 (sel imp
) sel -> imp
-> 内容
参考
iOS-底层原理 12:objc_msgSend流程分析