iOS源碼分析之IMP查找及消息轉發

在xcode中使用快捷鍵command+shift+0打開官方文檔,搜索Objective-C Runtime可以看到有這樣一段描述:

Overview
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.
...
譯文:Objective-C運行時是一個運行時庫,它為Objective-C語言的動態屬性提供支持,因此所有Objective-C應用程序都鏈接到它。Objective-C運行時庫支持在在/usr/lib/libobjc.A.dylib的共享庫中實現的函數。

我們都知道OC是一門動態性語言,主要是因為蘋果公司基于Runtime,在C語言的基礎上,加上編譯器運行時環境組成了OC語言的基本框架。編譯器clang可以將OC語言轉換成C++語言,而cc++都是靜態語言,一經編輯之后就不能再添加新的類、新的方法或者其他結構上的一些變化,而Runtime則可以實現這些動態性。

關于Runtime的基本概念以及簡單應用,我在之前的文章已經寫過傳送門,本文主要想通過源碼進行分析,分析過程比較長,可直接在文章末尾看總結。蘋果開源官網下載objc4-818.2源碼,如果想要調試源碼可以下載大神編譯好的objc4_debug


Objective-C從三種不同的層級與Runtime進行交互

  • 通過Objective-C源代碼:大部分情況下運行時系統在后臺自動運行,我們只需要編輯Objective-C源代碼,當編譯OC類和方法時,編譯器為了實現語言動態特性將自動創建一些數據結構和函數。
  • 通過NSObject的方法間接調用:Cocoa程序中大部分的類都是繼承自NSObject,繼承了NSObject的行為。某些情況下NSObject類僅僅是定義了方法模板而沒有提供所有需要的代碼,如 description方法會返回該類內容的字符串表示,然后NSObject并不知道子類中的內容,所以它只是返回類的名字和對象的地址,子類可以重新實現該方法以提供更多的信息。有些方法只是簡單的從Runtime系統中獲取信息,從而對允許對象進行一定程序的自我檢查,如class,superclass,isMemberOfClass,isKindOfClass,isSubclassOfClass,isAncestorOfObject,respondsToSelector,conformsToProtocol,methodForSelector這些方法的實現都調用了runtime中定義的方法。
  • 直接通過Runtime提供的API:Runtime提供的API很多,大致可以總結為幾大類
    • objc_xxx函數:這些函數是最頂層的操作,類或協議開配空間創建、注冊,類、元類對象、協議獲取,操作關聯對象,發送objc消息objc_msgSend
    • class_xxx函數:這些函數是對類的內部進行操作,如創建類的實例,添加實例變量、屬性、方法、協議,獲取、拷貝類包含的信息,檢查判斷。
    • object_xxx函數:針對對象進行操作,如獲取/設置對象的類、實例變量的值。
    • method_xxx函數:針對方法進行操作,獲取方法的參數及返回值,方法的實現以及交換等。
    • property_xxx函數:獲取屬性名稱、特性列表,拷貝屬性列表、特性值。
    • protocol_xxx函數:與協議相關操作
    • ivar_xxx函數:獲取實例變量的名稱、類型編碼、偏移量
    • sel_xxx函數:方法編號相關
    • imp_xxx函數:方法實現相關的block創建、獲取、刪除

clang分析

創建一個類繼承自NSObject例如CLAnimal,添加一個方法-(void)eat,在main.m中初始化并調用CLAnimal *ani = [[CLAnimal alloc] init]; [ani eat];,使用clang命令將main.m轉換為main.cpp文件

#終端命令  -rewrite-objc           Rewrite Objective-C source to C++
clang -rewrite-objc main.m

查看clang生成的main.cpp文件最后面如下

...此處省略十萬八千行...
typedef struct objc_object CLAnimal;
typedef struct {} _objc_exc_CLAnimal;
#endif

struct CLAnimal_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};


