Objective-C runtime机制(2)——消息机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013378438/article/details/80509863

当我们用中括号[]调用OC函数的时候,实际上会进入消息发送和消息转发流程:

消息发送(Messaging),runtime系统会根据SEL查找对用的IMP,查找到,则调用函数指针进行方法调用;若查找不到,则进入消息转发流程,如果消息转发失败,则程序crash并记录日志。

消息相关数据结构


SEL

SEL被称之为消息选择器,它相当于一个key,在类的消息列表中,可以根据这个key,来查找到对应的消息实现。

在runtime中,SEL的定义是这样的:

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

它是一个不透明的定义,似乎苹果故意隐藏了它的实现。目前SEL仅是一个字符串。

这里要注意,即使消息的参数不同或方法所属的类也不同,但只要方法名相同,SEL也是一样的。所以,SEL单独并不能作为唯一的Key,必须结合消息发送的目标Class,才能找到最终的IMP

我们可以通过OC编译器命令@selector()或runtime函数sel_registerName,来获取一个SEL类型的方法选择器。

method_t

当需要发送消息的时候,runtime会在Class的方法列表中寻找方法的实现。在方法列表中方法是以结构体method_t存储的。

struct method_t {
    SEL name;
    const char *types;
    IMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

可以看到method_t包含一个SEL作为key,同时有一个指向函数实现的指针IMPmethod_t还包含一个属性const char *types;
types是一个C字符串,用于表明方法的返回值和参数类型。一般是这种格式的:

v24@0:8@16

关于SEL type,可以参考Type Encodings

IMP

IMP实际是一个函数指针,用于实际的方法调用。在runtime中定义是这样的:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP是由编译器生成的,如果我们知道了IMP的地址,则可以绕过runtime消息发送的过程,直接调用函数实现。关于这一点,我们稍后会谈到。

在消息发送的过程中,runtime就是根据idSEL来唯一确定IMP并调用之的。

消息


当我们用[]向OC对象发送消息时,编译器会对应的代码修改为objc_msgSend, 其定义如下:

OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

其实,除了objc_msgSend,编译器还会根据实际情况,将消息发送改写为下面四个msgSend之一:
objc_msgSend
objc_msgSend_stret

objc_msgSendSuper
objc_msgSendSuper_stret

当我们将消息发送给super class的时候,编译器会将消息发送改写为**SendSuper的格式,如调用[super viewDidLoad],会被编译器改写为objc_msgSendSuper的形式。

objc_msgSendSuper的定义如下:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

可以看到,调用super方法时,msgSendSuper的第一个参数不是id self,而是一个objc_super *objc_super定义如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;

};

objc_super 包含两个数据,receiver指调用super方法的对象,即子类对象,而super_class表示子类的Super Class。

这就说明了在消息过程中调用了 super方法和没有调用super方法,还是略有差异的。我们将会在下面讲解。

至于**msgSend中以_stret结尾的,表明方法返回值是一个结构体类型。

objc_msgSend 的内部,会依次执行:

  1. 检测selector是否是应该忽略的,比如在Mac OS X开发中,有了垃圾回收机制,就不会响应retainrelease这些函数。
  2. 判断当前receiver是否为nil,若为nil,则不做任何响应,即向nil发送消息,系统不会crash。
  3. 检查Class的method cache,若cache未命中,则进而查找Classmethod list
  4. 若在Classmethod list中未找到对应的IMP,则进行消息转发
  5. 若消息转发失败,程序crash

objc_msgSend

objc_msgSend 的伪代码实现如下:

id objc_msgSend(id self, SEL cmd, ...) {
    if(self == nil)
        return 0;
    Class cls = objc_getClass(self);
    IMP imp = class_getMethodImplementation(cls, cmd);
    return imp?imp(self, cmd, ...):0;
}

而在runtime源码中,objc_msgSend方法其实是用汇编写的。为什么用汇编?一是因为objc_msgSend的返回值类型是可变的,需要用到汇编的特性;二是因为汇编可以提高代码的效率。

对应arm64,其汇编源码是这样的(有所删减):

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero     // nil check
END_ENTRY _objc_msgSend

虽然不懂汇编,但是结合注释,还是能够猜大体意思的。

首先,系统通过cmp x0, #0检测receiver是否为nil。如果为nil,则进入LNilOrTagged,返回0;

如果不为nil,则现将receiverisa存入x13寄存器;

x13寄存器中,取出isa中的class,放到x16寄存器中;

