目录
弄明白对象、类是怎么调用方法的就行
一、消息发送机制objc_msgSend
二、动态方法解析resolveMethod
三、消息转发机制objc_msgForward
四、程序崩掉unrecognized selector sent to instance
五、super
关键字objc_msgSendSuper
一、消息发送机制objc_msgSend
消息发送机制是指,OC对象调用方法[object methodName]
,都会转换为objc_msgSend(object, @selector(methodName))
函数的调用——即给一个对象发送一个消息,这个转换过程是在编译时就完成的,而具体怎么给对象发送消息则是在运行时完成的。那objc_msgSend
函数内部具体怎么给对象发送消息的呢?(消息发送流程、方法调用流程)
先总结在这里:
一进入
objc_msgSend
函数,系统会首先判断消息接收者是不是nil
,如果是nil
直接return
,结束该方法的调用,程序并不会崩掉。如果不是
nil
,则根据对象的isa
指针找到该对象所属的类,去这个类的方法缓存——cache
里查找方法,方法缓存是通过散列表实现的,所以查找效率非常高,如果找到了就直接调用,如果没有找到,则会去类的方法列表——methods
里查找,这里对于已排过序的方法列表采用二分查找,对于未排过序的方法列表则采用遍历查找,如果在类的方法列表找到了方法,则首先把该方法缓存到当前类的cache
中,然后调用该方法,如果没有找到,则会根据当前类的superclass
指针找到它的父类,去父类里查找。找到父类后,会首先去父类的方法缓存——
cache
里查找方法,如果找到了,则首先把该方法缓存到当前类的cache
中(注意不是父类的cache
哦),然后调用该方法,如果没有找到,则会去父类的方法列表——methods
里查找。如果在父类的方法列表找到了方法,则首先把该方法缓存到当前类的cache
中(注意不是父类的cache
哦),然后调用该方法,如果没有找到,则会一层一层往上,直到根类,直到nil
。如果到了
nil
,还是没有找到方法,就会触发动态方法解析。
从整个消息发送流程,我们也感受到:消息发送关注的仅仅是消息接收者和
SEL
,无非就是通过消息接收者的isa
指针和superclass
指针去找SEL
嘛,根本就没有什么绝对的标识来表明某个方法是实例方法还是类方法,所以如果出现类调用实例方法也不要惊讶哦,比如[NSObject -test]
是没有问题的,你只要抓紧方法调用流程这条线就可以了。
-
objc-msg-arm64.s
文件,汇编代码(伪代码)
/********************************************************************
*
* id objc_msgSend(id self, SEL _cmd, ...);
*
********************************************************************/
ENTRY _objc_msgSend // _objc_msgSend函数的入口
/*
cmp:比较。
p0:可以看做是_objc_msgSend函数的第一个参数——即消息接收者object。
#0:可以看做是nil。
*/
cmp p0, #0 // 判断消息接收者是不是nil
/*
b:跳转。
le:跳转的条件,小于等于。
LNilOrTagged:要跳转到的地方。
*/
b.le LNilOrTagged // 如果消息接收者为nil,则跳转到LNilOrTagged执行,如果消息接收者不为nil,则继续往下执行
ldr p13, [x0] // p13 = isa,从寄存器中获取该对象的isa共用体
and p16, p13, #ISA_MASK // p16 = class,isa & ISA_MASK,获取到该对象所属类的内存地址,这就算找到该对象所属的类了
LGetIsaDone:
CacheLookup NORMAL // 去这个类的方法缓存里查找方法
LNilOrTagged:
ret // 直接return
END_ENTRY _objc_msgSend // _objc_msgSend函数的出口
可见一进入objc_msgSend
函数,系统会首先判断消息接收者是不是nil
,如果是nil
直接return
,所以给nil
发送一个消息程序并不会崩溃;如果不是nil
,则根据对象的isa
指针找到该对象所属的类,去这个类的方法缓存里查找方法。
/********************************************************************
*
* CacheLookup、CacheHit、CheckMiss
*
********************************************************************/
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // 命中,即在方法缓存里找到了方法
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // 未命中,即在方法缓存里没有找到方法
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
.endmacro
.macro CacheHit
TailCallCachedImp x17, x12, x1 // 验证并直接调用IMP指向的函数
.endmacro
.macro CheckMiss
__objc_msgSend_uncached // 执行__objc_msgSend_uncached函数
.endmacro
/********************************************************************
*
* __objc_msgSend_uncached、MethodTableLookup
*
********************************************************************/
STATIC_ENTRY __objc_msgSend_uncached
MethodTableLookup // 查找方法列表
END_ENTRY __objc_msgSend_uncached
.macro MethodTableLookup
bl __class_lookupMethodAndLoadCache3 // 跳转执行__class_lookupMethodAndLoadCache3这个C函数,并要求一个返回值,收到返回值IMP后,会执行它所指向的函数
.endmacro
你看上面一堆代码,注释里又是散列表,又是SEL
、mask
,又是散列算法的,就知道它们是在方法缓存里查找方法,如果找到了就直接调用,如果没有找到,则会去类的方法列表里查找方法。
-
objc-runtime-new.mm
文件,C/C++代码(伪代码)
/********************************************************************
*
* _class_lookupMethodAndLoadCache
*
********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls/*当前对象所属的类*/, sel/*消息*/, obj/*当前对象*/,
YES/*initialize*/, NO/*标识在cache没有找到方法*/, YES/*resolver*/);
}
汇编在调用这个C函数的时候,默认会传递几个参数过来。
/********************************************************************
*
* lookUpImpOrForward
*
********************************************************************/
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
retry:
// 在当前类的方法列表里查找方法(汇编部分已经在当前类的方法缓存里查找过了)
{
Method meth = getMethodNoSuper_nolock(cls/*当前类*/, sel/*消息*/);
if (meth) { // 如果找到了方法
// 首先把该方法缓存到当前类的cache中
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done; // 然后跳转到done
}
}
// 在当前类父类的方法缓存和方法列表里查找方法,并一层一层往上,直到根类,直到nil
{
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass) // for循环的作用是一层一层往上,直到根类,直到nil
{
// 在父类的方法缓存里查找方法
imp = cache_getImp(curClass, sel);
if (imp) { // 如果找到了方法
// 首先把该方法缓存到当前类的cache中(注意不是父类的cache哦)
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done; // 跳转到done
}
// 在父类的方法列表里查找方法
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) { // 如果找到了方法
// 首先把该方法缓存到当前类的cache中(注意不是父类的cache哦)
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done; // 跳转到done
}
}
}
// 如果正常的消息发送流程走完,没有找到方法,就会触发动态方法解析
if (!triedResolver) { // 首先判断之前有没有进行过动态方法解析,有则直接触发消息转发机制,没有则进行动态方法解析
resolveMethod(cls, sel, inst); // 进行动态方法解析
// 动态方法解析完成后
triedResolver = YES; // 标记为已经进行过动态方法解析
goto retry; // 并重新走一遍消息发送流程来查找方法
}
// 如果正常的消息发送流程和动态方法解析都走完,还是没有找到方法,就会触发消息转发机制
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
return imp; // 返回方法的IMP,就返回到了汇编那里,汇编收到返回值IMP后,会执行它所指向的函数
}
如果在当前类的方法列表里找到了方法,则首先把该方法缓存到当前类的cache
中,然后调用该方法。
如果在当前类的方法列表里还是没有找到方法,则会根据当前类的superclass
指针找到它的父类,去父类的方法缓存里查找方法,如果找到了方法,则首先把该方法缓存到当前类的cache
中(注意不是父类的cache
哦),然后调用该方法。如果在父类的方法缓存里也没有找到方法,则会去父类的方法列表里查找方法,如果找到了方法,则首先把该方法缓存到当前类的cache
中(注意不是父类的cache
哦),然后调用该方法。如果在父类的方法列表也没有找到方法,则会一层一层往上,直到根类,直到nil
。
如果到了nil
还是没有找到方法,则会触发动态方法解析。
/********************************************************************
*
* getMethodNoSuper_nolock、search_method_list:在类的方法列表里查找方法
*
********************************************************************/
method_t *getMethodNoSuper_nolock(Class cls, SEL sel)
{
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists) // 根据类的methods成员变量,找到所有的方法列表,然后遍历这些方法列表来查找方法
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
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;
}
}
return nil;
}
前面我们在讲方法缓存的时候,已经说过方法缓存是通过散列表实现的,它的查找效率非常高。那类的方法列表呢,它是怎么查找方法的?当我们找到类的时候,会根据它的methods
成员变量,找到所有的方法列表(包括分类的,分类的方法列表排在前面),然后遍历这些方法列表来查找方法,对于排过序的方法列表,使用二分查找,对于未排序的方法列表,使用普通遍历查找。
二、动态方法解析resolveMethod
如果正常的消息发送流程走完,没有找到方法,就会触发动态方法解析。动态方法解析是指,如果我们在编译时没有为某个方法提供实现,可以在运行时通过类的+resolveInstanceMethod:
方法或+resolveClassMethod:
方法动态地为这个方法添加实现。
一触发动态方法解析,系统如果发现是没有找到实例方法,就会调用该类的
+resolveInstanceMethod:
方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到类的methods
里;如果发现是没有找到类方法,就会调用该类的+resolveClassMethod:
方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到元类的methods
里。动态方法解析完成后,会重新走一遍消息发送流程来查找方法。
如果动态方法解析也没有解决问题,就会触发消息转发机制。
/********************************************************************
*
* resolveMethod
*
********************************************************************/
void resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) { // 如果这个类不是元类——即没找到实例方法
resolveInstanceMethod(cls, sel, inst);
}
else { // 如果这个类是元类——即没找到类方法
resolveClassMethod(cls, sel, inst)
}
}
/********************************************************************
*
* resolveInstanceMethod、resolveClassMethod
*
********************************************************************/
void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 首先判断该类有没有实现+resolveInstanceMethod:方法,因为它是个类方法,所以传的是cls->ISA(),没有的话直接return
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO, YES, NO))
{
return;
}
// 调用该类的+resolveInstanceMethod:方法,们可以在这个方法里动态地为没找到的方法添加实现,会添加到类的methods里
objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
}
void resolveClassMethod(Class cls, SEL sel, id inst)
{
// 此处的cls已经是metaClass了,所以直接传的是cls
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, NO, YES, NO))
{
return;
}
// 调用该类的+resolveClassMethod:方法,我们可以在这个方法里动态地为没找到的方法添加实现,会添加到元类的methods里
objc_msgSend(nonmeta, SEL_resolveClassMethod, sel);
}
举个例子:
-----------main.m-----------
#import <Foundation/Foundation.h>
#import "INEPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
INEPerson *person;
[person eat]; // 我们没有实现-eat方法,所以会触发动态方法解析
[INEPerson drink]; // 我们没有实现+drink方法,所以会触发动态方法解析
}
return 0;
}
-----------INEPerson.h-----------
#import <Foundation/Foundation.h>
@interface INEPerson : NSObject
- (void)eat;
+ (void)drink;
@end
-----------INEPerson.m-----------
#import "INEPerson.h"
#import <objc/runtime.h>
@implementation INEPerson
// 动态方法解析,处理没找到的实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 动态地为这个方法添加实现
if (sel == @selector(eat)) { // 如果是eat方法再添加,别把别的方法也给写了
Method tempMethod = class_getInstanceMethod(self, @selector(otherEat));
// self:eat方法要添加到当前类的methods里,此处self就是当前类
// sel:为@selector(eat)添加对应的IMP,即为eat方法添加实现
// imp、types:我们假设要添加为otherEat方法的IMP和types
class_addMethod(self, sel, method_getImplementation(tempMethod), method_getTypeEncoding(tempMethod));
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 动态方法解析,处理没找到的类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(drink)) {
Method tempMethod = class_getClassMethod(self, @selector(otherDrink));
// object_getClass(self):drink方法要添加到当前类元类的methods里,所以此处是object_getClass(self)
class_addMethod(object_getClass(self), sel, method_getImplementation(tempMethod), method_getTypeEncoding(tempMethod));
return YES;
}
return [super resolveClassMethod:sel];
}
#pragma mark - other method
- (void)otherEat {
NSLog(@"INEPerson otherEat");
}
+ (void)otherDrink {
NSLog(@"INEPerson otherDrink");
}
@end
控制台打印:
INEPerson otherEat
INEPerson otherDrink
三、消息转发机制objc_msgForward
如果正常的消息发送流程和动态方法解析都走完,还是没有找到方法,就会触发消息转发机制。消息转发机制是指,把消息转发给别的对象,让别的对象来调用这个方法,因为到了这一步,就表明你这个类本身已经没有能力调用这个方法了,交给别人吧。消息转发又可以分为直接消息转发和完整消息转发。(消息转发机制的源码是不开源的)
- 直接消息转发
直接消息转发是指,系统会调用该类的
forwardingTargetForSelector:
方法,我们可以在这个方法里直接把消息转发给别的对象。
举个例子:
-----------main.m-----------
#import <Foundation/Foundation.h>
#import "INEPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
INEPerson *person = [[INEPerson alloc] init];
[person eat]; // 我们没有实现-eat方法,动态方法解析也没做处理,会触发消息转发机制
[INEPerson drink]; // 我们没有实现+drink方法,动态方法解析也没做处理,会触发消息转发机制
}
return 0;
}
-----------INEPerson.h-----------
#import <Foundation/Foundation.h>
@interface INEPerson : NSObject
- (void)eat;
+ (void)drink;
@end
-----------INEPerson.m-----------
#import "INEPerson.h"
#import <objc/runtime.h>
@implementation INEPerson
/**
* 把消息转发给别的对象
*
* @param aSelector 要把哪个方法转发给别的对象——即没找到的方法
*
* @return 要把消息转发给哪个对象——即你觉得能调用该方法的对象
*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
// 转发给Student对象,因为它能处理这个方法。我们猜测这里的底层实现,无非就是拿这个返回的对象调用它相应的方法,即objc_msgSend([[INEStudent alloc] init], aSelector)
return [[INEStudent alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
// 注意:处理类方法时,前面要换成“+”
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(drink)) {
return [INEStudent class];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
-----------INEStudent.h-----------
#import <Foundation/Foundation.h>
@interface INEStudent : NSObject
- (void)eat;
+ (void)drink;
@end
-----------INEStudent.m-----------
#import "INEStudent.h"
@implementation INEStudent
- (void)eat {
NSLog(@"INEStudent eat");
}
+ (void)drink {
NSLog(@"INEStudent drink");
}
@end
控制台打印:
INEStudent eat
INEStudent drink
- 完整消息转发
完整消息转发是指,系统会调用该类的
methodSignatureForSelector:
方法和forwardInvocation:
方法,我们可以在这两个方法里把消息转发给别的对象,当然完整消息转发比起直接消息转发可以做更多复杂的操作,甚至你不做消息转发,自己想干什么就在forwardInvocation:
方法里干什么都可以。
还是上面的例子,换成完整消息转发来实现就是:
-----------INEPerson.m-----------
#import "INEPerson.h"
#import "INEStudent.h"
@implementation INEPerson
/**
* 提供一个方法签名,用来生成invocation
*
* @param aSelector 要把哪个方法转发给别的对象——即没找到的方法
*
* @return 方法签名——即方法类型编码的包装,包含了方法的返回值和参数信息
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
// "v16@0:8":eat方法的类型编码
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
/**
* 把消息转发给别的对象
*
* @param anInvocation 根据上面的方法签名生成的invocation,它是一个对象,里面封装了方法调用者、消息、方法返回值参数等信息
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 转发给Student对象
[anInvocation invokeWithTarget:[[INEStudent alloc] init]];
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(drink)) {
// "v16@0:8":drink方法的类型编码
return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
// 转发给Student对象
[anInvocation invokeWithTarget:[INEStudent class]];
}
@end
- 这样咋一看,完整消息转发反而比直接消息转发更麻烦了,那它有什么好处呢?
举个例子,Person
对象转发一个方法后,想要获取到方法的返回值。
-----------main.m-----------
int main(int argc, const char * argv[]) {
@autoreleasepool {
INEPerson *person = [[INEPerson alloc] init];
[person addA:1 andB:2];
}
return 0;
}
-----------INEPerson.m-----------
@implementation INEPerson
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(addA:andB:)) {
// 通过直接消息转发,只能转发,没法获取返回值啊
return [[INEStudent alloc] init];
}
return [super methodSignatureForSelector:aSelector];
}
@end
-----------INEPerson.m-----------
@implementation INEPerson
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(addA:andB:)) {
// "i24@0:8i16i20":addA:andB:方法的类型编码
return [NSMethodSignature signatureWithObjCTypes:"i24@0:8i16i20"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 转发给Student对象
[anInvocation invokeWithTarget:[[INEStudent alloc] init]];
// 获取返回值
int returnValue;
[anInvocation getReturnValue:&returnValue];
NSLog(@"%d", returnValue); // 3
}
@end
可见完整消息转发确实可以获取方法的返回值,而直接消息转发就做不到,当然这只是一个很简单的例子,完整消息转发还可以做更多复杂的操作。
四、程序崩掉unrecognized selector sent to instance
如果消息转发机制都走完了,还是没法处理这个方法的调用,那就彻底没救了,程序才会崩掉,报unrecognized selector sent to instance
的错误,也就是说这个错误是消息转发机制报的,而不是消息发送机制或动态方法解析阶段报的。所以如果别人问你“什么时候会报unrecognized selector sent to instance
的错误”,你最好把整个消息发送流程、动态方法解析、消息转发流程都给他说一遍,而不仅仅是说“找不到方法的实现时”——这只是消息发送阶段。
五、super
关键字objc_msgSendSuper
我们知道
self
代表的是当前对象,可super
代表的可不是父类的一个对象啊。super
关键字仅仅是一个编译器指示符,它的作用就是告诉当前消息接收者直接去它的父类里的查找方法,而不是从它的类里开始查找,消息接收者还是self
。
super
调用方法[super methodName]
,都会转换为objc_msgSendSuper({self, [self class]}, @selector(methodName))
函数的调用,可见本质上确实还是给self
发送消息,只不过直接去[self class]
里查找方法而已。
下面是Runtime的源码(NSObject.mm
文件):
// 返回该对象所属的类
- (Class)class {
return object_getClass(self);
}
// 返回该对象所属类的父类
- (Class)superclass {
return [self class]->superclass;
}
举个例子:
@implementation INEStudent : INEPerson
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"%@", [self class]); // INEStudent
NSLog(@"%@", [self superclass]); // INEPerson
NSLog(@"%@", [super class]); // INEStudent
NSLog(@"%@", [super superclass]); // INEPerson
}
return self;
}
@end
[self class]
和[self superclass]
就不用说了,消息接收者都是self
,会从Student
类的方法缓存和方法列表里开始查找class
和superclass
方法,而这两个方法都是NSObject
类的方法,所以会一层一层往上,找到后发现两者的实现就是返回当前消息接收者self
的类和父类。
[super class]
和[super superclass]
的消息接收者其实都还是self
,只不过会跳过Student
类,直接从Person
类的方法缓存和方法列表里开始查找class
和superclass
方法,最后也还是找到NSObject
类那里,找到后发现两者的实现就是返回当前消息接收者self
的类和父类,而self
又没变。