Runtime消息机制
1. OC消息机制三个阶段
2. 源码跟读
我们OC的方法调用会转成runtime的API ,objc_msgSend("消息接收者","方法名即SEL");这个形式。
OC消息机制三个阶段
- 消息发送。
- 动态解析方法。
- 消息转发。
消息发送
objc_msgSend给消息接收者发送一条消息,接收者先从自己的cache_t里查找方法有没有缓存(这一阶段在源码里是汇编代码)。若是缓存列表里找不到方法,则去自己的methods方法列表里查找,若仍然没有找到。则通过superClass去找到他的父类的缓存列表里找,若是找了方法,则把方法缓存到自己的方法缓存列表。若是父类的缓存列表找不到方法,则去父类的方法列表里找,若是找到了,则缓存到自己的缓存列表(注意这是直接缓存到自己的缓存列表,没有缓存到父类的缓存列表)若是仍然没找到,再继续找父类的父类缓存列表,这样递归下去,直到找到,缓存到自己的方法列表。若找完基类仍是没有找到方法,则进入第二阶段,动态解析方法。
动态解析方法
系统会回调我们在类里实现的+ (BOOL)resolveInstanceMethod:(SEL)sel方法。在这个方法里我们可以把别的方法的实现交给这个找不到的sel,添加到methods里面,runtime会重新去找一次方法列表methods。
- (void)other
{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
Method method = class_getInstanceMethod(self, @selector(other));
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
return [super resolveInstanceMethod:sel];
}
若是这个resolveInstanceMethod:方法没有实现,或者没有给方法列表添加方法,runtime第二次去方法列表里查找也没找到方法,就会进入第三阶段,消息转发阶段。
消息转发
在消息发送阶段没有找到方法,并且动态解析方法也没有找到方法,当这个类已经没能力处理这个方法时,runtime就会进入消息转发阶段,我们在自己的调用方法的类里实现以下方法
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
return [[Dog alloc]init];
//objc_msgSend("Dog",aSelector)把消息发给d返回的类处理
}
return [super forwardingTargetForSelector:aSelector];
}
把我们找不到的方法转交给Dog类,就相当于objc_msgSend("Dog",aSelector)这么做。如果Dog类里实现了找不到的方法,那么Dog就会调用这个方法。但是如果在这个类里没有实现forwardingTargetForSelector:(SEL)aSelector方法,或者这个方法里没有return 任何东西,或者return了,但是Dog里并没有实现这个方法,就会进入以下阶段,需要我们实现以下两个方法:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(test)) {
//如果这里return nil 则不会调用forwardInvocation:
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
如果上面的方法实现了,并返回了签名,则会runtime会调用下面的方法,方法执行至此,已经不会崩溃,可以在里面做任何事。
- (void)forwardInvocation:(NSInvocation *)anInvocation{
anInvocation.target = [[MJCat alloc]init];
[anInvocation invoke];//把方法给MJCat执行
}
系统回调这两个方法,需要我们传回方法签名,这个签名就是Method_t里的 type成员
格式:v代表返回值void, 16代表参数和返回值总共占16字节,@代表id类型 ,0代表id类型内存从0个字节开始,:代表SEL类型,8代表SEL类型参数内存从第八个字节开始。可以使用@Encoding()方法获取对应类型的符号。
注意点 消息转发阶段的类方法,输入是没有提示的,需要我们先打出成员方法,再把减号改成加号。
源码跟读
第一阶段 消息发送
objc_msgSend("recevier","SEL");源码实现是在objc-msg-arm64文件里,是使用汇编代码实现的,源码如下
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x16, x13, #ISA_MASK // x16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
mov x10, #0xf000000000000000
cmp x0, x10
b.hs LExtTag
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]
b LGetIsaDone
LExtTag:
// 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
LReturnZero:
// x0 is already zero
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
MESSENGER_END_NIL
ret
END_ENTRY _objc_msgSend
- cmp x0, #0 判断x0(消息接收者,即第一个参数)是否是0
- b .le LNilOrTagged 如果小于0则跳转到 LNilOrTagged LNilOrTagged里是 b.eq LReturnZero返回0退出函数
- CacheLookup NORMAL 查找方法缓存(CacheLookup是个宏,根据注释可以看出是去cache_t里查找缓存)
.macro CacheLookup
// x1 = SEL, x16 = isa
ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x9, x17, [x12] // {x9, x17} = *bucket
1: cmp x9, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x9, x17, [x12, #-16]! // {x9, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
- 如果缓存里没有找到方法应该是执行CheckMiss $0这段代码
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz x9, LGetImpMiss
.elseif $0 == NORMAL
cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
- 因为我们在CacheLookup NORMAL步骤里传的是NORMAL所以执行的是 __objc_msgSend_uncached这句代码。
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x16 is the class to search
MethodTableLookup
br x17
END_ENTRY __objc_msgSend_uncached
- 这句代码执行的方法MethodTableLookup是从方法缓存列表去找
.macro MethodTableLookup
// push frame
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)]
// receiver and selector already in x0 and x1
mov x2, x16
bl __class_lookupMethodAndLoadCache3
// 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
.endmacro
- 我们根据注释先忽略那些对寄存器操作的代码,可以看到bl _class_lookupMethodAndLoadCache3这句代码,因为汇编的代码回对runtime 的api前面加 下划线,我们搜索_class_lookupMethodAndLoadCache3,去掉了第一个下划线,全工程搜索,可以在objc-runtime-new.mm文件看到查找方法的具体代码。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;//判断有没有动态方法解析的标记
runtimeLock.assertUnlocked();
//因为在汇编代码里找过一次缓存方法列表,这里过掉从缓存方法列表里找
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
//判断有没有对类重新组织过的意思,与本篇逻辑无关
if (!cls->isRealized()) {
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
//类有没有接收到消息初始化过
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
}
retry:
runtimeLock.assertReading();
imp = cache_getImp(cls, sel);//又从方法缓存里找了一次,因为怕上面代码执行的时候,有调用过方法,把方法加到了缓存列表。
if (imp) goto done;//如果找到方法,则结束返回IMP,返回到汇编代码里的
bl __class_lookupMethodAndLoadCache3 bl跳转的意思,即通过bl指令调用方法
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
{
unsigned attempts = unreasonableClassCount();
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
if (--attempts == 0) {
_objc_fatal("Memory corruption in class list.");
}
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// Superclass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
triedResolver = YES;
goto retry;
}
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlockRead();
return imp;
}
上面代码会先判断类有没有经过runtime重新规划合并类,初始化判断。
然后imp = cache_getImp(cls, sel);在从缓存里找一遍,因为上面重新规划合并类初始化的代码可能会把方法加到缓存列表。
if (imp) goto done;
如果找到方法则跳到done处执行代码runtimeLock.unlockRead();return imp;
结束返回IMP,返回到汇编代码里的
bl __class_lookupMethodAndLoadCache3
bl跳转的意思,即通过bl指令调用方法。
如果缓存里没有找到方法,则继续执行代码,去这个类里面的方法列表methods找方法,找到了则跳到done,返回imp到汇编代码执行。如果找到了就缓存到自己的缓存列表。
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
}
这段代码是具体查找方法,cls->data()->methods拿到类里的class_rw_t(包含了类的各种信息的结构体)
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
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;
}
如果自己的类方法列表里找不到,则去父类的缓存方法列表查找,父类缓存列表找不到,就去父类方法列表里查找,再去父类的父类找,一直递归到基类为止,这个过程中一旦找到方法就会把方法缓存到自己的缓存列表(不是任何一个父类)并且返回IMP到汇编通过bl执行方法。
{
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.");
}
//去父类缓存列表查找.
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 父类缓存列表里有方法,则缓存到自己的缓存列表
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 如果父类缓存列表没有方法,则去父类的方法列表里去查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
以上为runtime机制第一阶段,消息发送阶段源码。
第二阶段 消息转发
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// 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;
}
先判断有没有过动态解析的过程,由与第一次进方法,triedResolver == NO,所以会进这个判断, _class_resolveMethod(cls, sel, inst);这句话会调用我们在类里实现的resolveInstanceMethod:然后goto retry 重新查找,若是我们在resolveInstanceMethod:方法里实现了把方法加到方法列表,则会在类方法列表里找到方法,返回IMP到汇编通过bl 执行我们添加的方法。若是我们没有实现resolveInstanceMethod:或者没有往类里加方法,则还是会走到 if (resolver && !triedResolver)这一步判断,但是由于triedResolver在上次进来对其赋值YES,所以不会执行里面的代码。
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
上面的代码
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
这两句相当与 objc_msgSend(cls, SEL_resolveInstanceMethod, sel)调用我们的类里的resolveInstanceMethod:方法。
至此,第二阶段消息动态解析阶段结束。
第三阶段 消息转发
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
这段找不到源码,但是有前辈整理了这里的原理。这里就是runtime调用我们类里实现的方法
- (id)forwardingTargetForSelector:(SEL)aSelector
若是类里没有实现这个方法或者没有返回能处理消息的类 ,在这一步若是返回的类没有实现调用的方法,还是会报方法找不到的异常,则调用下面的方法 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector,需要我们返回方法签名,若是我们返回了方法签名则会调用下面的方法,猜测是用我们传进去的签名创建一个NSInvocation然后调用我们类里的forwardInvocation:方法传回来。
- (void)forwardInvocation:(NSInvocation *)anInvocation 在这里我们也可以给方法加具体实现,也可以不加,做打印收集信息,方法执行到这一步,已经不会崩溃。