1. 程式人生 > >Objective-C runtime機制(2)——訊息機制

Objective-C runtime機制(2)——訊息機制

當我們用中括號[]呼叫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)), "[email protected]:");
        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)), "[email protected]:");
        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的不同