前言
我们知道Objctive-C方法调用实际是给一个对象发送消息。从这句话我们不难看出必要的条件有消息的接收者,和怎么发送消息。
消息的发送过程是通过sel
找到imp
。
Runtime框架是一套运行时api,底层是用c/c++/汇编写的,提供给Objective-C/swift等使用
探究编译后的.cpp
文件
工程准备:新建一个工程

工程开启消息发送配置:target -> Build Settings -> Apple Clang - Preprocessing -> Enable Strict Checking of objc_msgSend Call 设置为NO
声明一个WJPerson类
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface WJPerson : NSObject
- (void)run;
- (void)walk;
+ (void)say;
@end
NS_ASSUME_NONNULL_END
#import "WJPerson.h"
@implementation WJPerson
- (void)run{
NSLog(@"%s",__func__);
}
- (void)walk{
NSLog(@"%s",__func__);
}
+ (void)say {
NSLog(@"%s",__func__);
}
@end
在main.m
#import <Foundation/Foundation.h>
#import "WJStudent.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
WJPerson *p = [[WJPerson alloc] init];
[p run];
}
return 0;
}
得到编译后的main.cpp
打开终端
;cd
到main.m
路径
// 编译
clang -rewrite-objc main.m -o main.cpp

可以看到main函数编译后的代码,方法调用其实就是objc_msgSend函数给对象发送消息。
核心疑问:怎么通过sel找到imp
objc_msgSend两种查找imp方式:
1.快速 -> 缓存里找 汇编cache_t 方法实现imp哈希表
2.慢速 -> c/c++ 找缓存
我们打开源码 objc4-756.2
第一部分“汇编环节”
:
objc_msgSend
底层进入到汇编的源码 entry _objc_msgSend

再到LNilOrTagged
都做了些什么

我们先看LReturnZero

检测指针如果为空,就立马返回。结论:给nil发送消息不会做处理。
再看LGetIsaDone

透过isa找到当前对象的objc_class
对象,从而找到Cache_t
进行缓存查找

CacheLookup
是一个宏定义

看出来CacheLookup
里有三步操作:
1.CacheHit
: 缓存击中,即找到了imp
2.CheckMiss
: CacheHit没有的情况下做的操作
3.add
: 在别的地方(如函数表里)找到了imp,就添加到缓存里。
第一步CacheHit
的三种方式:
1.NORMAL:
2.GETIMP
3.LOOKUP

自行看注释就好,我们重点在call imp。所以objc_msgSend在汇编部分是从缓存中获取imp。实际情况是objc_class结构体里的Cache_t里拿到imp。
第二步CheckMiss
的三种方式:
1.NORMAL:
2.GETIMP
3.LOOKUP

请看__objc_msgSend_uncached
是不是有一点熟悉的感觉了,我们再看里面做什么操作:


我们再搜一下__class_lookupMethodAndLoadCache3
就找不到了,至此查找imp汇编部分就结束了,接下来到了漫长查找过程:c/c++环节。
第三步add
是在MethodTableLookup
对类进行查找方法列表找到后进行添加到缓存操作。
第二部分“c/c++环节”
:
然后我们全局搜索这个class_lookupMethodAndLoadCache3
就找到了这个c函数,诶嘿!在混编环节在缓存Cache_t里找不到imp,继续进入c/c++环节继续找imp。

注意这里的参数为什么是填 YES NO YES呢?
看参数名对应的是:初始化、读缓存、解析器
大概不难猜到我们这里cache已经在汇编阶段做过判断了,就不需要再设置为YES了。
接着我们来看一下这个lookUpImpOrForward
函数到底做了啥事儿(分段截图,截图不完):

这里又查了一次缓存里的imp,注意是一定查不到imp的,因为我们上面就已经做了同样的查找了。
接着继续看:

这里判断类是否存在,如果不存在要做初始化。

做的操作是递归!干嘛呢?
1.又做了一次缓存查询,这一次主要做的工作是remap
对哈希表 重映射;
2.对类方法列表查询,对父类的方法列表查询,return imp
并且缓存到汇编环节的Cache_t
吧。 这里的log_and_fill_cache
请看isa查找方法流程:

如果类和父类的方法列表里都找不到imp呢?
请往下看代码

如果类和父类的方法列表里都找不到imp!就进入动态方法解析和消息转发。
并且动态方法解析只会执行一次。
至此c/c++查询imp环节就结束了。
第三部分动态方法解析
看到上一张截图的函数resolveMethod
。它会去
call两个方法,看下面截图:

案例:我声明一个WJPerson
类,给予其.h
文件指定一个+(void)walk;
声明,但没有在.m
文件实现方法。在main函数体
里面进行调用 [WJPerson walk];
运行这段代码,会产生崩溃。崩溃原因呢是没有找到这个walk方法:unrecognized selector sent to class 0x100004378'
。
方法调用走到这里,就是经历过上面两个查找imp环节都没有找到imp。
那我们是否可以救一下呢?答案是,可以的。因为我们还有动态方法解析和消息转发。
此时我在WJPerson去实现一个方法:
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"来了");
return [super resolveClassMethod:sel];
}
再次运行调用[WJPerson walk];
会发现我们捕获到了方法解析流程。并且来了!
会打印两次!!

