1. 方法调用的本质
本文我们探寻方法调用的本质,首先通过一段代码,将方法调用代码转为c++
代码查看方法调用的本质是什么样的:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
[person test];
// c++底层代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("test"));
// sel_registerName("test") == @selector(test)
通过上述源码可以看出c++
底层代码中方法调用其实都是转化为objc_msgSend
函数,OC
的方法调用也叫消息机制,表示给方法调用者发送消息。
拿上述代码举例,上述代码中实际为给person
实例对象发送一条test
消息,消息接受者是person
,消息名称test
。
在消息发送的过程中分为三个阶段:
- 消息发送阶段:负责从类及父类的缓存列表及方法列表查找方法。
- 动态解析阶段:如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现。
- 消息转发阶段:如果也没有实现动态解析方法,则会进行消息转发阶段,将消息转发给可以处理消息的接受者来处理。
如果消息转发也没有实现,就会报方法找不到的错误,无法识别消息:unrecognzied selector sent to instance
接下来我们通过源码探寻消息发送者的三个阶段分别是如何实现的。
2. 消息发送阶段
在runtime
源码中搜索_objc_msgSend
查看其内部实现,在objc-msg-arm64.s
汇编文件可以知道_objc_msgSend
函数的实现
objc源码路径:https://opensource.apple.com/source/objc4/objc4-756.2/runtime/Messengers.subproj/objc-msg-arm64.s.auto.html
// 入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // p0 为第一个参数消息接受者receiver
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // 如果p0为nil,跳转到 LNilOrTagged
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend // 查找缓存的方法的入口
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // 跳转到 LReturnZero 返回
// 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
CacheLookup
↓↓↓↓↓
.macro CacheLookup
...
...
...
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
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.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // 找到缓存方法 call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // 没有找到缓存方法 miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
CheckMiss
↓↓↓↓↓
.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
__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
↓↓↓↓↓
.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 // 最终会来到C语言函数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
上述汇编源码中会首先判断消息接受者reveiver
的值。
如果传入的消息接受者为nil
,则会执行LNilOrTagged
,LNilOrTagged
内部会执行LReturnZero
,而LReturnZero
内部则直接return 0
。
如果传入的消息接受者不为nil
,则执行CacheLookup
,内部对方法缓存列表进行查找,如果找到则执行CacheHit
,进而调用方法。否则执行CheckMiss
,CheckMiss
内部调用__objc_msgSend_uncached
。
__objc_msgSend_uncached
内会执行MethodTableLookup
也就是方法列表查找,MethodTableLookup
则会执行lookUpImpOrForward
方法。
最终会来到C语言的lookUpImpOrForward
函数。
Tips:
老版本的runtime
源码会多一个__class_lookupMethodAndLoadCache3
函数,内部也会执行lookUpImpOrForward
函数
首先通过一张图看一下汇编语言中_objc_msgSend
的运行流程。
objc源码路径:https://opensource.apple.com/source/objc4/objc4-756.2/runtime/objc-runtime-new.mm.auto.html
2.1 方法查找的核心逻辑
_class_lookupMethodAndLoadCache3
函数
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
lookUpImpOrForward
函数
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
// initialize = YES , cache = NO , resolver = YES
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// 缓存查找, 因为cache传入的为NO, 这里不会进行缓存查找的过程, 因为在这步之前,汇编语言中CacheLookup已经查找过
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.lock();
checkIsKnownClass(cls);
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (initialize && !cls->isInitialized()) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
retry:
runtimeLock.assertLocked();
// 防止动态添加方法,缓存会变化,所以再次查找缓存
imp = cache_getImp(cls, sel);
// 如果查找到imp, 直接调用done, 返回方法地址
if (imp) goto done;
// 查找方法列表, 传入类对象(或元类对象)和方法名
{
// 根据sel去类对象里面查找方法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 如果方法存在,则缓存方法,
// 内部调用的就是 cache_fill 上文中已经详细讲解过这个方法,这里不在赘述了。
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
// 方法缓存之后, 取出imp, 调用done返回imp
imp = meth->imp;
goto done;
}
}
// 如果类方法列表中没有找到, 则去父类的缓存中或方法列表中查找方法
{
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) {
// 在父类中找到方法, 在本类中缓存方法, 注意这里传入的是cls, 将方法缓存在本类缓存列表中, 而非父类中
log_and_fill_cache(cls, imp, sel, inst, curClass);
// 执行done, 返回imp
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;
// 执行done, 返回imp
goto done;
}
}
}
// ---------------- 消息发送阶段完成 ---------------------
// ---------------- 进入动态解析阶段 ---------------------
// 上述列表中都没有找到方法实现, 则尝试解析方法
if (resolver && !triedResolver) {
runtimeLock.unlock();
resolveMethod(cls, sel, inst);
runtimeLock.lock();
triedResolver = YES;
goto retry;
}
// ---------------- 动态解析阶段完成 ---------------------
// ---------------- 进入消息转发阶段 ---------------------
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
// 返回方法地址
return imp;
}
getMethodNoSuper_nolock
函数
在方法列表中查找方法:
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
assert(cls->isRealized());
// cls->data() 得到的是 class_rw_t
// class_rw_t->methods 得到的是methods二维数组
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
// mlists 为 method_list_t
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
上述源码中getMethodNoSuper_nolock
函数中通过遍历方法列表拿到method_list_t
最终通过search_method_list
函数查找方法:
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 {
// 否则则遍历列表查找
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 指向数组中间的值
probe = base + (count >> 1);
// 取出中间method_t的name,也就是SEL
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
// 取出 probe
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
// 返回方法
return (method_t *)probe;
}
// 如果keyValue > probeValue 则折半向后查询
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
至此为止,消息发送阶段已经完成。
2.2 总结
我们通过一站图来看一下lookUpImpOrForward
函数内部消息发送的整个流程
如果消息发送阶段没有找到方法,就会进入动态解析方法阶段。
3. 动态解析阶段
当本类包括父类cache
包括class_rw_t
中都找不到方法时,就会进入动态方法解析阶段。我们来看一下动态解析阶段源码。
动态解析的方法
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;
}
_class_resolveMethod
函数内部,根据类对象或元类对象做不同的操作
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);
}
}
}
上述代码中可以发现,动态解析方法之后,会将triedResolver = YES;
那么下次就不会在进行动态解析阶段了,之后会重新执行retry
,会重新走方法查找的流程。也就是说无论我们是否实现动态解析方法,无论动态解析方法是否成功,retry
之后都不会在进行动态的解析方法了,假如动态解析没有方法的时候,会去到动态转发。
3.1 如何动态解析方法
动态解析对象方法时,会调用+(BOOL)resolveInstanceMethod:(SEL)sel
方法。
动态解析类方法时,会调用+(BOOL)resolveClassMethod:(SEL)sel
方法。
这里以实例对象为例通过代码来看一下动态解析的过程
@implementation Person
- (void) other {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 给test方法动态的添加方法实现
if (sel == @selector(test)) {
// 获取other方法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other));
// 动态添加test方法的实现为other方法的实现
class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
// 返回YES表示有动态添加方法
return YES;
}
NSLog(@"%s", __func__);
return [super resolveInstanceMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
[person test];
}
return 0;
}
输出:
-[Person other]
上述代码中可以看出,person
在调用test
方法时经过动态解析成功调用了other
方法。
通过上面对消息发送的分析我们知道,当本类和父类cache
和class_rw_t
中都找不到方法时,就会进行动态解析的方法,也就是说会自动调用类的resolveInstanceMethod:
方法进行动态查找。因此我们可以在resolveInstanceMethod:
方法内部使用class_addMethod
动态的添加方法实现。
这里需要注意class_addMethod
用来向具有给定名称和实现的类添加新方法,class_addMethod
将添加一个方法实现的覆盖,但是不会替换已有的实现。也就是说如果上述代码中已经实现了-(void)test
方法,则不会再动态添加方法,这点在上述源码中也可以体现,因为一旦找到方法实现就直接return imp
并调用方法了,不会再执行动态解析方法了。
class_addMethod
函数
我们来看一下class_addMethod
函数的参数分别代表什么。
/**
第一个参数: cls:给哪个类添加方法
第二个参数: SEL name:添加方法的名称
第三个参数: IMP imp: 方法的实现,函数入口,函数名可与方法名不同(建议与方法名相同)
第四个参数: types :方法类型,需要用特定符号,参考API
*/
class_addMethod(__unsafe_unretained Class cls, SEL name, IMP imp, const char *types)
上述参数上文中已经详细讲解过,这里不再赘述。
需要注意的是我们在上述代码中通过class_getInstanceMethod
获取Method
的方法
// 获取other方法 指向method_t的指针
Method otherMethod = class_getInstanceMethod(self, @selector(other));
其实Method
是objc_method
类型结构体,可以理解为其内部结构同method_t
结构体相同,上文中提到过method_t
是代表方法的结构体,其内部包含SEL、type、IMP
,我们通过自定义method_t
结构体,将objc_method
强转为method_t
查看方法是否能够动态添加成功。
struct method_t {
SEL sel;
char *types;
IMP imp;
};
- (void) other {
NSLog(@"%s", __func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// 动态的添加方法实现
if (sel == @selector(test)) {
// Method强转为method_t
struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
NSLog(@"%s,%p,%s",method->sel,method->imp,method->types);
// 动态添加test方法的实现
class_addMethod(self, sel, method->imp, method->types);
// 返回YES表示有动态添加方法
return YES;
}
NSLog(@"%s", __func__);
return [super resolveInstanceMethod:sel];
}
输出:
2020-02-11 16:48:19.371934+0800 Runtime的本质3[22914:12016218] other,0x10f548f80,v16@0:8
2020-02-11 16:48:19.372410+0800 Runtime的本质3[22914:12016218] -[Person other]
动态添加c语言的函数实现
可以看出确实可以打印出相关信息,那么我们就可以理解为objc_method
内部结构同method_t
结构体相同,可以代表类定义中的方法。
另外上述代码中我们通过method_getImplementation
函数和method_getTypeEncoding函
数获取方法的实现imp
和编码类型type
。当然我们也可以通过自己写的方式来调用,这里以动态添加有参数的方法为例。
+(BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat:)) {
class_addMethod(self, sel, (IMP)cook, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void cook(id self ,SEL _cmd,id Num)
{
// 实现内容
NSLog(@"%@的%@方法动态实现了,参数为%@",self,NSStringFromSelector(_cmd),Num);
}
上述代码中当调用eat:
方法时,动态添加了cook
函数作为其实现并添加id
类型的参数。
3.2 动态解析类方法
当动态解析类方法的时候,就会调用+(BOOL)resolveClassMethod:(SEL)sel
函数,而我们知道类方法是存储在元类对象里面的,因此cls
第一个对象需要传入元类对象以下代码为例
void other(id self, SEL _cmd)
{
NSLog(@"other - %@ - %@", self, NSStringFromSelector(_cmd));
}
+ (BOOL)resolveClassMethod:(SEL)sel
{
if (sel == @selector(test)) {
// 第一个参数是object_getClass(self),传入元类对象。
class_addMethod(object_getClass(self), sel, (IMP)other, "v16@0:8");
return YES;
}
return [super resolveClassMethod:sel];
}
我们在上述源码的分析中提到过,无论我们是否实现了动态解析的方法,系统内部都会执行retry
对方法再次进行查找,那么如果我们实现了动态解析方法,此时就会顺利查找到方法,进而返回imp
对方法进行调用。如果我们没有实现动态解析方法。就会进行消息转发。
3.3 总结
用流程图来总结一下动态解析方法的流程:
4. 消息转发阶段
如果我们自己也没有对方法进行动态的解析,那么就会进行消息转发:
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
自己没有能力处理这个消息的时候,就会进行消息转发阶段,会调用_objc_msgForward_impcache
函数。
通过搜索可以在汇编中找到__objc_msgForward_impcache
函数实现,__objc_msgForward_impcache
函数中调用__objc_msgForward
进而找到__objc_forward_handler
。
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
objc源码路径:https://opensource.apple.com/source/objc4/objc4-756.2/runtime/objc-runtime.mm.auto.html
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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
4.1 forwardingTargetForSelector
有消息接收者
我们发现这仅仅是一个错误信息的输出。
其实消息转发机制是不开源的,但是我们可以猜测其中可能拿返回的对象调用了objc_msgSend
,重走了一遍消息发送,动态解析,消息转发的过程。最终找到方法进行调用。
我们通过代码来看一下,首先创建Car
类继承自NSObject
,并且Car
有一个- (void) driving
方法,当Person
类实例对象失去了驾车的能力,并且没有在开车过程中动态的学会驾车,那么此时就会将开车这条信息转发给Car
,由Car
实例对象来帮助person
对象驾车。
#import "Car.h"
@implementation Car
- (void) driving
{
NSLog(@"car driving");
}
@end
--------------
#import "Person.h"
#import
#import "Car.h"
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(driving)) {
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
--------------
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
[person driving];
}
return 0;
}
// 打印内容
// 消息转发[3452:1639178] car driving
由上述代码可以看出,当本类没有实现方法,并且没有动态解析方法,就会调用forwardingTargetForSelector
函数,进行消息转发,我们可以实现forwardingTargetForSelector
函数,在其内部将消息转发给可以实现此方法的对象。
4.2 forwardingTargetForSelector
没有消息接收者
如果forwardingTargetForSelector
函数返回为nil
或者没有实现的话,就会调用methodSignatureForSelector
方法,用来返回一个方法签名,这也是我们正确跳转方法的最后机会。
如果methodSignatureForSelector
方法返回正确的方法签名就会调用forwardInvocation
方法,forwardInvocation
方法内提供一个NSInvocation
类型的参数,NSInvocation
封装了一个方法的调用,包括方法的调用者,方法名,以及方法的参数。在forwardInvocation
函数内修改方法调用对象即可。
如果methodSignatureForSelector
返回的为nil
,就会来到doseNotRecognizeSelector:
方法内部,程序crash
提示无法识别选择器unrecognized selector sent to instance
。
我们通过以下代码进行验证:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(driving)) {
// 返回nil则会调用methodSignatureForSelector方法
return nil;
// return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving)) {
// return [NSMethodSignature signatureWithObjCTypes: "v@:"];
// return [NSMethodSignature signatureWithObjCTypes: "v16@0:8"];
// 也可以通过调用Car的methodSignatureForSelector方法得到方法签名,这种方式需要car对象有aSelector方法
return [[[Car alloc] init] methodSignatureForSelector: aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
// anInvocation.target 方法调用者
// anInvocation.selector 方法名
// [anInvocation getArgument: NULL atIndex: 0]; 获得参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
// anInvocation中封装了methodSignatureForSelector函数中返回的方法。
// 此时anInvocation.target 还是person对象,我们需要修改target为可以执行方法的方法调用者。
// anInvocation.target = [[Car alloc] init];
// [anInvocation invoke];
[anInvocation invokeWithTarget: [[Car alloc] init]];
}
// 打印内容
// [25864:13104198] car driving
上述代码中可以发现方法可以正常调用。接下来我们来看一下消息转发阶段的流程图
4.3 NSInvocation
封装方法的调用
methodSignatureForSelector
方法中返回的方法签名,在forwardInvocation
中被包装成NSInvocation
对象,NSInvocation
提供了获取和修改方法名、参数、返回值等方法,也就是说,在forwardInvocation
函数中我们可以对方法进行最后的修改。
同样上述代码,我们为driving
方法添加返回值和参数,并在forwardInvocation
方法中修改方法的返回值及参数。
#import "Car.h"
@implementation Car
- (int) driving:(int)time
{
NSLog(@"car driving %d",time);
return time * 2;
}
@end
#import "Person.h"
#import
#import "Car.h"
@implementation Person
- (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(driving)) {
return nil;
}
return [super forwardingTargetForSelector:aSelector];
}
// 方法签名:返回值类型、参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving:)) {
// 添加一个int参数及int返回值type为 i@:i
return [NSMethodSignature signatureWithObjCTypes: "i@:i"];
}
return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封装了一个方法调用,包括:方法调用者,方法,方法的参数
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
int time;
// 获取方法的参数,方法默认还有self和cmd两个参数,因此新添加的参数下标为2
[anInvocation getArgument: &time atIndex: 2];
NSLog(@"修改前参数的值 = %d",time);
time = time + 10; // time = 110
NSLog(@"修改前参数的值 = %d",time);
// 设置方法的参数 此时将参数设置为110
[anInvocation setArgument: &time atIndex:2];
// 将tagert设置为Car实例对象
[anInvocation invokeWithTarget: [[Car alloc] init]];
// 获取方法的返回值
int result;
[anInvocation getReturnValue: &result];
NSLog(@"获取方法的返回值 = %d",result); // result = 220,说明参数修改成功
result = 99;
// 设置方法的返回值 重新将返回值设置为99
[anInvocation setReturnValue: &result];
// 获取方法的返回值
[anInvocation getReturnValue: &result];
NSLog(@"修改方法的返回值为 = %d",result); // result = 99
}
#import
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
// 传入100,并打印返回值
NSLog(@"[person driving: 100] = %d",[person driving: 100]);
}
return 0;
}
输出:
2020-02-13 15:09:24.527271+0800 Runtime的本质3[27478:13129349] 修改前参数的值 = 100
2020-02-13 15:09:24.527379+0800 Runtime的本质3[27478:13129349] 修改前参数的值 = 110
2020-02-13 15:09:24.527461+0800 Runtime的本质3[27478:13129349] car driving 110
2020-02-13 15:09:24.527536+0800 Runtime的本质3[27478:13129349] 获取方法的返回值 = 220
2020-02-13 15:09:24.527673+0800 Runtime的本质3[27478:13129349] 修改方法的返回值为 = 99
2020-02-13 15:09:24.527806+0800 Runtime的本质3[27478:13129349] [person driving: 100] = 99
从上述打印结果可以看出forwardInvocation
方法中可以对方法的参数及返回值进行修改。
并且我们可以发现,在设置tagert
为Car
实例对象时,就已经对方法进行了调用,而在forwardInvocation
方法结束之后才输出返回值。
通过上述验证我们可以知道只要来到forwardInvocation
方法中,我们便对方法调用有了绝对的掌控权,可以选择是否调用方法,以及修改方法的参数返回值等等。
4.4 类方法的消息转发
类方法消息转发同对象方法一样,同样需要经过消息发送,动态方法解析之后才会进行消息转发机制。我们知道类方法是存储在元类对象中的,元类对象本来也是一种特殊的类对象。需要注意的是,类方法的消息接受者变为类对象。
当类对象进行消息转发时,对调用相应的+号的forwardingTargetForSelector、methodSignatureForSelector、forwardInvocation
方法,需要注意的是+号方法仅仅没有提示,而不是系统不会对类方法进行消息转发。
下面通过一段代码查看类方法的消息转发机制。
int main(int argc, const char * argv[]) {
@autoreleasepool {
[Person driving];
}
return 0;
}
#import "Car.h"
@implementation Car
+ (void) driving;
{
NSLog(@"car driving");
}
@end
#import "Person.h"
#import
#import "Car.h"
@implementation Person
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
// 返回能够处理消息的对象
if (aSelector == @selector(driving)) {
// 这里需要返回类对象
return [Car class];
// 这里返回实例对象也是可以的
// 因为objc_msgSend的本质就是消息接收者和函数名
return [[Car alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
// 如果forwardInvocation函数中返回nil 则执行下列代码
// 方法签名:返回值类型、参数类型
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
if (aSelector == @selector(driving)) {
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
[anInvocation invokeWithTarget: [Car class]];
}
// 打印结果
// 消息转发[6935:2415131] car driving
上述代码中同样可以对类对象方法进行消息转发。需要注意的是类方法的接受者为类对象。其他同对象方法消息转发模式相同。
4.5 @dynamic
和@synthesize
@synthesize
自动实现属性的setter
和getter
方法
@dynamic
不会自动实现属性的setter
和getter
方法,需要自己手动去实现,也可以使用上述runtime
的消息转发机制在运行时来实现。