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