调用CacheLookup NORMAL,在这个函数中,首先查找class的cache,如果未命中,则进入objc_msgSend_uncached

objc_msgSend_uncached 也是汇编,实现如下:

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search

    MethodTableLookup
    br  x17

    END_ENTRY __objc_msgSend_uncached

其内部调用了MethodTableLookupMethodTableLookup是一个汇编的宏定义,其内部会调用C语言函数_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

最终,会调用到lookUpImpOrForward来寻找classIMP实现或进行消息转发。

lookUpImpOrForward

lookUpImpOrForward方法的目的在于根据classSEL,在class或其super class中找到并返回对应的实现IMP,同时,cache所找到的IMP到当前class中。如果没有找到对应IMPlookUpImpOrForward会进入消息转发流程。

lookUpImpOrForward 的简化版实现如下:

MP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 先在class的cache中查找imp
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

   runtimeLock.read();

    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();
        // 如果class没有被relize,先relize
        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        // 如果class没有init,则先init
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }


 retry:    
    runtimeLock.assertReading();

    // relaized并init了class,再试一把cache中是否有imp
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 现在当前class的method list中查找有无imp
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 在当前class中没有找到imp,则依次向上查找super class的方法列表
    {
        unsigned attempts = unreasonableClassCount();
        // 进入for循环,沿着继承链,依次向上查找super class的方法列表
        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.");
            }

            // 先找super class的cache
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 在super class 的cache中找到imp,将imp存储到当前class(注意,不是super  class)的cache中 
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    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;
                }
            }

            // 在Super class的cache中没有找到,调用getMethodNoSuper_nolock在super class的方法列表中查找对应的实现
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 在class和其所有的super class中均未找到imp,进入动态方法解析流程resolveMethod
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }

    // 如果在class,super classes和动态方法解析 都不能找到这个imp,则进入消息转发流程,尝试让别的class来响应这个SEL

    // 消息转发结束,cache结果到当前class
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

通过上的源码,我们可以很清晰的知晓runtime的消息处理流程:

  1. 尝试在当前receiver对应的class的cache中查找imp
  2. 尝试在class的方法列表中查找imp
  3. 尝试在class的所有super classes中查找imp(先看Super class的cache,再看super class的方法列表)
  4. 上面3步都没有找到对应的imp,则尝试动态解析这个SEL
  5. 动态解析失败,尝试进行消息转发,让别的class处理这个SEL

在查找class的方法列表中是否有SEL的对应实现时,是调用函数getMethodNoSuper_nolock

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

方法实现很简单,就是在class的方法列表methods中,根据SEL查找对应的imp

PS:这里顺便说一下Category覆盖类原始方法的问题,由于在methods中是线性查找的,会返回第一个和SEL匹配的imp。而在classrealizeClass方法中,会调用methodizeClass来初始化class的方法列表。在methodizeClass方法中,会将Category方法和class方法合并到一个列表,同时,会确保Category方法位于class方法前面,这样,在runtime寻找SEL的对应实现时,会先找到Category中定义的imp返回,从而实现了原始方法覆盖的效果。 关于Category的底层实现,我们会在其他章节中讲解。

关于消息的查找,可以用下图更清晰的解释:

这里写图片描述

runtime用isa找到receiver对应的class,用superClass找到class的父类。

这里用蓝色的表示实例方法的消息查找流程:通过类对象实例的isa查找到对象的class,进行查找。

紫色表示类方法的消息查找流程: 通过类的isa找到类对应的元类, 沿着元类的super class链一路查找

关于元类,我们在上一章中已经提及,元类是“类的类”。因为在runtime中,类也被看做是一种对象,而对象就一定有其所属的类,因此,类所属的类,被称为类的元类(meta class)

我们所定义的类方法,其实是存储在元类的方法列表中的。

关于元类的更多描述,可以查看这里

动态解析

如果在类的继承体系中,没有找到相应的IMP,runtime首先会进行消息的动态解析。所谓动态解析,就是给我们一个机会,将方法实现在运行时动态的添加到当前的类中。然后,runtime会重新尝试走一遍消息查找的过程:

// 在class和其所有的super class中均未找到imp,进入动态方法解析流程resolveMethod
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
    }

在源码中,可以看到,runtime会调用_class_resolveMethod,让用户进行动态方法解析,而且设置标记triedResolver = YES,仅执行一次。当动态解析完毕,不管用户是否作出了相应处理,runtime,都会goto retry, 重新尝试查找一遍类的消息列表。

