前言
前文iOS runtime之方法的本质objc_msgSend分析一探索了objc_msgSend
的缓存查找(快速查找)流程,本文将接着探索没有缓存时的方法列表查找(慢速查找)流程。
1: __objc_msgSend_uncached
流程分析
objc_msgSend
根据sel
在类或元类的缓存中查找IMP
时,如果缓存中没有查找到相应的IMP
,就会跳转MissLabelDynamic
即__objc_msgSend_uncached
流程,在方法列表里面查找相应的IMP
。
objc4-818.2
源码里全局搜索__objc_msgSend_uncached
,然后按住command
,鼠标左键点击文件名旁边的小箭头,收起所有文件,选择objc-msg-arm64.s
文件点开,选择STATIC_ENTRY __objc_msgSend_uncached
开始查看__objc_msgSend_uncached
的汇编源码。
备注:支持__has_feature(ptrauth_calls)指针身份验证功能的情况(A12及之后的仿生芯片),本文不做分析。
图解:
1.1: __objc_msgSend_uncached
汇编源码
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
// 方法列表查找_cmd对应的imp
MethodTableLookup
// x17 = IMP,调用IMP
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
复制代码
MethodTableLookup
根据x1
的_cmd
在类或元类的方法列表里查找对应的IMP
。TailCallFunctionPointer
调用查找到的IMP
。
1.2: MethodTableLookup
汇编源码
.macro MethodTableLookup
SAVE_REGS MSGSEND
// /* method lookup */
// enum {
// LOOKUP_INITIALIZE = 1,
// LOOKUP_RESOLVER = 2,
// LOOKUP_NIL = 4,
// LOOKUP_NOCACHE = 8,
// };
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
// x2 = x16 = class
mov x2, x16
// x3 = LOOKUP_INITIALIZE | LOOKUP_RESOLVER = 3
mov x3, #3
// 官方的注释说明了一切,调用lookUpImpOrForward函数
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// bl:b:跳转 l:链接寄存器 //在跳转到之前_lookUpImpOrForward之前,
// 将下一条指令的地址保存到lr寄存器中,既将(mov x17, x0)的指令地址保存在lr中
// 当_lookUpImpOrForwar执行完以后,执行lr寄存器中的地址
bl _lookUpImpOrForward
// x0即是第一个寄存器,同时也是返回值寄存器,此处是_lookUpImpOrForward的返回值IMP
// IMP in x0
// x17 = x0 = IMP
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
复制代码
- 官方注释说明了一切,
x0
为receiver
,x1
为selector
,mov x2, x16
,将x16
(类或元类)赋值给x2
,mov x3, #3
,将x3
赋值为3
,然后调用函数lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
。 mov x17, x0
,x0
即是第一个寄存器,同时也是返回值寄存器,此处是_lookUpImpOrForward
的返回值IMP
,将查找到的IMP
赋值给x17
。
1.3: TailCallFunctionPointer
汇编源码
#if __has_feature(ptrauth_calls)
// JOP
.macro TailCallFunctionPointer
// $0 = function pointer value
braaz $0
.endmacro
// JOP
#else
// not JOP
.macro TailCallFunctionPointer
// $0 = function pointer value
// 跳转$0,即调用IMP
br $0
.endmacro
// not JOP
#endif
复制代码
br $0
,跳转$0
,即调用IMP
。
1.4: __objc_msgSend_uncached
流程图
2: lookUpImpOrForward
函数流程
前面汇编源码里官方注释已经指明MethodTableLookup
里的_lookUpImpOrForward
调用的是lookUpImpOrForward
函数,全局搜索_lookUpImpOrForward
也只能在汇编文件里找到调用代码,没有实现代码,所以全局搜索lookUpImpOrForward
函数,按住command
点击文件名前面的小箭头折叠所有文件,然后再次点击objc-runtime-new.mm
文件的小箭头,打开并找到lookUpImpOrForward
函数查看相关源码。
2.1: lookUpImpOrForward
函数源码解析
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
// behavior = 3 = LOOKUP_INITIALIZE | LOOKUP_RESOLVER
// 指定消息转发的forward_imp
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// 判断类或元类是否初始化,如果没有初始化,
// behavior = LOOKUP_INITIALIZE | LOOKUP_RESOLVER | LOOKUP_NOCACHE = 11
// 二进制为1011
if (slowpath(!cls->isInitialized())) {
// The first message sent to a class is often +new or +alloc, or +self
// which goes through objc_opt_* or various optimized entry points.
//
// However, the class isn't realized/initialized yet at this point,
// and the optimized entry points fall down through objc_msgSend,
// which ends up here.
//
// We really want to avoid caching these, as it can cause IMP caches
// to be made with a single entry forever.
//
// Note that this check is racy as several threads might try to
// message a given class for the first time at the same time,
// in which case we might cache anyway.
behavior |= LOOKUP_NOCACHE;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
// 加锁,保证线程安全
runtimeLock.lock();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
// 检查类是否注册(是否是被dyld加载的类),防止被伪装的类进行攻击
checkIsKnownClass(cls);
// 递归实现类、父类和元类
// 初始化类和父类
// 此处不分析,后面单独发文分析类的加载和初始化
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookup the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
// 获取锁后,代码再次查找类的缓存,但绝大多数情况下,证据表明大部分时间都未命中,因此浪费时间。
// 唯一没有执行某种缓存查找的代码路径就是class_getInstanceMethod()。
// unreasonableClassCount,类迭代上限,函数注释翻译得到
// 死循环根据sel查找IMP,根据break,goto等语句退出
for (unsigned attempts = unreasonableClassCount();;) {
// 判断是否有共享缓存缓存优化,一般是系统的方法比如NSLog,一般的方法不会走
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
/*
支持共享缓存,再次查询共享缓存,目的可能在你查询过程中
别的线程可能调用了这个方法,共享缓存中有了
*/
// 根据sel在缓存中查找IMP
imp = cache_getImp(curClass, sel);
// 找到IMP就跳转done_unlock流程
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// 二分查找法在curClass中根据sel查找IMP
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) { // 找到了sel对于的方法
imp = meth->imp(false); // 获取对于的IMP
goto done; // 跳转done流程
}
// 获取父类,父类为nil,走if里面的流程,不为nil,就继续下面流程
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 按照继承链(cls->supercls->nil)一直查找到nil都没有查找到sel对应的IMP
// 动态方法决议也没起作用,就开始消息转发
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.
// 父类缓存中根据sel查找IMP
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
// 如果父类返回的是forward_imp,停止查找,跳出循环
// 但是不要缓存,首先调用此类的动态方法决议(下面的resolveMethod_locked)
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
// 在父类缓存中根据sel找到了IMP,进入done流程,在此类中缓存它。
goto done;
}
}
// No implementation found. Try method resolver once.
// 未找到实现。尝试一次动态方法决议。
// behavior = 3 = 0011 LOOKUP_RESOLVER = 2 = 0010
// 0011 & 0010 = 0010 = 2,条件成立
// 再次判断behavior = 1 == 0001,0001 & 0010 = 0,条件不成立
// 动态方法决议只执行一次
if (slowpath(behavior & LOOKUP_RESOLVER)) {
// behavior = 3 ^ 2 = 0011 ^ 0010 = 0001 = 1
behavior ^= LOOKUP_RESOLVER;
// 动态方法决议
return resolveMethod_locked(inst, sel, cls, behavior);
}
done: // 在本类或父类方法列表中或者父类缓存中根据sel找到了IMP
// 不是LOOKUP_NOCACHE,即不是+new or +alloc, or +self等方法
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES // 支持共享缓存,相关处理
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
// 将查询到的sel和IMP插入类的缓存 注意:插入的是消息接收者的类的缓存
// 到这里就跟前面的cache_t探索的内容联系起来了
// cache_t的读、写流程到这里就有了一个闭环
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
// 解锁
runtimeLock.unlock();
/*
如果(behavior & LOOKUP_NIL)成立
imp == forward_imp,没有找到IMP,且动态方法决议没起作用
直接返回nil
*/
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
复制代码
2.1.1: 慢速查找流程分析
慢速查找流程
- 检查
receiver
(消息接收者)的类是否注册,如果没注册直接报错。 - 判断
cls
(类或元类)是否实现和初始化,如果没有就递归实现和初始化cls
以及相关的继承链
和isa指向链
中的父类
和元类
(原因:因为方法查找时本类找不到,会向父类查找,直到没有父类为止;同时类方法存在元类中)。
递归循环查找
查找receiver
(消息接收者)的类
- 判断是否有共享缓存,因为可能在查询过程中,所查的方法被调用缓存了,如果有就直接从共享缓存中取,没有就开始在
receiver
(消息接收者)的类中查询。 - 采用二分查找法在
receiver
(消息接收者)的类的方法列表中根据sel
查找对应的IMP
。 - 如果
receiver
(消息接收者)的类的方法列表中没找到IMP
,就获取父类,开始递归父类查找。
查找父类(或父元类)的缓存(curClass = superclass
)
- 如果父类链中有一个循环了,就报错并停止。
- 父类缓存中根据
sel
查找IMP
,如果在父类缓存中没找到了IMP
,就开始查找父类方法列表。
查询父类(或父元类)的方法列表(curClass = superclass
)
- 采用二分查找法在父类的方法列表中根据
sel
查找IMP
,没找到就继续获取父类的父类,先查找缓存,再查找方法列表,一直递归到父类为nil
为止。
方法存在
- 如果在
本类或父类链方法列表中
或者父类缓存中
根据sel
找到了IMP
,就跳出循环,判断是否需要插入缓存(+new or +alloc, or +self
等方法不需要),需要的话就将IMP
和sel
插入receiver
(消息接收者)的类的缓存中。
动态方法决议
- 如果父类链缓存中返回的是
消息转发
或者递归完所有父类之后都没有找到对应的IMP
,则跳出循环,判断是否执行过动态方法决议,没有就执行一次动态方法决议,动态方法决议会再次调用慢速查找流程(后续不会再执行动态方法决议)。
消息转发
- 如果一直查找到父类为nil都没有查找到对应的
IMP
,且动态方法决议也没起作用,就将消息转发的forward_imp
插入缓存中,开始消息转发流程。
2.1.2: lookUpImpOrForward
流程图
2.2: 实现和初始化类
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
runtimeLock.assertLocked();
// !cls->isRealized()小概率发生 cls->isRealized()大概率是YES
// 判断类是否实现 目的是实现isa指向链和父类链相关的类。
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// 判断类是否初始化,没有就先初始化
if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
return cls;
}
复制代码
- 递归实现类以及类的
isa
指向链和父类链相关的类。 - 初始化类和父类链相关的类(一直递归到
nil
)。
2.3: 二分查找法
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin(); // 第一个method的位置
auto base = first;
decltype(first) probe;
// 将key直接转换成uintptr_t值,因为修复过后的method_list_t中的元素是排过序的
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// count = 数组的个数,count >> 1 相当于 count / 2 取整
// count >>= 1 = (count = count >> 1) = (count = count / 2)
/*
案例一:例如 count = 8 需要查找的sel的index为2
1(第一次).count = 8
2(第二次).count = (8 >>= 1) = 4
*/
/*
案例二:例如 count = 8 需要查找的sel的index为7
1(第一次).count = 8
2(第二次).count = (7 >>= 1) = 3(下面count--了)
3(第三次).count = (2 >>= 1) = 1(下面count--了)
*/
for (count = list->count; count != 0; count >>= 1) {
// 内存平移,获取探查值(中间值)
/*
案例一:
1.probe = 首地址(0) + (count / 2) = 0 + 4
2.prebe = 0 + (4 / 2) = 2
*/
/*
案例二:
1.probe = 首地址(0) + (count / 2) = 0 + 4
2.probe = 5 + (3 / 2) = 6
3.probe = 7 + (1 / 2) = 7
*/
probe = base + (count >> 1);
// 获取探查的sel的uintptr_t值
uintptr_t probeValue = (uintptr_t)getName(probe);
/*
案例一:
1.key = 2,probe = 4,不相等
2.key = 2,prebe = 2,相等,返回method_t *
*/
/*
案例二:
1.key = 7,probe = 4,不相等
2.key = 7,prebe = 6,不相等
3.key = 7,probe = 7,相等,返回method_t *
*/
if (keyValue == probeValue) { // 如果目标sel的uintptr_t值和探查sel的uintptr_t值匹配成功
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 探查值不是第一个 && 上一个sel的uintptr_t值也等于keyValue
// 说明有分类同名方法覆盖,后去分类的方法,多个分类取最后编译的
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
// 返回
return &*probe;
}
// 如果keyValue > 探查值
/*
案例一:
1. 2 不大于 4,不进入,继续循环
*/
/*
案例二:
1. 7 大于 4,进入
2. 7 大于 6,进入
*/
if (keyValue > probeValue) {
/*
案例二:
1.base = 4 + 1 = 5,count-- = 8-- = 7
2.base = 6 + 1 = 7,count-- = 3-- = 2
*/
base = probe + 1;
count--;
}
}
return nil; // 查询完没找到就返回nil
}
复制代码
备注:方法列表里面的方法是经过fixupMethodList
函数按选择器地址排序过的。
二分查找法
就是每次取范围内的中间值
为探查值
与要查找的目标进行比较,相等,如果没有分类同名方法,就直接返回查找到的方法。- 如果分类有同名方法,就返回分类的方法,如果有多个分类同名方法,就返回最后加载的分类的同名方法
- 不相等就一直缩小查找的范围继续查找,如果到最后都没有找到就返回
nil
。
2.4: 二分查找法案例图解
备注:base
作为探查范围的起始index,count
为每次探查的个数。
2.5: cache_getImp
慢速查找流程中,当类支持共享缓存或者每次获得父类后,都会调用cache_getImp
开始快速查找流程
来查询缓存,按住command
加control
鼠标左键点击查看,发现在C++
文件里cache_getImp
只有声明:
由前文iOS runtime之方法的本质objc_msgSend分析一经验可知快速查找流程是使用汇编源码实现的,全局搜索cache_getImp
,进入objc-msg-arm64.s
查看相关定义:
// 传入参数 p0 = class p1 = sel
STATIC_ENTRY _cache_getImp
// // 将class存入p16
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
// 如果没有找到缓存,直接返回nil,p0是第一个寄存器,也是返回值寄存器
LGetImpMissDynamic:
mov p0, #0
ret
LGetImpMissConstant:
mov p0, p2
ret
END_ENTRY _cache_getImp
复制代码
GetClassFromIsa_p16
宏定义此流程传入参数src = class, needs_auth = 0
,因为class
在之前的慢速查找流程中已经验证过了,所以这里直接赋值p16 = class
就可以了。
// 传入参数 src = class, needs_auth = 0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
#if SUPPORT_INDEXED_ISA // armv7k or arm64_32
// Indexed isa
mov p16, \src // optimistically set dst = src
tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f // done if not non-pointer isa
// isa in p16 is indexed
adrp x10, _objc_indexed_classes@PAGE
add x10, x10, _objc_indexed_classes@PAGEOFF
ubfx p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS // extract index
ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:
#elif __LP64__ // true arm64
.if \needs_auth == 0 // _cache_getImp takes an authed class already
// 走这里
// _cache_getImp拿到的是已经验证过的class
// 直接赋值 p16 = class
mov p16, \src
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
复制代码
CacheLookup
流程在前文iOS runtime之方法的本质objc_msgSend分析一中已经分析过了,此处只是传入参数不一样CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
。
- 如果缓存没有命中就执行
LGetImpMissDynamic
流程。
// 如果没有找到缓存,直接返回nil,p0是第一个寄存器,也是返回值寄存器
LGetImpMissDynamic:
mov p0, #0
ret
复制代码
- 如果缓存命中就走
CacheHit
宏定义里面的GETIMP
流程。
// 传入参数Mode($0) = GETIMP
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP // 走这里
// p0 = p17(imp)
mov p0, p17
// cbz 比较,为零则跳转;
// 如果imp = 0则调整9流程,return 0
cbz p0, 9f // don't ptrauth a nil imp
// imp ^= class,imp解码
AuthAndResignAsIMP x0, x10, 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, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
复制代码
p17 = imp
,将p17
赋值给p0
。- 如果
imp = 0
直接调9
流程,return 0
。 AuthAndResignAsIMP
宏定义将imp
解码(cache_t::insert
插入时会对imp
进行编码),然后返回解码的imp
。
.macro AuthAndResignAsIMP
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = class
// imp解码
$0 = $0 ^ $3 = imp ^ class
eor $0, $0, $3
.endmacro
复制代码
- 对从缓存中获取的
imp
进行解码。
2.6: 为什么缓存查找使用汇编,其他却用C++
- 汇编更加接近机器语言,速度更快,效率更高,最大化发挥缓存的优势和意义。
- 汇编更安全。
- C语言函数的参数必须明确指定,汇编可以动态指定,更加灵活。
3: unrecognized selector sent to xxx
3.1: 单身狗没有女朋友案例
首先来看一个案例:单身狗没有女朋友案例。
创建一个SDSingleDog
类,声明一个girlfriend
方法,不实现(单身狗没有女朋友
)。
#import <Foundation/Foundation.h>
#import "SDSingleDog.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
SDSingleDog *singleDog = [SDSingleDog alloc];
[singleDog girlfriend];
}
return 0;
}
复制代码
运行程序,打印输出,崩溃(所以没有女朋友是很崩溃的事
)。
从这个案例可以看出,如果方法快速查找和方法慢速查找没有找到相应的方法、动态方法决议和消息转发都没有实现的情况下,就会抛出unrecognized selector sent to xxx
的经典异常,
3.2: unrecognized selector
源码探索
全局搜索unrecognized selector sent to
,找到3个相关的函数和方法。
但是打上断点,发现程序不会进入这3个地方,查看方法注释和函数调用栈,发现实际调用的是CoreFoundation
里面的方法,而CoreFoundation
并未开源,所以后续分析消息转发时再来探索。
4: 类调用NSObject
对象方法成功原因分析
4.1: 案例代码
@interface NSObject (Goddess)
- (void)kneelAndLick;
@end
@implementation NSObject (Goddess)
- (void)kneelAndLick
{
NSLog(@"Single dog kneel and lick Goddess");
}
@end
#import <Foundation/Foundation.h>
#import "SDSingleDog.h"
#import "NSObject+Goddess.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 类对象调用实例对象方法
[SDSingleDog kneelAndLick];
}
return 0;
}
*********************** 打印输出 ***********************
2021-07-08 16:40:25.510538+0800 KCObjcBuild[4157:161599] Single dog kneel and lick Goddess
Program ended with exit code: 0
复制代码
4.2: 案例分析
- OC底层没有所谓的对象方法和类方法之分,类也是元类的类对象,类方法是以对象方法的形式存在元类里面的。
- 根元类的父类是根类,当根元类没有查找到目标方法后,就会查找到根类,然后就将根类的对象方法返回了。
5: 预告
方法的查找流程实际是个很复杂的流程。现在已经探讨了方法快速查找流程和慢速查找流程,后面还有动态方法决议,以及消息转发流程,敬请期待。