方法的本质
我们通过clang命令 clang -rewrite-objc main.m 将main文件编译成main.cpp文件
//main函数中方法的调用
LGPerson *person = [LGPerson alloc];
[person sayHello];
//clang后的方法的调用
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
可以看出 方法的调用本质是 objc_msgSend来发送消息。我们还可以手动调用来验证一下
导入头文件<objc/message.h>
LGPerson *person = [LGPerson alloc];
//1.方法的调用
[person study];
//2.消息发送 ((void (*)(id, SEL))(void *)objc_msgSend)(p, sel_registerName("study"));
打印结果如下
image.png
可以看到打印结果是一致的,这也证明了[person study]等价于objc_msgSend(person, sel_registerName("study"))。
执行父类的方法
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%@", [self class]);
NSLog(@"%@", [super class]);
}
return self;
}
先来看一下这段代码打印的是什么
image.png
打印的结果都是 LGTeacher,这是为什么呢?
我们用clang命令编译LGTeacher.m,得到如下编译代码
static instancetype _I_ LGTeacher_init(LGTeacher * self, SEL _cmd) {
if (self = ((LGTeacher *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("init"))) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_ LGTeacher_8d6cff_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_ LGTeacher_8d6cff_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGStudent"))}, sel_registerName("class")));
}
return self;
}
[self class]发送的消息是objc_msgSend,消息接收者是self,方法编号是class
[super class]本质是objc_msgSendSuper, 消息接收者也是self, 方法编号class, 所以[super class]打印出来的一样是self这个对象所指的类,也就是LGStudent。
小结
- [super class]中的super只是一个编译指示器,就是让当前对象获取该方法,越过本类,直接从父类找。
- 消息的接受这还是本类self
方法快速查找
image.png搜索objc_msgSend 在源码中以 objc-msg-arm64为例,看objc_msg_arm64中的源码,可以看到都是汇编代码。这里我们可以思考一个问题,苹果为什么objc_msgSend这部分代码要使用汇编来编写呢?答案很简单--效率。汇编的效率是比c/c++更快的,因为汇编大多是直接对寄存器的读写,相比较对内存的操作更底层,效率也更高。
//进入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
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
objc_msgSend 开始到找类对像cache方法结束的流程。
首先判断receiver是否存在,以及是否是taggedPointer类型的指针,如果不是我们取出对象的isa指针(x13寄存器中),通过isa指针找到类对象(x16寄存器),然后通过CacheLookup,在类对象的cache中查找是否有方法缓存,如果有就调用,如果没有走objc_msg_uncached分支。
方法慢速查找(从方法列表中查找)
方法在cache内找不到就是调用_objc_msgSend_uncached函数,我们在_objc_msgSend_uncached函数内部可以看到它会调用MethodTableLookup函数,MethodTableLookup函数会调用_lookUpImpOrForward。_lookUpImpOrForward函数在汇编内是看不到,那我们直接在源码内搜索lookUpImpOrForward。
image.png image.pnglookUpImpOrForward函数内部首先会判断cache内部有没有方法,因为在多线程环境下现在cache可能有该方法。
image.png
如果本类没有的话会调用getMethodNoSuper_nolock函数,然后依次调用search_method_list_inline、findMethodInSortedMethodList函数。我们可以看到findMethodInSortedMethodList是采用二分查找来获取方法的。
image.png
image.png
image.png
image.png
当我们在本类里面找到方法时,它会进行goto done,然后会进入log_and_fill_cache函数,可以看到log_and_fill_cache函数内部调用了cache.insert()方法,也就是加入缓存里面了。需要注意的是哪个类调用就会加入哪个类的cache里面,也就是如果子类调用父类的方法,之后该方法会缓存到子类的cache。
image.png
image.png
如果本类也没有方法,通过curClass = curClass->getSuperclass(),把curClass转换成父类,然后找父类的cache,cache如果没有在通过二分查找找方法列表。如果再没有把curClass转换成父类的父类依次查找。
image.png
总结:
消息的慢速查找流程:
- 调用
lookUpImpOrForward
- 看本类的
cache
里面有没有该方法 - 如果没有看本类的
methodList
有没有该方法(二分查找) - 如果没有看父类的
cache
里面有没有该方法 - 如果没有看父类的
methodList
有没有该方法(二分查找) - 然后逐级向上查找
- 当父类是
nil
的时候也就是查找到NSObject
的时候,都没有的话就会进入消息转发流程。
网友评论