根据是调用的实例方法或类方法,runtime会在对应的类中调用如下方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel  // 动态解析实例方法
+ (BOOL)resolveClassMethod:(SEL)sel     // 动态解析类方法

resolveInstanceMethod

+ (BOOL)resolveInstanceMethod:(SEL)sel用来动态解析实例方法,我们需要在运行时动态的将对应的方法实现添加到类实例所对应的类的消息列表中:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(singSong)) {
        class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(unrecoginzedInstanceSelector)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

- (void)unrecoginzedInstanceSelector {
    NSLog(@"It is a unrecoginzed instance selector");
}

resolveClassMethod

+ (BOOL)resolveClassMethod:(SEL)sel用于动态解析类方法。 我们同样需要将类的实现动态的添加到相应类的消息列表中。

但这里需要注意,调用类方法的‘对象’实际也是一个类,而类所对应的类应该是元类。要添加类方法,我们必须把方法的实现添加到元类的方法列表中。

在这里,我们就不能够使用[self class]了,它仅能够返回当前的类。而是需要使用object_getClass(self),它其实会返回isa所指向的类,即类所对应的元类

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(payMoney)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(unrecognizedClassSelector)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (void)unrecognizedClassSelector {
    NSLog(@"It is a unrecoginzed class selector");
}

这里主要弄清楚,元类实例方法类方法在不同地方存储,就清楚了。

关于class方法和object_getClass方法的区别:

self是实例对象时,[self class]object_getClass(self)等价,因为前者会调用后者,都会返回对象实例所对应的类。

self是类对象时,[self class]返回类对象自身,而object_getClass(self)返回类所对应的元类

消息转发

当动态解析失败,则进入消息转发流程。所谓消息转发,是将当前消息转发到其它对象进行处理。

- (id)forwardingTargetForSelector:(SEL)aSelector  // 转发实例方法
+ (id)forwardingTargetForSelector:(SEL)aSelector  // 转发类方法,id需要返回类对象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果forwardingTargetForSelector没有实现,或返回了nilself,则会进入另一个转发流程。
它会依次调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,然后runtime会根据该方法返回的值,组成一个NSInvocation对象,并调用- (void)forwardInvocation:(NSInvocation *)anInvocation注意,当调用到forwardInvocation时,无论我们是否实现了该方法,系统都默认消息已经得到解析,不会引起crash。

整个消息转发流程可以用下图表示:
这里写图片描述

注意,和动态解析不同,由于消息转发实际上是将消息转发给另一种对象处理。而动态解析仍是尝试在当前类范围内进行处理。

消息转发 & 多继承

通过消息转发流程,我们可以模拟实现OC的多继承机制。详情可以参考官方文档
这里写图片描述

直接调用IMP

runtime的消息解析,究其根本,实际上就是根据SEL查找到对应的IMP,并调用之。如果我们可以直接知道IMP的所在,就不用再走消息机制这一层了。似乎不走消息机制会提高一些方法调用的速度,但现实是这样的吗?

我们比较一下:

CGFloat BNRTimeBlock (void (^block)(void)) {
    mach_timebase_info_data_t info;
    if (mach_timebase_info(&info) != KERN_SUCCESS) return -1.0;

    uint64_t start = mach_absolute_time ();
    block ();
    uint64_t end = mach_absolute_time ();
    uint64_t elapsed = end - start;

    uint64_t nanos = elapsed * info.numer / info.denom;
    return (CGFloat)nanos / NSEC_PER_SEC;

} // BNRTimeBlock

 Son *mySon1 = [Son new];
 setter ss = (void (*)(id, SEL, BOOL))[mySon1 methodForSelector:@selector(setFilled:)];
    CGFloat timeCost1 = BNRTimeBlock(^{
        for (int i = 0; i < 1000; ++i) {
            ss(mySon1, @selector(setFilled:), YES);
        }
    });

CGFloat timeCost2 = BNRTimeBlock(^{
        for (int i = 0; i < 1000; ++i) {
            [mySon1 setFilled:YES];
        }
    });

将timeCost1和timeCost2打印出来,你会发现,仅仅相差0.000001秒,几乎可以忽略不计。这样是因为在消息机制中,有缓存的存在。

参考文献

Objective-C Runtime
Objective-C 消息发送与转发机制原理
object_getClass与objc_getClass的不同

猜你喜欢

转载自blog.csdn.net/u013378438/article/details/80509863