我们在探索oc类的
结构和方法的存储,方法的查找发现大量使用到了objc_msgSend消息机制。今天我们就对objc_msgSend进行探索分析及对实例方法的动态解析和类方法的动态决议进行分析探索
一. objc_msgSend及方法的查找
1、objc_msgSend的解析
来吧~先写上我们熟悉的代码:
@interface NYPerson : NSObject
- (void)study;
- (void)happy;
+ (void)eat;
@end
@implementation NYPerson
- (void)study {
NSLog(@"%s",__func__);
}
- (void)happy {
NSLog(@"%s",__func__);
}
+ (void)eat {
NSLog(@"%s",__func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
NYPerson *p = [NYPerson alloc];
[p study];
[p happy];
}
return NSApplicationMain(argc, argv);
}
我们定义了两个实例方法 study,happy和一个类方法eat,然后我们用clang -rewrite-objc命令编译出main.cpp文件来看,转换成cpp后分析具体的代码。
我们看到
main函数代码块被转换成了如上图所示代码。
发现objc_msgSend(第一个参数:消息的接受者,第二个参数:消息的方法名)
那个我们在- (void)study:(NSString *)str;增加str参数,进行调用[p study:@"YN"]
在通过clang -rewrite-objc命令编译生成mian.cpp看看编译做了什么。
我们发有同名的 static 参数被生成和调用。
我们也可以用
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, NSSelectorFromString(@"happy"))替代[p happy]方法调用。
总结: 在iOS中调⽤⽅法其实就是在给对象发送某条消息。消息的发送在编译的时候编译器就会把⽅法转 换为objc_msgSend这个函数。objc_msgSend有俩个隐式的参数,消息的接收者和消息的⽅法
名。objc_msgSend这个函数就能够通过这俩个隐式的参数去找到⽅法具体的实现。如果消息的接
收者是实例对象,isa就指向类对象,后再通过第⼆个参数⽅法名,去类对象⾥⾯找对应的⽅法实
现。如果消息的接收者是类对象,isa就指向元类,就会去元类⾥⾯找对应的⽅法实现。
2、objc_msgSendSuper的解析
我们用一个例子来探索objc_msgSendSuper如下:
@interface NYPerson : NSObject
-(void)study;
@end
@implementation NYPerson
-(void)study {
NSLog(@"%s",__func__);
}
@end
@interface NYTeacher : NYPerson
@end
@implementation NYTeacher
-(instancetype)init {
if (self = [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
}
return self;
}
- (void)study {
struct objc_super lg_objc_super;
lg_objc_super.receiver = self;
lg_objc_super.super_class = NYPerson.class;
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&lg_objc_super,@selector(study));
}
@end
运行看下init方法里:[self class],[super class]分别打印什么?
@implementation NYTeacher
-(instancetype)init {
if (self = [super init]) {
NSLog(@"%@",[self class]);
NSLog(@"%@",[super class]);
}
return self;
}
- (void)study {
//[super study];
struct objc_super lg_objc_super;
lg_objc_super.receiver = self;
lg_objc_super.super_class = NYTeacher.class; //会死循环
void* (*objc_msgSendSuperTyped)(struct objc_super *self,SEL _cmd) = (void *)objc_msgSendSuper;
objc_msgSendSuperTyped(&lg_objc_super,@selector(study));
}
@end
都是打印一样的NYTeacher 是为什么呢?
我们继续通过
clang -rewrite-objc命令编译生成NYTeacher.cpp看看编译做了什么。
self就代表NYTeacher对象,objc_msgSend 向self(NYTeacher) 发送"class"的方法名消息。
objc_msgSendSuper向self,并且id=class_getSuperclass=NYTeacher类,这个时候self的Superclass指向NYTeacher类。所以这个时候objc_msgSendSuper搜索的起点父类是NYTeacher类。
从xcode的帮助文档得知:objc_msgSendSuper 和 objc_msgSend 区别在于他们的搜索出发点不一样。
objc_msgSendSuper 开始搜索方法实现的超类,简单说新从父类找起。
附上objc_super代码:
3、方法的快速查找流程
我们在objc源码中找到对应objc_msgsend的实现 (arm64)下:
我们发现这是一段汇编代码,使用汇编函数的执行比C快。
通过真机运行和符号断点进行分析得到:
p0消息的接收者是否存在。
一个简单流程图总结一下:
_objc_msgSend_uncached 函数是什么呢?下面我们进行分析。
4、方法的慢速查找算法
我们从objc 源码中发现了_objc_msgSend_uncached方法的实现和关联的方法MethodTableLookup.
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
....
}
我们找到了慢速查找的具体实现方法lookUpImpOrForward,我们现在开始分析它。
getMethodNoSuper_nolock -> search_method_list_inline -> findMethodInSortedMethodList 里的具体算法。
总结:在慢速查找中
findMethodInSortedMethodList使用了二分查找,找到目标方法如果:主类方法和分类方法同名时,会优先调用分类方法。
5、方法的慢速查找流程
找到方法后会执行log_and_fill_cache -> cache.insert 加入到缓存中.
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver);
}
子类调用父类方法 -- 缓存在子类的 cache 中
附消息的慢速查找流程图:
总结:objc_msgSend消息机制,通过汇编进行cache->buckets快速查找hit命中执行。但由于cache-buckets在编译执行中通过桶子扩容规则,有可能造成cache中并没有对应方法。就会进行_objc_msgSend_uncached->lookUpImpOrForward(二分查找)的慢速查找流程。这里提示分类存在同名方法,分类方法优先命中。(子类调用父类方法 -- 缓存在子类的 cache 中)
(ps:如有不对的地方,请及时指正)
二. 实例方法与类方法的解析
1、实例方法的动态解析
前面我们了解到当在父类中没有找到对应的方法时,消息机制就会去调用_objc_msgForward_impcahe这个函数,我们总从这里继续探索分析。
来看看代码:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
extern void _objc_msgForward_impcache(void);
#else
extern id _objc_msgForward_impcache(id, SEL, ...);
#endif
//汇编代码
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17 //函数指针调用
END_ENTRY __objc_msgForward
我们看到核心代码 _objc_forward_handler是这个函数。
这个代码里的文字好熟悉啊,就是我们运行项目在xcode中找不到方法实现的报错。
然后在lookUpImpOrForward方法代码中找到resolveMethod_locked这个函数。
我们进入这个
resolveMethod_locked方法看看:
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
我们发现在报错前会执行 SEL resolve_sel = @selector(resolveInstanceMethod:);
这个resolveInstanceMethod方法。
写个demo验证 resolveInstanceMethod 动态决议。
@interface NYPerson : NSObject
- (void)test;
@end
@implementation NYPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"%s",__func__);
return [super resolveInstanceMethod:sel];
}
@end
//main函数中调用
NYPerson *p = [[NYPerson alloc] init];
[p test];
看到打印了两次,
为什么会执行两次呢?我们后面研究。
我们在resolveInstanceMethod 中给NYPerson 实例的 test方法添加具体的实现imp(动态添加imp)代码如下:
验证打印结果:
总结:在oc中方法是通过objc_msgSend消息机制来查找执行的,在查找过程中
lookUpImpOrForward如果当前类及父类都没有对应的实现方法。会在报错之前执行resolveInstanceMethod方法一次,我们称之为动态决议。然后我们代码验证了在resolveInstanceMethod中 动态添加对test的具体imp实现来解决找不到test方法所产生的错误。
2、类方法的动态决议
猜想一下,我们动态添加的方法test会添加到cache->buckets()中吗?我们来验证一下:
我们在
$2+5的时候得到了test的打印。(如有不明白步骤的可以看oc类底层cache_t详解)
说明了,我们在给test方法添加动态决议的时候也会添加到cache中。
回到代码:
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
...
if (! cls->isMetaClass()) {//判断是否是元类
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);//不是元类的时候
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);//是类方法时候调用
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
....
}
接着,我们在demo中添加代码NYPerson ->+ (void)test1; 然后 NYPerson->resolveClassMethod
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"%s--%@",__func__,NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
//打印
**2022-05-11 23:19:24.858879+0800 SXObjcDebug[24465:803714] +[NYPerson resolveClassMethod:]--test1**
**2022-05-11 23:19:24.859595+0800 SXObjcDebug[24465:803714] +[NYPerson resolveClassMethod:]--test1**
**2022-05-11 23:19:24.861324+0800 SXObjcDebug[24465:803714] +[NYPerson test1]: unrecognized selector sent to class 0x100008408**
这里同样打印了两次test1,依葫芦画瓢继续添加类的动态决议。
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"%s--%@",__func__,NSStringFromSelector(sel));
if (sel == @selector(test1)) {
class_addMethod(objc_getMetaClass("NYPerson"), sel, class_getMethodImplementation(self.class, @selector(method2)), "v@:");
return YES;;
}
return [super resolveClassMethod:sel];
}
- (void)method2 {
NSLog(@"%s",__func__);
}
打印结果:
我们动态添加了method3的空imp方法,打印发现会执行2次
resolveInstanceMethod然后->resolveClassMethod->_forwardStackInvocation:
从resolveMethod_locked源码中我们知道未在cache中找到方法会执行resolveInstanceMethod。
_forwardStackInvocation这个是啥呢?为什么会这样呢?
查找了一些资料,发现_forwardStackInvocation是系统内部的方法,没有对外暴露所以无法调用。其中涉及了消息转发的机制,下面会进行探索解析。
3、AOP及消息转发
前面的知识我们想到类方法在元类中,resolveClassMethod又可以在oc类中重写。objc_msgSend消息机制慢速查找,会一层层从父类中找方法。所有类的根元类是NSObject,而在消息机制中慢速查找分类方法优先级高。我们创建一个NSObject+NY的分类,来处理所有类的动态决议。
+(BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test1)) {
class_addMethod(self.class, sel, class_getMethodImplementation(self.class, @selector(method1)), "v@:");
return YES;;
}
if (sel == @selector(test2)) {
class_addMethod(objc_getMetaClass("NYPerson"), sel, class_getMethodImplementation(objc_getClass("NYPerson"), @selector(method2)),"v@:");
return YES;
}
return NO;
}
//打印
**2022-05-12 15:55:52.167049+0800 SXObjcDebug[27064:867931] Hello World!**
**2022-05-12 15:55:52.169458+0800 SXObjcDebug[27064:867931] -[NSObject(NY) method1]**
//aop 小例子,核心代码。
+(void)load
{
static dispatch_once_t oncet;
dispatch_once(&oncet, ^{
Method method1 = class_getInstanceMethod(self.class, @selector(viewWillAppear:));
Method method2 = class_getInstanceMethod(self.class, @selector(aopviewWillAppear));
method_exchangeImplementations(method1, method2);
});
}
-(void)aopviewWillAppear
{
NSLog(@"进来了 %@",self.class);
[self aopviewWillAppear]; //这里不会死循环,替换了viewWillAppear
}
这里我们引入一个知识点AOP切面编程,AOP为Aspect Oriented Programming的缩写,意为:[面向切面编程]通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
附图:
(ps:借用了百度百科)
消息转发
lookUpImpOrForward->log_and_fill_cache中
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) { //判断写入日志
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cls->cache.insert(sel, imp, receiver);
}
(objcMsgLogEnabled && implementer)同时未真时才会执行logMessageSend,bool objcMsgLogEnabled = false; 默认是false 。只有在 instrumentObjcMessageSends 方法中有对objcMsgLogEnabled 进行赋值。
只有这里赋值。
那我们就可以写个demo看下日志的生成:
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
NYPerson *person = [NYPerson alloc];
instrumentObjcMessageSends(YES);
[person test];
instrumentObjcMessageSends(NO);
}
return 0;
}
**2022-05-12 17:01:11.176277+0800 instrumentObjcMessageSends[28144:900951] Hello, World!**
运行后我进入/tmp/文件下找找看,我们生成的日志文件。
嘿嘿~(不由自主)😊:点开这个文件。
就是我们执行的方法。如果我们不实现test方法呢?会写入什么样的日志?
我们通过一个demo来了解一下:
forwardingTargetForSelector:和 methodSignatureForSelector:
@interface NYPerson : NSObject
- (void)method1;
+ (void)method2;
@end
@implementation NYPerson
//消息转发
-(id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s-%@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(method1)) {
return [NYTeacher alloc];//如果方法是method1指定NYTeacher来处理
}
return [super forwardingTargetForSelector:aSelector];
}
//消息的慢速转发, 三根救命稻草(三夏老师说的)
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s-%@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation {
NYTeacher *t = [NYTeacher alloc];
if ([self respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self];
}else if ([t respondsToSelector:anInvocation.selector] ) {
[anInvocation invokeWithTarget:t];
}else {
NSLog(@"%s-%@",__func__,NSStringFromSelector(anInvocation.selector));
}
}
@end
//打印
**2022-05-12 23:18:51.940174+0800 003--消息转发[31160:979705] -[NYPerson forwardingTargetForSelector:]-method1**
**2022-05-12 23:18:51.940470+0800 003--消息转发[31160:979705] -[NYTeacher method1]**
消息转发流程如下:
1.先调用实例方法resolveInstanceMethod
如果作者在这里使用runtime动态添加对应的方法,并且返回yes。就万事大吉。对象找到了处理的方法,
并且将这个新增的方法添加到类的方法缓存列表cache
2.如果上面的方法返回NO的话,对象会调用forwardingTargetForSelector方法
允许作者选择其他的对象,处理这个消息。
这个方法,也是待会我们要做文章的地方。画重点。
3.如果上面两个方法都没有做处理,那么对象会执行最后一个方法methodSignatureForSelector,提供一个有效的方法签名,若提供了有效的方法签名,程序将会通过forwardInvocation方法执行签名。若没有提供方法签名就会触发doesNotRecognizeSelector方法,触发崩溃。
整个调用流程图如下:
4、执行两次resolveInstanceMethod详解
第一次进入动态决议:
第二次进入动态决议:
第一次动态决议找不到的时候会调用forwarding->methodSignatureForSelector(进行消息的慢速转发)->
class_getInstanceMethod->lookUpImpOrForward->resolveInstanceMethod
所以
resolveInstanceMethod 动态决议执行了两次。
大总结:
objc_msgSend消息机制,通过汇编进行cache->buckets快速查找hit命中执行。但由于cache-buckets在编译执行中通过桶子扩容规则,有可能造成cache中并没有对应方法。就会进行_objc_msgSend_uncached->lookUpImpOrForward(二分查找)的慢速查找流程。这里提示分类存在同名方法,分类方法优先命中。(子类调用父类方法 -- 缓存在子类的 cache 中)
如果在lookUpImpOrForward慢速查找没找对应的方法,会执行一次resolveInstanceMethod动态决议,我们可以在这里进行imp的动态实现(会添加到类的cache中),使程序不会崩溃。然后我们发现在程序崩溃前还会执行两个方法,forwardingTargetForSelector和methodSignatureForSelector进行消息的慢速转发。若没有提供方法签名就会触发doesNotRecognizeSelector方法,触发崩溃。