objc_msgSend发送消息完整流程

objc_msgSend

Objective-C方法是由一个selector(SEL),和一个implement(IMP)组成的。selector相当于门牌号,而Implement才是真正的住户(函数实现);和现实生活一样,门牌可以随便发(@selector(XXX)),但是不一定都找得到住户;而方法调用,其实都是转换为objc_msgSend函数的调用;Objective-C中的方法调用就是消息发送:给receiver(方法调用者)发送了一条消息(selector方法名);

一段简单的代码可以验证方法调用,底层调用的函数:

  • 断点,运行;
  • 选择查看汇编代码:
  • 找到我们调用的方法,发现底层就是objc_msgSend函数:

同样我们也能将OC代码转化为C/C++代码,大概窥探其底层实现;
clang -rewrite-objc main.m转化后的代码同样能找到底层调用objc_msgSend函数;

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSString *test = (NSString *)&__NSConstantStringImpl__var_folders__p_gphdp0fd4yv2vlq4f0y1wm500000gn_T_main_4010a0_mi_0;
        ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)test, sel_registerName("uppercaseString"));

    }
    return 0;
}

其中(id)test即消息接收者receiver,sel_registerName("uppercaseString")即发送的消息;

源码分析

objc_msgSend函数的实现,我们可以进一步通过源码分析窥探一二;

  • 源码官网
  • 选择objc4
  • 下载最新版本并打开项目
  • Xcode搜索objc_msgSend(,选择对应的架构;

ENTRY _objc_msgSendEND_ENTRY _objc_msgSend这段就是objc_msgSend函数的具体实现;都是汇编语言,这里不具体分析,简单理下过程;

第一阶段:消息发送

  1. 首先判断消息接收者receiver是否是nil,如果为nil调用LNilOrTagged、LReturnZero返回0;
  2. receiver不为nil,调用CacheLookup查找方法实现IMP缓存;
  3. 如果找到缓存直接调用TailCallCachedImp调用IMP;
  4. 没有缓存,则调用宏MethodTableLookup,调用_class_lookupMethodAndLoadCache3函数 --> lookUpImpOrForward函数;主要代码如下:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;
    .....
    
    imp = cache_getImp(cls, sel);
    if (imp) goto done;
    
    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }
    
    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    .....
}
  1. 在当前class中查找方法实现:调用getMethodNoSuper_nolock --> search_method_list函数,通过传入的SEL类型参数selector从方法列表中查找方法实现imp;如果能查找到则缓存imp并调用;
  2. 如果当前class方法列表中没有找到,则递归向父类class查找;(同样的流程:先查找缓存,再从方法列表中查找)

以上流程,就是objc_msgSend的第一阶段:消息发送
如果以上都没有查找到方法实现,那么就会进入第二阶段:动态方法解析

第二阶段:动态方法解析

这阶段对应的源码为:

    if (resolver  &&  !triedResolver) { // 控制只调用一次
        runtimeLock.unlock();
        resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        triedResolver = YES;
        goto retry; // 动态添加完后 再次查找方法(第一阶段)
    }
static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) { // 不是元类,添加实例方法
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        resolveClassMethod(cls, sel, inst); // 添加类方法
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}

两个关键的函数resolveInstanceMethod,resolveClassMethod;我们可以重写NSObject对应的这两个方法,动态的添加实例方法、类方法;

@implementation Dog

- (void)bark {
    NSLog(@"wang wang");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(mew)) {
        Method method = class_getInstanceMethod(self, @selector(bark));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES; // YES动态添加了方法  NO没有添加
    }
    return [super resolveInstanceMethod:sel];
}

@end

以上代码,Dog没有mew方法,但我们为其动态添加了一个bark的实现;分析源码可以知道,当添加完方法后会调用goto retry再次执行第一阶段的查找方法流程,由于我们动态添加了方法,这次在方法列表中一定会找到对应的方法实现,然后直接调用;
这就是动态方法解析的过程;
如果我们没有动态添加方法,那将进入:消息转发阶段;

第三阶段:消息转发

