runtime
我们都知道大部分语言是编译时决议的,而Object-C
是在运行时决议,这来源于强大的runtime
。通过runtime
可以动态对类各方面进行配置,还有就是消息传递。消息传递其实就是通过objc_msgSend
按照sel
找到函数imp
的过程。
objc_msgSend
新建一个工程,在main.m
文件夹内创建一个LGPerson
类。在main
函数内部调用[p study]
,[p happy]
。
@interface LGPerson : NSObject
- (void)study;
- (void)happy;
+ (void)eat;
@end
@implementation LGPerson
- (void)study {
NSLog(@"%s",__func__);
}
- (void)happy {
NSLog(@"%s",__func__);
}
+ (void)eat {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
[p study];
[p happy];
}
return NSApplicationMain(argc, argv);
}
通过clang探索objc_msgSend
使用clang
编译,打开该工程目录输入clang -rewrite-objc main.m
,然后打开该工程目下main.cpp
文件,我们可以看到[p study]
,[p happy]
被编译成了objc_msgSend()函数
。第一个参数是接受者,第二个参数是方法名,系统会根据这2个参数找到方法具体实现。
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("study"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));
我们把 [p study]
直接改为编译后的方法((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"study"))
,直接运行验证,我们可以看到study
方法是可以执行。
我们在study
方法加个参数str
,看看clang
编译文件,打开该工程目录输入clang -rewrite-objc main.m
,然后打开该工程目下main.cpp
文件。
我们可以看到如果有参数的话,系统编译会自动加上相应的参数。
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_mx_2ljwkcpn0kg_4m1bgs1507f00000gn_T_main_5b862f_mi_3);
objc_msgSend的种类
我们查看main.cpp
文件的顶部。我们发现有5种objc_msgSend
。
-
objc_msgSend
: 发消息给本类 -
objc_msgSendSuper
: 发消息给父类 -
objc_msgSend_stret
: 发消息返回值是一个结构体 -
objc_msgSendSuper_stret
: 发消息给父类,返回值是一个结构体 -
objc_msgSend_fpret
: 发消息返回值是一个浮点型的
objc_msgSendSuper
首先我们来个经典的面试题,新建一个LGPerson
,还是申明一个study
方法,再新建一个LGTeacher
继承自LGPerson
,在LGTeacher
的init方法里面打印[self class]
和[super class]
。
@interface LGPerson : NSObject
-(void)study;
@end
@interface LGTeacher : LGPerson
@end
@implementation LGTeacher
-(instancetype)init {
if (self = [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
}
return self;
}
@end
初始化一个LGTeacher
的实例对象,我们通常会认为第一个打印是LGTeacher
,第二的打印位LGPerson
。看看是不是啊,运行。
我们可以看到运行结果2个打印都是
LGTeacher
。这是为什么呢?使用clang
编译,打开该工程目录输入clang -rewrite-objc LGTeacher.m
,打开LGTeacher.cpp
文件,找到init
函数,然后我们可以看到了[super class]
其实就是调用了objc_msgSendSuper
函数。
那我看苹果官方文档是怎么解释
objc_msgSendSuper
这个函数的,首先打开Xcode
菜单栏的help
然后点击Developer Documentation
,选择Objective-C
,然后点击搜索框搜索objc_msgSendSuper
。
查看objc_msgSendSuper官方文档。
Parameters
super
A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.
op
A pointer of type SEL. Pass the selector of the method that will handle the message.
...
A variable argument list containing the arguments to the method.Return Value
The return value of the method identified by op.Discussion
When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.参数
父类
指向 objc_super 数据结构的指针。 传递标识消息发送到的上下文的值,包括要接收消息的类的实例和开始搜索方法实现的超类。
操作
SEL 类型的指针。 传递将处理消息的方法的选择器。
...
包含方法参数的变量参数列表。返回值
op 标识的方法的返回值。-
讨论
当遇到方法调用时,编译器生成对函数 objc_msgSend、objc_msgSend_stret、objc_msgSendSuper 或 objc_msgSendSuper_stret 之一的调用。 发送到对象超类的消息(使用 super 关键字)使用 objc_msgSendSuper 发送; 其他消息使用 objc_msgSend 发送。 将数据结构作为返回值的方法使用 objc_msgSendSuper_stret 和 objc_msgSend_stret 发送。
我们查看苹果官方文件可以查看到(使用 super 关键字)使用 objc_msgSendSuper 发送
,所以说我们前文[super class]
是实际是调用objc_msgSendSuper
函数的,接受者是类的实例。
我们可以看到main.app
内部[super class]
,其实接受者还是self
一个LGTeacher
的实例对象,所以说消息的接受者还是LGTeacher
的实例对象,所以[super class]
输出为LGTeacher
。
重写objc_msgSendSuper
我们在子类LGTeacher
重写父类LGPerson
的study
方法,然后不进行调用,然后我们重写objc_msgSendSuper
。查看objc_super
结构体我们可以发现它需要传一个receiver
(接受者)和super_class
(父类)。receiver
还是LGTeacher
,super_class
是LGPerson.class
。
struct objc_super lg_objc_super;
lg_objc_super.receiver = self;
lg_objc_super.super_class = LGPerson.class;
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&lg_objc_super,@selector(study));
运行我们可以发现是可以调用LGPerson的study方法。
那我们把
super_class
改为NSObject.class
试试;
lg_objc_super.super_class = NSObject.class;
运行,我们可以看到是找不到这个study
方法的,因为使用objc_msgSendSuper
时候它会直接从它的super_class
直接找相应的方法实现,我们设置的是NSObject.class
,NSObject
没有实现study
方法,所以就直接就找不到study
方法的实现了。
方法的快速查找
我们是结合objc4-838进行探索的,我们全局搜索objc_msgSend
,可以看到有很多,我们找到真机arm64
架构下的,objc_msgSend
是由汇编写的,为什么会采用汇编呢,是因为汇编效率性能高,可以节约不少的时间。
我们新建个工程,连上真机,在
[t study]
处打上断点,然后再加上objc_msgSend
符号断点。运行至断点处打开objc_msgSend
符号断点。我们就可以看到汇编了。
- objc_msgSend汇编解析
//进入objc_msgSend流程
ENTRY _objc_msgSend
//流程开始,无需frame
UNWIND _objc_msgSend, NoFrame
//判断p0(消息接受者)是否存在,不存在则重新开始执行objc_msgSend
cmp p0, #0 // nil check and tagged pointer check
//如果支持小对象类型。返回小对象或空
#if SUPPORT_TAGGED_POINTERS
//b是进行跳转,b.le是小于判断,也就是小于的时候LNilOrTagged
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//等于,如果不支持小对象,就LReturnZero
b.eq LReturnZero
#endif
//通过p13取isa
ldr p13, [x0] // p13 = isa
//通过isa取class并保存到p16寄存器中
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
//LGetIsaDone是一个入口
LGetIsaDone:
// calls imp or objc_msgSend_uncached
//进入到缓存查找或者没有缓存查找方法的流程
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil check判空处理,直接退出
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
我们可以看到是和objc4-838中objc_msgSend
函数汇编是一样的。
- 首先进入
objc_msgSend
流程,cmp p0
,判断p0
是否存在,我们读取寄存器看看,可以看到x0
是一个LGTeacher
的实例对象,x1
是study
方法。
2.ldr p13, [x0]
通过p13
取isa
指针地址,我们可以打印x13
地址为0x000021a1047b96e1
,然后我们通过x0
打印对象的isa
,可以看到x13
就是对象的isa
指针。
3.GetClassFromIsa_p16 p13, 1, x0
:通过isa&掩码(0xffffffff8)
获取class
并保存到p16寄存器中,我们打印x16
可以看到x16
是类对象地址。
4.CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
开始找方法,取出x16
的class
移到x15
,通过x16
找cache
。
5.ldr x11, [x16, #0x10]
:x16
通过内存平移可以得到x11
,x11
就是之前cache详解内的第一个8字节成员_bucketsAndMaybeMask
。
6.
and x10, x11, #0xfffffffffffe
:x11&0xfffffffffffe
就得到buckets的
首地址x10
。然后在cache
进行方法查找。如果找到了会进行CacheHit(缓存命中)
,找不到的话会调用_objc_msgSend_uncached
函数。
总结:
当调用objc_msgSend(receiver, sel)
时:
- 看
receiver
是否存在 - 通过
receiver
的isa
指针获取类对象 - 通过类对象内存平移获取
cache
- 通过
cache
找到buckets
- 根据
buckets
找相应的sel
- 如果有相应的
sel
走CacheHit
函数 - 如果没有相应的
sel
会调用_objc_msgSend_uncached
函数
方法的慢速查找
方法在cache
内找不到就是调用_objc_msgSend_uncached
函数,我们在_objc_msgSend_uncached
函数内部可以看到它会调用MethodTableLookup
函数,MethodTableLookup
函数会调用_lookUpImpOrForward
。_lookUpImpOrForward
函数在汇编内是看不到,那我们直接在源码内搜索lookUpImpOrForward
。
我们可以看到
lookUpImpOrForward
函数内部首先会判断cache
内部有没有方法,因为在多线程环境下现在cache
可能有该方法。
如果本类没有的话会调用
getMethodNoSuper_nolock
函数,然后依次调用search_method_list_inline
、findMethodInSortedMethodList
函数。我们可以看到findMethodInSortedMethodList
是采用二分查找来获取方法的。
当我们在本类里面找到方法时,它会进行
goto done
,然后会进入log_and_fill_cache
函数,可以看到log_and_fill_cache
函数内部调用了cache.insert()
方法,也就是加入缓存里面了。需要注意的是哪个类调用就会加入哪个类的cache里面,也就是如果子类调用父类的方法,之后该方法会缓存到子类的cache
。
如果本类也没有方法,通过
curClass = curClass->getSuperclass()
,把curClass
转换成父类,然后找父类的cache
,cache
如果没有在通过二分查找找方法列表。如果再没有把curClass
转换成父类的父类依次查找。
总结:
消息的慢速查找流程:
- 调用
lookUpImpOrForward
- 看本类的
cache
里面有没有该方法 - 如果没有看本类的
methodList
有没有该方法(二分查找) - 如果没有看父类的
cache
里面有没有该方法 - 如果没有看父类的
methodList
有没有该方法(二分查找) - 然后逐级向上查找
- 当父类是
nil
的时候也就是查找到NSObject
的时候,都没有的话就会进入消息转发流程。