欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
写在前面
书接上文说到cache_t
缓存的是方法,那么方法又是什么呢?这一切都要从Runtime
开始说起
一、Runtime
1.什么是Runtime?
Runtime
是一套API
,由c、c++、汇编
一起写成的,为OC
提供了运行时
- 运行时:代码跑起来,将可执行文件装载到内存
- 编译时:正在编译的时间——翻译源代码将高级语言(OC、Swift)翻译成机器语言(汇编等),最后变成二进制
2.Runtime版本
Runtime有两个版本——Legacy
和Modern
,苹果开发者文档都写得清清楚楚
源码中-old
、__OBJC__
代表Legacy
版本,-new
、__OBJC2__
代表Modern
版本,以此做兼容
3.Runtime的作用及调用
Runtime
底层经过编译会提供一套API和供FrameWork
、Service
使用
Runtime
调用方式:
- Runtime API,如 sel_registerName()
- NSObject API,如 isKindOf()
- OC上层方式,如 @selector()
原来平常在用的这么多方法都是Runtime啊,那么方法究竟是什么呢?
二、方法的本质
1.研究方法
通过clang编译成cpp文件
可以看到底层代码,得到方法的本质
- 兼容编译(代码少):clang -rewrite-objc main.m -o main.cpp
- 完整编译(不报错):xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
2.代码转换
FXPerson *p = [FXPerson alloc];
[p fly];
FXPerson *p = ((FXPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("FXPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("fly"));
((FXPerson *(*)(id, SEL))(void *)
是类型强转(id)objc_getClass("FXPerson")
获取FXPerson类对象sel_registerName("alloc")
等同于@selector()
那么可以理解为((类型强转)objc_msgSend)(对象, 方法调用)
3.方法的本质
方法的本质是通过objc_msgSend
发送消息,id
是消息接收者,SEL
是方法编号
如果外部定义了C函数并调用如
void fly() {}
,在clang编译之后还是fly()
而不是通过objc_msgSend
去调用。因为发送消息就是找函数实现的过程,而C函数可以通过函数名
——指针
就可以找到
4.向不同对象发送消息
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface FXFather: NSObject
- (void)walk;
+ (void)run;
@end
@implementation FXFather
- (void)walk { NSLog(@"%s",__func__); }
+ (void)run { NSLog(@"%s",__func__); }
@end
@interface FXSon: FXFather
- (void)jump;
+ (void)swim;
@end
子类FXSon
有实例方法jump
、类方法swim
父类FXFather
有实例方法walk
、类方法run
①发送实例方法
消息接收者——实例对象
FXSon *s = [FXSon new];
objc_msgSend(s, sel_registerName("jump"));
②发送类方法
消息接收者——类对象
objc_msgSend(objc_getClass("FXSon"), sel_registerName("swim"));
objc_msgSend
不能向父类发送消息,需要使用objc_msgSendSuper
,并给objc_super
结构体赋值(在objc2中只需要赋值receiver
、super_class)
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
③向父类发送实例方法
receiver——实例对象
;super_class——父类类对象
struct objc_super superInstanceMethod;
superInstanceMethod.receiver = s;
superInstanceMethod.super_class = objc_getClass("FXFather");
objc_msgSendSuper(&superInstanceMethod, sel_registerName("walk"));
④向父类发送类方法
receiver——类对象
;super_class——父类元类对象
struct objc_super superClassMethod;
superClassMethod.receiver = [s class];
superClassMethod.super_class = class_getSuperclass(object_getClass([s class]));
objc_msgSendSuper(&superClassMethod, sel_registerName("run"));
如果出现Too many arguments to function call, expected 0, have 2
问题,来到BuildSetting
把配置修改成如下图
三、消息查找流程
消息查找流程其实是通过上层的
方法编号sel
发送消息objc_msgSend
找到具体实现imp
的过程
objc_msgSend
是用汇编写成的,至于为什么不用C而是用汇编写,是因为:
- C语言不能通过写一个函数,保留未知的参数,跳转到任意的指针,而汇编有寄存器
- 对于一些调用频率太高的函数或操作,使用汇编来实现能够提高效率和性能,容易被机器来识别
1.开始查找
打开objc
源码,由于主要研究arm64结构
的汇编实现,来到objc-msg-arm64.s
①开始objc_msgSend
②判断消息接收者
是否为空,为空直接返回
③判断tagged_pointers
(之后会讲到)
④取得对象中的isa
存一份到p13
中(寄存器指令在逆向篇中会讲到)
⑤根据isa
进行mask
地址偏移得到对应的上级对象
(类、元类)
GetClassFromIsa_p16
定义,主要就是进行isa & mask
得到class
操作
(其定义方式与iOS探索 isa初始化&指向分析一文中提到的shiftcls
异曲同工)
⑥开始在缓存中查找imp
——开始了快速流程
2.快速流程
从CacheLookup
开始了快速查找流程(此时x0是sel
,x16是class
)
#CACHE
是个宏定义表示16个字节,[x16, #CACHE]
表示类对象
内存地址偏移16字节
得到cache
。cache
一分为二——8字节的buckets
存放在p10,两个4字节的occupied
和mask
存放在p11
#define CLASS __SIZEOF_POINTER__
#define CACHE (2 * __SIZEOF_POINTER__)
②x1是sel即cmd
,取出p11中的低32位(w11)——mask
,两者进行与运算得到hash下标
存放在x12
③p12先左移动(1+PTRSHIFT)
,再与p10buckets
相加得到新的p12——bucket
④拿出p12bucket
地址所在的值,放在p17imp
和p9sel
中,这点可以从bucket_t
的结构中看出(sel强转成key)用bucket
中的sel
与x1cmd
作对比,如果相同则缓存命中CacheHit
得到其中的imp
;如果不等就跳转⑤
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
MethodCacheIMP _imp;
cache_key_t _key;
#else
cache_key_t _key;
MethodCacheIMP _imp;
#endif
...
}
⑤如果bucket->sel == 0
则CheckMiss
;比较p12bucket
和p10buckets
,如果不相等就将x12bucket
的值进行自减操作(查找上一个bucket
),跳转回④重新循环,直到bucket == buckets
遍历结束跳转⑥
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
⑥平移哈希使得p12 = first bucket
,再重复进行一下类似④⑤⑥的操作——
防止不断循环的过程中多线程并发,正好缓存更新了。如果bucket->sel == 0
走CheckMiss
,如果bucket == buckets
走JumpMiss
,本质是一样的
.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
当NORMAL
时,CheckMiss
和JumpMiss
都走__objc_msgSend_uncached
⑦__objc_msgSend_uncached
调用MethodTableLookup
⑧保存参数调用c++方法进入慢速流程(准备好装备和药水打BOSS)
总结:消息查找的快速流程
可以和cache_t::find
方法对比加深理解
3.慢速流程
汇编
__class_lookupMethodAndLoadCache3
与c++中_class_lookupMethodAndLoadCache3
相对应
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
// initialize = YES , cache = NO , resolver = YES
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 缓存查找,cache为NO直接跳过
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// 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.
// lock是为了防止多线程操作; 类是否被编译
runtimeLock.lock();
checkIsKnownClass(cls);
// 为查找方法做准备条件,如果类没有初始化时,初始化类和父类、元类等
if (!cls->isRealized()) {
realizeClass(cls);
}
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
// 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
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
// 从缓存里面查找一遍,若有直接goto done
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
// 形成局部作用域,避免局部变量命名重复
{
// 在类的方法列表中查找方法,若有直接cache_fill
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
// Try superclass caches and method lists.
{
unsigned attempts = unreasonableClassCount();
// 遍历父类进行查找
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache.
// 在父类缓存中查找,若有直接cache_fill
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
// 在父类的方法列表中查找方法,若有直接cache_fill
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
// 如果方法仍然没找到,就开始做动态方法解析
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 开始消息转发
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
慢速流程主要分为几个步骤:
①_class_lookupMethodAndLoadCache3
调用lookUpImpOrForward
,此时参数initialize=YES cache=NO resolver=YES
②runtimeLock.lock()
为了防止多线程操作
③realizeClass(cls)
为查找方法做准备条件,如果类没有初始化时,初始化类和父类、元类等
④imp = cache_getImp(cls, sel)
为了容错从缓存
中再找一遍,若有goto done⑨
⑤// Try this class's method lists
局部作用域中,在类的方法列表
中查找方法,若有直接log_and_fill_cache
并goto done⑨
⑥// Try superclass caches and method lists
局部作用域中,遍历父类:先在父类缓存
中查找,若有直接log_and_fill_cache
并goto done
;没有再去父类的方法列表中
查找方法,若有直接log_and_fill_cache
并goto done⑨
⑦如果还没找到就动态方法解析_class_resolveMethod
,标记为triedResolver = YES(已自我拯救过)
,动态方法解析结束后跳转慢速流程④
⑧如果动态方法解析之后再找一遍仍然没找到imp
,就抛出错误_objc_msgForward_impcache
得到imp
并cache_fill
⑨done
:多线程解锁,返回imp
接下来拆解步骤进行说明:
cache_getImp
这个方法后续会解释getMethodNoSuper_nolock
遍历调用search_method_list
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
search_method_list
利用二分查找寻找方法
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
// 如果方法列表已经排序好了,则通过二分查找法查找方法,以节省时间
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
// 如果方法列表没有排序好就遍历查找
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
findMethodInSortedMethodList
二分查找算法的具体实现(了解即可)
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// >>1 表示将变量n的各个二进制位顺序右移1位,最高位补二进制0
// count >>= 1 如果count为偶数则值变为(count / 2);如果count为奇数则值变为(count-1) / 2
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
// 取出中间method_t的name,也就是SEL
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 继续向前二分查询
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 取出 probe
return (method_t *)probe;
}
// 如果keyValue > probeValue 则折半向后查询
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
log_and_fill_cache
->cache_fill
->cache_fill_nolock
进行缓存
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill (cls, sel, imp, receiver);
}
_class_resolveMethod
动态方法解析——在找不到imp时的自我拯救操作cls
是元类的话说明调用类方法,走_class_resolveInstanceMethod
;非元类的话调用了实例方法,走_class_resolveInstanceMethod
- 两者逻辑大同小异,主要逻辑是是objc_msgSend函数发送
SEL_resolveInstanceMethod
消息,系统调用resolveInstanceMethod
- 发送消息后,系统会再查找一下sel方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
_objc_msgForward_impcache
在汇编中调用了_objc_msgForward
,然后又进入_objc_forward_handler
,它在c++调用了objc_defaultForwardHandler
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
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);
}
原来unrecognized selector sent to instance xxx
是这么来的啊...
4.消息查找流程图
写在后面
OC的消息机制分为三个阶段:
- 消息查找阶段:从类及父类的方法缓存列表及方法列表查找方法
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现
- 消息转发阶段:如果没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理
本文主要讲了消息查找流程
,顺带提了几句动态方法解析
,下一篇文章将通过案例来详细解读动态方法解析
并着重介绍消息转发机制
最后准备了一份动态方法决议的Demo,有兴趣的小伙伴们可以自己下断点看看方法查找流程
和研究下动态方法决议