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这两个库都有类似实现:
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266