为什么是两次呢?我们打点点到NSLog
那行,重新运行。第一次断点处调试查看堆栈:

从堆栈可以看到,调用顺序和我们之前分析的一毛一样:会进入到源码里的resolveClassMethod

再看源码,这里可以看出为什么会打印两次了。系统自动给我们发了一条消息。
然后再看第二次log的堆栈

看一下这个方法调用结构,就是著名的消息转发流程

上面的补救措施:
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(walk)) {
// 我们动态解析我们的 对象方法
NSLog(@"类方法解析走这里");
SEL hellowordSEL = @selector(helloWord);
// Method hellowordM1= class_getClassMethod(self, hellowordSEL);
Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
IMP hellowordImp = method_getImplementation(hellowordM);
const char *type = method_getTypeEncoding(hellowordM);
NSLog(@"%s",type);
return class_addMethod(object_getClass(self), sel, hellowordImp, type);
}
return [super resolveClassMethod:sel];
}
特别注意:
上面有一段代码注释了我们拿出来说:
Method hellowordM1= class_getClassMethod(self, hellowordSEL);
Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
第一句的self指的是WJPerson类对象,意在给类对象添加类方法。
第二句object_getClass(self)是WJPerson的元类,意在给元类添加实例方法。
这两句代码其实是等价的
因为类方法以实例方法的形态存储于元类的方法列表
举例:
/*
* 1.WJPerson声明一个类方法+(void)walk,但是在.m没有实现它
* 2.而在NSObject的分类里 实现了实例方法-(void)walk
* 3.此时调用[WJPerson walk];是不会崩溃的!!!
*/
[WJPerson walk];
// 因为类方法以实例方法的形态存储于元类的方法列表
// 如果元类的方法列表没有找到,则去找根元类,直到找到NSObject为止。
我们再来看一下isa的走位图:

再回来看我们的动态方法解析的源码:

以上我们差不多得出动态方法解析的大致流程,然后我们分实例方法和类方法的动态方法解析做分析。
动态实例方法解析
案例:声明一个WJPerson类继承NSObject,给它声明一个实例方法 -(void)say; 不写say方法实现
分析方法调用:[[WJPerson alloc] say];
1.缓存查找,没有找到
2.类查找/父类查找,
3.动态方法解析_class_resolveMethod
,再去找cls的元类是否实现了+(Bool)resolveInstacenMethod:(SEL)sel
4.如果还是没有实现,再去找根元类,直到找到NSObject的+(Bool)resolveInstacenMethod:(SEL)sel方法实现
5.系统主动给类对象
发送一条消息,为了补救imp没有找到的错误,可以在我们的类里的+(Bool)resolveInstacenMethod
动态给类添加imp去绑定sel与imp
6.接着还会往类里通过sel查找一次imp,最终向上做缓存
接下来看源码:
如果cls类对象
不是元类,就会进入_class_resolveInstanceMethod
方法里

如果当前类实现了+(Bool)resloveInstanceMethod:(SEL)sel
,再次调用一次方法列表递归查找imp lookUpImpOrNil
肯定能找到imp,就来看方法体里做了啥事儿。
注意这一步,如果当前类没有实现+(Bool)resloveInstanceMethod:(SEL)sel
,再次调用一次方法列表递归查找imp lookUpImpOrNil
,这一次传递的参数是isa(元类)
与resloveInstanceMethod
,查找是否实现了实例动态方法解析,如果没有实现,就不会再次进入_class_resolveMethod
里,防止死递归。
它是怎么防止死递归的呢?
1.元类对象里找resloveInstanceMethod的实现;
2.若元类没找到,则往元类的继承链里找根元类,最后到NSObject;
3.NSObject肯定是有实现这个resloveInstanceMethod的,只是直接return NO;
接着看方法体做了啥事儿:发送一条消息,即调用元类/根元类里的resolveInstanceMethod的实现。

系统会自发一条消息,来到我们类.m文件里的重写resloveInstanceMethod方法实现,让其sel与imp做一个绑定
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(walk)) {
// 我们动态解析我们的 对象方法
NSLog(@"类方法解析走这里");
SEL hellowordSEL = @selector(helloWord);
// Method hellowordM1= class_getClassMethod(self, hellowordSEL);
Method hellowordM= class_getInstanceMethod(object_getClass(self), hellowordSEL);
IMP hellowordImp = method_getImplementation(hellowordM);
const char *type = method_getTypeEncoding(hellowordM);
NSLog(@"%s",type);
return class_addMethod(object_getClass(self), sel, hellowordImp, type);
}
return [super resolveClassMethod:sel];
}
那么问题来了,这里我只是返回Bool并且sel与imp绑定了,并没有返回imp出去,是怎么能调用成功的呢?
答案是:重新再通过sel找一次imp,它会return imp; 期间还会对imp进行缓存的,来看:

