runtime的那些事(四)—— selector、IMP、Method

之前對 NSObject 類內部結構體做了一個基本的分析。原本是想從 runtime 層面上整理消息傳遞流程,但為了能夠順暢的整理知識點,決定這篇還是先整理幾個非常重要的結構體概念。

目錄

1. selector

2. IMP

3. Method


1. selector

?selector 是指方法選擇器,在面向對象里可以理解為函數(shù)的指針。@selector() 作用就是在指定類中尋找指定名稱的方法。
&emsp關于 selector 的用法,其返回類型為 SEL。關于 SEL 的定義,最權威的還是在官方文檔中的解釋。SEL官方文檔鏈接
?關于官方文檔對于 SEL 的聲明,翻譯過來大意如下:selector 方法選擇器用于在運行時表示方法的名稱,一個 selector 選擇器其實就是已經(jīng)向運行時注冊或者映射過的C字符串,通過編譯器生成的 selector 選擇器在類加載時由運行時自動映射。允許在運行時添加新的 selector 選擇器,并可以使用函數(shù) sel_registerName 檢索已有的 selector 選擇器。但是在使用 selector 選擇器時,必須使用函數(shù) sel_registerName 或者 Objective-C 編譯器的指令 @selector() 返回的值,而不能直接將 C字符串強制轉換成 SEL。
關于 SEL 在 runtime 中的定義,在 runtime 源碼中僅僅是找到了結構體的聲明。

typedef struct objc_selector *SEL;

?雖然看不到關于 struct objc_selector 的內部聲明,但是可以去推測內部結構。在結構體中,一定會有一個 char 類型的變量用于存儲該函數(shù)名的C字符串。
?關于 selector 創(chuàng)建與獲取,不管是創(chuàng)建 @selector() 、還是獲取 NSSelectorFromString()method_getName(),其底層的實現(xiàn)都是通過 sel_registerName 函數(shù)來實現(xiàn)的。

關于 sel_registerName() 函數(shù)的底層實現(xiàn)

從 runtime 源碼 objc-sel.mm 文件中找到了其定義。

SEL sel_registerName(const char *name) {
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}

內部通過C函數(shù) static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 來完成實現(xiàn)。

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;

    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
    if (namedSelectors) {
        result = (SEL)NXMapGet(namedSelectors, name);
    }
    if (result) return result;

    // No match. Insert.

    if (!namedSelectors) {
        namedSelectors = NXCreateMapTable(NXStrValueMapPrototype, 
                                          (unsigned)SelrefCount);
    }
    if (!result) {
        result = sel_alloc(name, copy);
        // fixme choose a better container (hash not map for starters)
        NXMapInsert(namedSelectors, sel_getName(result), result);
    }

    return result;
}

?上述函數(shù)中,result SEL 類型的變量就是最終返回的結果。從源碼中初步看了下,會發(fā)生四種不同的 SEL 類型結果返回情況。從上往下的順序依次是:

  1. 當傳入方法名為 nil 時,則直接返回內容為0的值;
  2. 再傳入的方法名與 builtins 中的進行比對,若存在相同方法名,則直接返回 builtins 中的方法名。
    (PS:此處的 builtins 作用為生成一個共享緩存,用于保存預先優(yōu)化過的選擇器,以此可以實現(xiàn)更快速地查找方法,該函數(shù)的實現(xiàn)是由 C++ 定義的命名空間 objc_opt 來完成。關于 builtins 的實現(xiàn)原理就不展開了,以后有時間再細細研究 C++ 的命名空間以及 objc_opt 的內部細節(jié)。)
  3. 若上述流程未找到,則將傳入的方法名作為 key,去 NXMapTable 中去搜索 SEL 類型的結果。 NXMapTable 的作用就是將方法名與對應的 SEL 字符串進行綁定映射,并存入該哈希表中。
  4. 若上述哈希表依然沒有找到,則會將當前的方法名創(chuàng)建新的 SEL,并將 SEL 插入至 NXMapTable 中保存與對應方法名的映射關系。同時將該方法名創(chuàng)建的 SEL 作為返回值返回。

創(chuàng)建 selector 途徑有:

  • sel_registerName
  • @selector()

獲取 selector 的途徑有:

  • NSSelectorFromString()
  • method_getName()

通過官方文檔對 NSSelectorFromString 的解釋,將一個方法名的UTF-8編碼字符串傳給 sel_registerName 函數(shù)并返回 SEL;關于 method_getName() 函數(shù)的實現(xiàn),通過 runtime 源碼層面也可以發(fā)現(xiàn)也是通過 sel_registerName 來完成;而編譯器指令 @selector()
因此,關于 selector 的簡要總結:

  • selector 返回的類型為 SEL;
  • SEL 是指向 objc_selector 結構體的指針;
  • objc_selector 雖然并沒有公開結構體的實現(xiàn),但其內部至少存在一個保存 selector 名字的字符串變量;
  • 關于 selector 的創(chuàng)建,若與共享緩存、NXMapTable映射表中的都未注冊,則創(chuàng)建一個新的 SEL 并插入至 NXMapTable 中,同時保存于方法名的映射關系。