这个阶段会使用_objc_msgForward函数指针代替 IMP 。最后,执行这个 IMP;
_objc_msgForward这部分代码没有开源,看不到源码;大体流程如下:

  1. 调用forwardingTargetForSelector:将消息转发给能处理sel的对象
@implementation Dog

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == NSSelectorFromString(@"mew")) {
        // 将消息发送给Cat实例 objc_msgSend(cat,aSelector)
        return [[Cat alloc] init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

Dog没有mew,我们通过forwardingTargetForSelector转发给Cat对象;

  1. 如果没有转发给其他target,调用methodSignatureForSelector:返回方法签名;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(mew)) {
        // 返回一个方法签名:方法返回值、方法参数
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
  1. 如果methodSignatureForSelector返回了方法签名,然后就会将这个返回的MethodSignature包装成一个NSInvocation对象;NSInvocation对象封装了一个方法调用,包括方法调用者,方法名,方法参数,方法返回值等;然后调用forwardInvocation:方法将这个Invocation对象交由开发者处理:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    anInvocation.target = [[Cat alloc] init]; // 转发给Cat实例
    [anInvocation invoke]; // 调用
}

以上代码同第一步的forwardingTargetForSelector效果一样;但NSInvocation对象不仅限于修改target;还可以修改参数值,返回值等;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Dog *dog = [[Dog alloc] init];
        [dog bark:1];
    }
    return 0;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(bark:)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:I"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    int value = 0;
    [anInvocation getArgument:&value atIndex:2]; // 0,1参数分别为self,SEL
    value ++;
    NSLog(@"%d",value); // 2
}

成功获取到了参数的值,并更改了;

消息转发的几个方法,NSObject.h中我们发现只有实例-方法;并没有对应的+类方法;是不是意味着类方法就不能转发呢?
类方法同样可以实现消息转发,只需将对应-改为+类方法即可:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == NSSelectorFromString(@"mew")) {
        // 将消息发送给Cat objc_msgSend(Cat,aSelector)
        return [Cat class] ;
    }
    return [super forwardingTargetForSelector:aSelector];
}

另外我们也可以直接调用_objc_msgForward函数,这样调用函数时会直接跳过消息发送的前两个阶段而直接进行消息转发
直接调用_objc_msgForward也是有实际应用场景的,比如实现hook功能;

doesNotRecognizeSelector:
当以上流程都未处理时,运行时系统将调用doesNotRecognizeSelector:方法。然后,该方法引发一个NSInvalidArgumentException,抛出错误消息。

unrecognized selector sent to instance xxxxxx

doesNotRecognizeSelector消息通常只由运行时系统发送。但是,我们也可以在程序代码中手动调用;一个常用的场景就是调用这个方法抛异常来防止方法被继承;
我们也可以重写这个方法,在抛出错误前做一些自己的处理;

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    .... // do something
    [super doesNotRecognizeSelector:aSelector];
}

理论上重写这个方法时可以不调用super方法,保证不抛出异常;但官方文档要求“它必须要抛出异常”;

消息转发应用场景

  • 解决向NSNull发送消息时崩溃的问题
    对服务器返回的JSON数据解析时总会出现null数据导致崩溃的现象,避免这种崩溃出现的方法之一就是对返回的数据类型进行判断,但每个返回数据的地方都这么判断的话就会有点繁琐。另一个简单实用的方法就是消息转发:将向NSNull发送的消息转发给可以执行该方法的类。这里通过创建一个NSNull的分类就能实现:
#import "NSNull+signature.h"

#define nullObjects @[@"",@0,@[],@{}] // 执行相关方法的数据类型

@implementation NSNull (signature)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        for (NSObject *object in nullObjects) {
            signature = [object methodSignatureForSelector:aSelector];
            if (signature) {
                break;
            }
        }
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    for (NSObject *object in nullObjects) {
        if ([object respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:object];
        }
    }
}

@end
  • 通过消息转发实现hook功能:可以替换方法实现,或在原有方法基础上添加自己的方法;
    JSPatch,Aspects这两个库都有类似实现: