深入理解 Objective-C 的方法调用流程

作者: hi_xgb | 来源:发表于2016-04-05 22:10 被阅读6609次

    我们知道,Objective-C 的方法调用不同于其他编程语言。在 Objective-C 中,所有的 [receiver message] 都会转换为 objc_msgSend(receiver, @selector(message));

    objc_msgSend 的调用又涉及到方法查找、消息动态处理等过程。下面我们结合 objc 的源码来深入了解 Objective-C 的方法调用流程。

    首先了解几个概念,

    SEL

    打开 objc.h,我们能看到 SEL 的定义如下:

    /// An opaque type that represents a method selector.
    typedef struct objc_selector *SEL;
    

    Objective-C 在编译时,会根据方法的名字生成一个用来区分这个方法的唯一的一个ID,本质上就是一个字符串。只要方法名称相同,那么它们的ID就是相同的。

    IMP

    打开 objc.h,我们看到 IMP 的定义如下:

    typedef id (*IMP)(id, SEL, ...); 
    

    实际上就是一个函数指针,指向方法实现的首地址。

    通过取得 IMP,我们可以跳过 runtime 的消息传递机制,直接执行 IMP指向的函数实现,这样省去了 runtime 消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些,当然必须说明的是,这种方式只适用于极特殊的优化场景,如效率敏感的场景下大量循环的调用某方法[1]

    直接使用 IMP 执行方法调用的例子如下:

    - (void)callFunctionUsingIMP
    {
        //Build Setting --> Enable Strict Checking of objc_msgSend Calls  改为 NO
        void (*imp) (id,SEL,id) = (void (*)(id,SEL,id))[self methodForSelector:@selector(testImp:)];
        imp(self,@selector(testImp:),@"hello");
    }
    
    - (void)testImp:(NSString *)string
    {
        NSLog(@"%@",string);
    }
    

    有两点需要注意:

    1. 需要在 Build Setting 中将 Enable Strict Checking of objc_msgSend Calls 的设置改为 NO
    2. 通过 methodForSelector 获取到 IMP 需要根据具体的参数进行类型转换,参考这个问题

    Method

    objc.h 中, Method 的定义如下:

    /// An opaque type that represents a method in a class definition.
    typedef struct objc_method *Method;
    
    struct objc_method {
        SEL method_name                                          OBJC2_UNAVAILABLE;
        char *method_types                                       OBJC2_UNAVAILABLE;
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }
    
    

    Method = SEL + IMP + method_types,相当于在SEL和IMP之间建立了一个映射。

    iOS方法调用流程

    objc_msgSend() Tour 系列文章通过对 objc_msgSend 的汇编源码分析,总结出以下流程:

    1. 检查 selector 是否需要忽略
    2. 检查 target 是否为 nil,如果是 nil 就直接 cleanup,然后 return
    3. 在 target 的 Class 中根据 selector 去找 IMP

    寻找 IMP 的过程[2]:

    1. 在当前 class 的方法缓存里寻找(cache methodLists)
    2. 找到了跳到对应的方法实现,没找到继续往下执行
    3. 从当前 class 的 方法列表里查找(methodLists),找到了添加到缓存列表里,然后跳转到对应的方法实现;没找到继续往下执行
    4. 从 superClass 的缓存列表和方法列表里查找,直到找到基类为止
    5. 以上步骤还找不到 IMP,则进入消息动态处理和消息转发流程,详见这篇文章

    我们能在 objc.h 源码中找到上述寻找 IMP 的过程,具体对应的代码如下:

    /***********************************************************************
    * lookUpMethod.
    * The standard method lookup. 
    * initialize==NO tries to avoid +initialize (but sometimes fails)
    * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
    * Most callers should use initialize==YES and cache==YES.
    * May return _objc_msgForward_internal. IMPs destined for external use 
    *   must be converted to _objc_msgForward or _objc_msgForward_stret.
    **********************************************************************/
    __private_extern__ IMP lookUpMethod(Class cls, SEL sel, 
                                        BOOL initialize, BOOL cache)
    {
        Class curClass;
        IMP methodPC = NULL;
        Method meth;
        BOOL triedResolver = NO;
    
        // Optimistic cache lookup
        if (cache) {
            methodPC = _cache_getImp(cls, sel);
            if (methodPC) return methodPC;    
        }
    
        // realize, +initialize, and any special early exit
        methodPC = prepareForMethodLookup(cls, sel, initialize);
        if (methodPC) return methodPC;
    
    
        // The lock is held to make method-lookup + cache-fill atomic 
        // with respect to method addition. Otherwise, a category could 
        // be added but ignored indefinitely because the cache was re-filled 
        // with the old value after the cache flush on behalf of the category.
     retry:
        lockForMethodLookup();
    
        // Try this class's cache.
    
        methodPC = _cache_getImp(cls, sel);
        if (methodPC) goto done;
    
        // Try this class's method lists.
    
        meth = _class_getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, cls, meth, sel);
            methodPC = method_getImplementation(meth);
            goto done;
        }
    
        // Try superclass caches and method lists.
    
        curClass = cls;
        while ((curClass = _class_getSuperclass(curClass))) {
            // Superclass cache.
            meth = _cache_getMethod(curClass, sel, &_objc_msgForward_internal);
            if (meth) {
                if (meth != (Method)1) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, curClass, meth, sel);
                    methodPC = method_getImplementation(meth);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
    
            // Superclass method list.
            meth = _class_getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, curClass, meth, sel);
                methodPC = method_getImplementation(meth);
                goto done;
            }
        }
    
        // No implementation found. Try method resolver once.
    
        if (!triedResolver) {
            unlockForMethodLookup();
            _class_resolveMethod(cls, sel);
            // Don't cache the result; we don't hold the lock so it may have 
            // changed already. Re-do the search from scratch instead.
            triedResolver = YES;
            goto retry;
        }
    
        // No implementation found, and method resolver didn't help. 
        // Use forwarding.
    
        _cache_addForwardEntry(cls, sel);
        methodPC = &_objc_msgForward_internal;
    
     done:
        unlockForMethodLookup();
    
        // paranoia: look for ignored selectors with non-ignored implementations
        assert(!(sel == (SEL)kIgnore  &&  methodPC != (IMP)&_objc_ignored_method));
    
        return methodPC;
    }
    

    通过上述代码,我们清晰地了解到了 runtime 库寻找 IMP 的过程。

    需要注意的是,在 superClass 中寻找 IMP 时,不论是在 cache methodLists 还是 methodLists 中找到 IMP,都会先存入当前 class 的 cache methodLists 再跳转到对应的方法实现。

    这是我写的 runtime 系列文章中的一篇,还有以下几篇从其他方面对 runtime 进行了介绍

    1. iOS runtime之消息转发
    2. iOS runtime 之 Class 和 MetaClass
    3. iOS runtime 之 Category
    4. Objective-C 深入理解 +load 和 +initialize

    参考资料

    如果您觉得本文对您有所帮助,请点击「喜欢」来支持我。

    转载请注明出处,有任何疑问都可联系我,欢迎探讨。

    相关文章

      网友评论

      • 大号鱼骨头:当前类的方法缓存或者方法列表里只有实例方法吗?
        進撃超人:Class metaClass = objc_getMetaClass([@"XXX" UTF8String]);
        Method *allMethods = class_copyMethodList(metaClass, &count);
        类方法是metaClass里面存储的。
        狂奔的兔子:是的,类方法是缓存在与类匹配的元类里的

      本文标题:深入理解 Objective-C 的方法调用流程

      本文链接:https://www.haomeiwen.com/subject/vfgplttx.html