2. IMP

?IMP 表示指向方法實現(xiàn)地址的指針,當發(fā)起 Objective-C 消息后,最終要執(zhí)行的代碼就是由 IMP 指針來決定,SEL 的目的是為了查找方法最終實現(xiàn)的 IMP。若通過獲取到實例對象指定方法的 IMP 并直接調用,則可以繞過消息傳遞流程,直接執(zhí)行 IMP 對應的方法,這樣可以提升訪問效率。但也就意味著編譯器并不會檢查直接通過 IMP 去執(zhí)行指定的方法,編譯時期編譯器并不能判斷是否調用 IMP 錯誤,只有在運行時執(zhí)行到 IMP 指向的方法實現(xiàn)時,才能判斷是否正確。
關于 IMP 的定義

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

?第一個參數(shù)傳入一個指向 self 指針(指定類生成的實例對象的內存,或者類方法時指向元類的指針),第二個參數(shù)傳入方法選擇器,后續(xù)參數(shù)為可配置參數(shù)。
?調用 IMP 的方式在默認生成的項目工程下,調用編譯器獲取 IMP 會直接報錯,項目配置中默認為下圖配置:



?這樣的話,IMP 被定義為無參數(shù)無返回類型的函數(shù),關閉即可。還有更高效的方法,就是重新定義一個和有參數(shù)的 IMP 指針相同類型的指針,并把獲取到 IMP 時將其強轉為該類型。


3. Method

Method 結構體定義 typedef struct method_t *Method;,順藤摸瓜去查看 method_t 的結構體內容。

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP 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; }
    };
};

?結構體中,有關鍵作用的成員變量包含 SEL name; 方法名、const char *types; 返回類型的 encode 碼以及 MethodListIMP imp; 方法地址的指針。
關于 Method 的存儲位置,在runtime的那些事(二)——NSObject數(shù)據(jù)結構文章中已經(jīng)有過說明,在編譯時存放于 objc_class -> class_data_bits_t bits -> class_ro_t -> method_arrary_t *baseMethodList 中,而到了運行時 Method 會再存放于 objc_class -> class_data_bits_t bits -> class_rw_t -> method_arrary_t methods中。
?關于 Method 的初始化,是在 static Class realizeClass(Class cls) 函數(shù)中完成的,runtime的那些事(二)——NSObject數(shù)據(jù)結構也針對該函數(shù)做了源碼層面的分析,這里不再進行說明。
在 Objective-C 語言中,允許我們通過 BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) 函數(shù)在運行時動態(tài)加載新的 Method 方法。

BOOL 
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
    if (!cls) return NO;

    mutex_locker_t lock(runtimeLock);
    return ! addMethod(cls, name, imp, types ?: "", NO);
}

static IMP 
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
    IMP result = nil;

    runtimeLock.assertLocked();

    checkIsKnownClass(cls);
    
    assert(types);
    assert(cls->isRealized());

    method_t *m;
    if ((m = getMethodNoSuper_nolock(cls, name))) {
        // already exists
        if (!replace) {
            result = m->imp;
        } else {
            result = _method_setImplementation(cls, m, imp);
        }
    } else {
        // fixme optimize
        method_list_t *newlist;
        newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
        newlist->entsizeAndFlags = 
            (uint32_t)sizeof(method_t) | fixed_up_method_list;
        newlist->count = 1;
        newlist->first.name = name;
        newlist->first.types = strdupIfMutable(types);
        newlist->first.imp = imp;

        prepareMethodLists(cls, &newlist, 1, NO, NO);
        cls->data()->methods.attachLists(&newlist, 1);
        flushCaches(cls);

        result = nil;
    }

    return result;
}

?在向 Class 添加 Method 時,判斷要添加的 Method 是否已存在。若存在相同的 SEL 方法名,根據(jù) BOOL 類型變量 replace 判斷,若為 NO,則從已有的 Method 中取出 IMP 并返回;若為 YES則會將新的 IMP 與 對應的 SEL 方法名進行映射綁定。當 Class 中不存在指定的 SEL 方法名,則會向 Class 結構體中 class_rw_t 下的 method_array_t *methods 列表中注冊添加新的 Method ,添加完成后當前 Class 類的內存地址發(fā)生變化,必須清除 Class 類以及子類的 bucket 緩存。


?此篇文章,先對 selector、IMP、Method 的概念做一次整理,下一篇文章會嘗試從 runtime 源碼上研究下消息傳遞的完整流程。

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

推薦閱讀更多精彩內容