// -(void)eat;
/* @end */

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2__ts8l68y17cncks73d_vjz4vh0000gn_T_main_bf6255_mi_0);

        CLAnimal *animal = ((CLAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((CLAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CLAnimal"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("eat"));

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

我們同樣可以直接使用使用這種寫法,需要導入頭文件#import <objc/message.h>,另外往父類發送消息有些不同objc_msgSendSuper(id super, SEL _cmd), 其中super是一個結構體objc_super{ id receiver,Class super_class}, 從super_class中查找方法,消息接受者super->receiver仍然是self

從上面的代碼可以很直觀的得出兩個結論:

  • OC對象的本質是就是一個結構體objc_object
  • 方法的本質就是消息發送objc_msgSend(id self, SEL _cmd)id類型參數表示調用者,_cmd表示方法編號

objc_msgSend的實現過程

從源碼中查找c函數objc_msgSend,發現并沒有它的實現,因為它是使用匯編來實現的,搜索對應的匯編代碼ENTRY _objc_msgSend。為什么要使用匯編呢?原因有兩個,一是因為C函數不能直接保留未知的參數,然后跳轉到任意的指針,但是匯編有寄存器可以保存x0~x31,二是因為使用匯編效率高,匯編語言是二進制指令的文本形式,與指令是一一對應的關系,是最底層的低級語言。

    ENTRY _objc_msgSend //方法入口
    UNWIND _objc_msgSend, NoFrame 

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

匯編部分我們可以大致分析一下:

  • 首先檢查是否為niltagged pointer類型(NSNumber、NSDate、NSString等小對象),是執行LNilOrTagged,再次檢查是nil則跳轉到LReturnZero結束,tagged pointer類型執行GetTaggedClass ---> LGetIsaDone
  • 第一步檢查不是nil或tagged pointer類型將會繼續執行GetClassFromIsa_p16 ---> LGetIsaDone
  • LGetIsaDoneisa操作之后會調用CacheLookup 在緩存中查找imp,找到則調用 imp,找不到調用objc_msgSend_uncached
  • LGetIsaDone中只是對CacheLookup這個宏的調用CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
   // calls imp or objc_msgSend_uncached
  .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
   LLookupStart\Function:
  1.  ldp   p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
      cmp   p9, p1              //     if (sel != _cmd) {
      b.ne  3f              //         scan more
  2.  CacheHit \Mode                // hit:    call or return imp
  3.  cbz   p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
      #if CACHE_MASK_STORAGE == ...
      add ...
  4.  ldp   p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
      cmp   p9, p1              //     if (sel == _cmd)
      b.eq  2b              //         goto hit
      cmp   p9, #0              // } while (sel != 0 &&
      ccmp  p13, p12, #0, ne        //     bucket > first_probed)
      b.hi  4b

LLookupStart\Function:可以看到查找的4種情況(通過注釋以及單詞大致推測):

  1. sel_cmd不匹配,繼續
  2. 執行或返回imp
  3. 找不到則調用MissLabelDynamicCacheLookup的第三個參數__objc_msgSend_uncached,隨后添加到緩存中
  4. 根據sel循環查找,查找到執行2
  • 先看下CacheHit這個方法,它也是個宏,這里進入時為NORMAL,直接調用IMPTailCallCachedImp x17, x10, x1, x16
  • 接下來我們需要看一下__objc_msgSend_uncached的實現
  • 繼續搜索跟蹤發現它的實現里面又調用了MethodTableLookup,翻譯一下就是方法列表查找,而它下面TailCallFunctionPointer x17翻譯一下就是調用函數指針,很明顯得出結論是在MethodTableLookup這個方法中進行的IMP查找,不然下面x17里面的地址從哪來呢。
  • 查看MethodTableLookup實現,bl跳轉到_lookUpImpOrForward,繼續搜索_lookUpImpOrForward,咦?全是callbl,沒有實現??仔細看匯編實現里面有兩行注釋,原來是在C函數lookUpImpOrForward里面
.macro MethodTableLookup
    SAVE_REGS MSGSEND
    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward
    // IMP in x0
    mov x17, x0
    RESTORE_REGS MSGSEND
.endmacro

最終在objc_runtime_new.mm文件中找到它的實現方法,看到這里終于舒服一些了,這一刻我忽然覺得C語言竟然也是這么的親切~,這個方法就是漫長的IMP查找流程,我在里面加了一些中文注釋,最新的源碼與之前的版本有些不一樣,但是大體的邏輯都是一樣的

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    runtimeLock.assertUnlocked();
   if (slowpath(!cls->isInitialized())) {//判斷類是否已經被初始化通過元類中的class_rw_t中的flag字段
       behavior |= LOOKUP_NOCACHE;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);//檢查是否為runtime中已知的類,包括共享緩存,加載的image或使用api注冊的
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    runtimeLock.assertLocked();
    curClass = cls;
   for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
           // curClass method list.獲取當前類的方法列表中查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done; //若找到,則跳轉到done位置
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                //將curClass的父類 賦值給curClass
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;//若當前類是NSObject,那么不需要再繼續查找了,跳出循環 給imp賦值為forward_imp
            }
        }
         // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache. 
        //當前類不是NSObject,查找父類緩存,這里又是匯編查找:ENTRY _cache_getImp --> CacheLookUp
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            //找到的是一個轉發方法,停止搜索,且不進行緩存
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            //在父類中找到了這個方法,緩存它到當前類
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

 // No implementation found. Try method resolver once.
//最終沒有找到實現方法,那么嘗試一次調用resolver
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
    //將imp添加到當前類緩存的方法
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

至此IMP查找環節就結束了,若沒有查找到IMP則開始動態方法解析


動態解析

接著上一步IMP查找,如果沒有找到,則會調用resolveMethod_locked,在此方法中會根據是否為元類分別調用resolveClassMethodresolveInstanceMethod

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

以類方法resolveInstanceMethod為例,我們可以寫個示例測試一下,調用一個未實現的SEL,并重寫resolveInstanceMethod,然而發現,這個方法竟然被調用了兩次?

image.png

仔細分析兩次的調用棧并不一樣,第一次調用確實是與剛才的分析一致,第二次是從methodSignatureForSelector調用,具體的流程暫且不清楚,但是可以確認的一點是我們還可以在第一次調用resolveInstanceMethod:之后的methodSignatureForSelector方法內去處理消息。

我們嘗試使用runtime動態添加添加一個IMP,代碼示例,果然不再崩潰,另外很多人會被那張經典的消息轉發流程圖所誤導,以為只有返回YES消息才是已處理,NO會嘗試消息轉發,其實不然,查看源碼發現方法返回值只跟日志打印有關系,只有在返回YES并且OBJC_PRINT_RESOLVED_METHODS = YES時才會打印,可以自己嘗試驗證一下

image.png

image.png

若沒有處理消息會發生什么呢?通過debug源碼跟蹤發現在動態方法解析之后會再次調用lookUpImpOrForward方法,并在最后一次for循環也就是NSObject層時,會對IMP進行賦值為forward_imp,這個forward_imp = _objc_msgForward_impcache,即消息轉發。這個方法是匯編實現的

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache

    
    ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

經過一頓搜索查找。。。跟不下去了,沒有后續的源碼實現了,蘋果對這塊并沒有開源,怎么辦呢?我們只能通過查看log來試試,在源碼內部有一個非常好用的函數

// env NSObjCMessageLoggingEnabled
OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

從注釋可以看到我們可以直接添加環境變量NSObjCMessageLoggingEnabled來使用,它會輸出一個tmp/msgSends-(xxx pid)的文件,搜索logMessageSend方法可以看到寫入文件路徑,這里的完整路徑是Macintosh HD --> private --> tmp --> msgSends-xxx,在818.2源碼添加完可能沒有寫入成功,將源碼中的objcMsgLogLock注釋掉,然后重新編譯一下objc再進行debug即可

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
 {
    ...
    //    objcMsgLogLock.lock();
        write (objcMsgLogFD, buf, strlen(buf));
    //    objcMsgLogLock.unlock();
  }
image.png

可以看到在崩潰之前,它內部的方法調用順序,在動態解析之后調用了forwardingTargetForSelector:方法,然后又調用了methodSignatureForSelector,哈哈,爽歪歪有沒有?

消息轉發

這部分源碼并沒有開源,我們只能得到它的調用順序,然我們可以在蘋果的官方文檔查看forwardingTargetForSelector用法和描述,使用command+shift+0打開官方文檔,只摘重點

If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, 
that returned object is used as the new receiver object and the message dispatch resumes to that new object. 

如果一個對象實現(或繼承)這個方法,并返回一個非nil(非self)結果,
那么返回的對象將被用作新的接收方對象,消息調度將恢復到這個新對象。

This is useful when you simply want to redirect messages to another object 
and can be an order of magnitude faster than regular forwarding.

當您只想將消息重定向到另一個對象時使用此方法比常規轉發快一個數量級。

快速轉發簡單使用示例:


image.png

同樣的對于methodSignatureForSelector也只能通過官方文檔查閱,可以知道該方法用于協議的實現。此方法還用于必須創建NSInvocation對象的情況,例如在消息轉發期間。如果您的對象維護委托或能夠處理它不直接實現的消息,則應該重寫此方法以返回適當的方法簽名。

This method is used in the implementation of protocols. 
This method is also used in situations where an NSInvocation object must be created, 
such as during message forwarding. 
If your object maintains a delegate or is capable of handling messages 
that it does not directly implement, 
you should override this method to return an appropriate method signature.

慢速轉發流程示例:


image.png

如果對消息沒有處理最終會調用doesNotRecognizeSelector崩潰


總結

最后總結一下整個IMP查找,動態解析以及消息轉發的完整過程,它是由匯編,c/c++共同完成的

  • 首先會進行匯編的快速查找ENTRY _objc_msgSend,在當前類的緩存中查找,若有則直接調用IMP。在runtime中對應的查詢路徑為objc_class-->cache_t-->buckets-->{sel, imp},查找過程使用了哈希算法
  • 匯編查找不到則會調用c的方法_lookUpImpOrForward
  • _lookUpImpOrForward查找流程為先在自己的方法列表中查找,若找到則返回IMP,并緩存
  • 如果在自己的方法列表中查找不到,判斷當前是否為NSObject類,如果是跳出循環;
  • 循環查詢父類的緩存,在父類緩存中查詢到的IMP若為forward_imp,那么直接調用這個IMP且不做緩存,若不是將它緩存到當前類
  • 在循環中繼續查詢父類方法列表,直到根類NSObject
  • 循環查詢父類方法列表,直到根類NSObject
  • 若最終都沒有找到,則開始動態方法解析resolveInstanceMethod,類方法是resolveClassMethod。這個方法返回值只影響log打印。
  • 再次執行 _lookUpImpOrForward,循環查找是否實現了resolveInstanceMethod直到NSObject,最終會將forward_imp賦值給IMP并返回
  • forward_imp進入消息轉發流程,這塊是由匯編寫的,可通過instrumentObjcMessageSends方法開啟寫入日志查看崩潰前的方法調用順序日志,然后查看官方文檔解釋作進一步分析得出,在崩潰前消息轉發有快速轉發和慢速轉發兩種。快速轉發是forwardingTargetForSelector提供一個對象處理消息。慢速轉發是使用forwardInvocation:methodSignatureForSelector:兩個方法。
  • 若消息最后沒有處理,將會拋出異常doesNotRecognizeSelector

PS:本文是對照最新的objc4-818.2源碼進行對照,與舊版本方法以及參數有些出入的地方,另外對于匯編不太了解,基本上都是按照推測來寫,如有錯誤請指正。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容