动态类方法解析
案例:声明一个WJPerson类继承NSObject,给它声明一个实例方法 +(void)walk; 不写walk方法实现
分析方法调用:[WJPerson say];
1.缓存查找,没有找到
2.元类/根元类的方法列表找-(void)say,没有找到
3.动态方法解析_class_resolveMethod
,再去找元类是否实现了+(Bool)resolveClassMethod:(SEL)sel
4.如果还是没有实现,就去找元类->根元类
直到找到NSObject
肯定是有实现+(Bool)resolveInstanceMethod:(SEL)sel
的
5.给元类发送一条消息(这里苹果做了处理,下面做源码分析),让sel+imp绑定
接下来看源码:

先是从元类->根元类->NSObject
里的方法列表找+(Bool)resolveClassMethod:(SEL)sel
的方法实现

上面看到系统自发了一条消息,其中_class_getNonMetaClass
是处理元类返回类对象的,什么个操作呢?看下图:

看代码注释的逻辑,当我们调用[WJPerson walk];的时候,这里对元类的处理,返回的是WJPerson这个类对象:


所以在_class_resolveClassMethod
这个函数里系统发送一条消息,还是发送给类对象。接着系统自动往方法列表里再通过sel查找imp一遍(lookUpImpOrNil)
。
所以找到WJPerson类.m里边去实现+(Bool)resolveClassMethod:(SEL)sel
来处理没有实现的类方法。
程序继续往下走:

这个判断是,如果我们开发者没有重写去实现这个+(Bool)resolveClassMethod:(SEL)sel
,那就会进入这个if体里边:_class_resolveInstanceMethod
这就跟上面的动态实例方法解析走一样的流程,但是注意这里的cls是元类。
接下来的操作就是,去根元类 -> NSObject
方法列表查找+(Bool)resolveInstanceMethod:(SEL)sel
是否实现,最后在NSObject
找到。
接着系统给元类
发送一条消息SEL_resolveInstanceMethod
,并且最终通过lookUpImpOrNil
使用sel
去查找imp
一次。
不管是调用实例方法还是类方法(都没有写方法实现的情况下),都可以通过在NSObject的分类里重写+(Bool)resolveInstanceMethod:(SEL)sel
捕获到,动态添加imp
消息转发
当resolveInstanceMethod
/ resolveClassMethod
返回NO
就会进入消息转发。
在lookUpImpOrForward
查找imp函数里边的动态方法解析下面,就有消息转发:

其中_objc_msgForward_impcache
就是消息转发的流程;又到了我们的源码汇编阶段:


__objc_forward_handler
又跑回了c

看到上面截图的报错吗,是不是和我们没有找到imp方法是崩溃报错是那么熟悉。
消息转发过程只有汇编在调用,消息转发是调用CoreFoundation的代码没有开源!
看不到函数调用过程了。但是可以想办法让它开源。让它反汇编出来:
首先我们找到这个路径下的CoreFoundation
文件,把它复制到桌面
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
下载安装一个反编译软件 Hopper Disassembler v4
,将CoreFoundation
文件拖入反编译软件中得到反编译代码
接下来看两张图
第一张是我们Xcode方法调用时没有写方法实现,并且没有做动态方法解析成功的报错

第二张是反编译软件的代码

点进去__forwarding_prep_0__

再点进去看__forwarding__
到底做了什么事

得到了汇编里的__forwarding__
很长的一段代码,发现了forwardingTargetForSelector
如果类里没有实现forwardingTargetForSelector
就会 goto loc_12ca57
之后就会进入慢速转发
环节:

就会做僵尸对象判断,然后继续往下走,拿到方法签名sel: methodSignatureForSelector
,如果响应了方法名称还会继续往下走,做一些方法签名的判断等等,接着


这就是消息转发流程的汇编的调用顺序。
所以我们消息转发可以进行如下处理:
#pragma mark - 动态消息转发流程 ---- 只有汇编进行调用
// 消息快速转发
+ (id)forwardingTargetForSelector: (SEL)aSelector {
return [super forwardingTargetForSelector:aSelector];
}
// 消息慢速转发
+ (NSMethodSignature *)methodSignatureForSelector: (SEL) aSelector {
if (aSelector == @selector(walk)) {
return [NSMethodSignature signatureWithObjCTypes:@"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation: (NSInvocation *)anInvocation {
NSString *str = @"我是xxx";
anInvocation.target = [WjStudent class];
[anInvocation setArgument:&str atIndex:2];
anInvocation.selector = @selector(run:);
[anInvocation invoke];
}

以上就是方法调用的实质分析。
objc_msgSend
网友评论