2019-10-15數組
類的方法包括實例方法(instance method)和類方法(class method),二者保存在徹底不一樣的地方,實例方法保存在類的方法列表中,類方法保存在元類的方法列表中。在Runtime源代碼解讀(實現面向對象初探)中已介紹過方法的基本數據結構objc_method
,以及方法的基本響應鏈,本文介紹方法列表的具體實現原理。緩存
在 Runtime源代碼解讀2(類和對象)中介紹過:類的class_ro_t
包含method_list_t
類型的baseMethodList
成員;類的class_rw_t
包含method_array_t
類型的methods
成員。二者一樣保存方法列表,卻使用不一樣的數據結構,那麼它們之間有什麼關係呢?因爲class_ro_t
是保存的基本上是編譯時決議的數據,baseMethodList
顯然是保存類定義時所定義的方法,這些方法是編譯時決議的。而class_rw_t
的methods
實際上纔是類的完整方法列表,並且class_rw_t
的methods
包含了class_ro_t
的baseMethodList
。bash
類的class_rw_t
保存方法列表保存形式並非簡單的一維數組結構,而是二維數組結構,這是爲了區分類構建階段定義的基本方法,以及不一樣分類之間的定義的方法。objc_class
保存方法列表的數組結構是method_array_t
類,method_array_t
繼承list_array_tt
模板類。數據結構
Runtime 定義list_array_tt
類模板表示二維數組容器。list_array_tt
保存的數據主體是一個聯合體,包含list
和arrayAndFlag
成員,表示:容器要麼保存一維數組,此時聯合體直接保存一維數組的地址,地址的最低位必爲0
;要麼保存二維數組,此時聯合體保存二維數組的首個列表元素的地址,且最低位置爲1
。調用hasArray()
方法能夠查詢容器是否保存的是二維數組,返回arrayAndFlag & 1
,即經過最低位是否爲1
進行判斷;調用list_array_tt
的array()
方法能夠獲取二維數組容器的地址,返回arrayAndFlag & ~1
,即忽略最低位。函數
當聯合體保存二維數組時,聯合體的arrayAndFlag
指向list_array_tt
內嵌定義的array_t
結構體。該結構體是簡單的一維數組容器,但其元素爲指向列表容器的指針,所以array_t
的本質是二維數組。調用array_t
的byteSize()
能夠返回列表容器佔用的總字節數,爲array_t
結構體自己的size
與保存列表元素的連續內存區塊的size
之和。post
// 二維數組容器模板
template <typename Element, typename List>
class list_array_tt {
// 定義二維數組的外層一維數組容器
struct array_t {
uint32_t count;
List* lists[0];
static size_t byteSize(uint32_t count) {
return sizeof(array_t) + count*sizeof(lists[0]);
}
// 計算容器佔用字節數
size_t byteSize() {
return byteSize(count);
}
};
protected:
// 內嵌迭代器的定義(後文 1.1.1 節介紹)
...
private:
union {
List* list; // 要麼指向一維數組容器
uintptr_t arrayAndFlag; // 要麼指向二維數組容器
};
// 容器是否保存的是二維數組
bool hasArray() const {
return arrayAndFlag & 1;
}
// 獲取容器保存的二維數組的地址
array_t *array() {
return (array_t *)(arrayAndFlag & ~1);
}
void setArray(array_t *array) {
arrayAndFlag = (uintptr_t)array | 1;
}
public:
// 計算方法列表二維數組容器中全部方法的數量
uint32_t count() {
uint32_t result = 0;
for (auto lists = beginLists(), end = endLists();
lists != end;
++lists)
{
result += (*lists)->count;
}
return result;
}
// 獲取方法列表二維數組容器的起始迭代器
iterator begin() {
return iterator(beginLists(), endLists());
}
// 獲取方法列表二維數組容器的結束迭代器
iterator end() {
List **e = endLists();
return iterator(e, e);
}
// 獲取方法列表二維數組容器包含的方法列表數量
uint32_t countLists() {
if (hasArray()) {
return array()->count;
} else if (list) {
return 1;
} else {
return 0;
}
}
// 獲取方法列表二維數組容器的起始地址
List** beginLists() {
if (hasArray()) {
return array()->lists;
} else {
return &list;
}
}
// 獲取方法列表二維數組容器的結束地址
List** endLists() {
if (hasArray()) {
return array()->lists + array()->count;
} else if (list) {
return &list + 1;
} else {
return &list;
}
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
// 後文 1.1.2 節介紹
...
}
void tryFree() {
// 後文 1.1.2 節介紹
...
}
template<typename Result> Result duplicate() {
// 後文 1.1.2 節介紹
...
}
};
複製代碼
list_array_tt
容器模板的內嵌迭代器iterator
類的定義以下,包含三個成員:ui
lists
:當前迭代到的數組的位置;listsEnd
:二維數組的外層容器的結尾;m
:當前迭代到的數組 中的元素的位置;mEnd
:當前迭代到的數組的結尾;注意:構建
list_array_tt
的迭代器時,只能從方法列表到方法列表,不能從容器中的方法列表的某個元素的迭代器開始迭代。this
class iterator {
List **lists;
List **listsEnd;
typename List::iterator m, mEnd;
public:
// 構建從begin指向的列表,到end指向的列表的迭代器
iterator(List **begin, List **end)
: lists(begin), listsEnd(end)
{
if (begin != end) {
m = (*begin)->begin();
mEnd = (*begin)->end();
}
}
const Element& operator * () const {
return *m;
}
Element& operator * () {
return *m;
}
bool operator != (const iterator& rhs) const {
if (lists != rhs.lists) return true;
if (lists == listsEnd) return false; // m is undefined
if (m != rhs.m) return true;
return false;
}
//迭代時,若達到當前數組的結尾,則切換到下一個數組的開頭
const iterator& operator ++ () {
assert(m != mEnd);
m++;
if (m == mEnd) {
assert(lists != listsEnd);
lists++;
if (lists != listsEnd) {
m = (*lists)->begin();
mEnd = (*lists)->end();
}
}
return *this;
}
};
複製代碼
list_array_tt
容器模板的三個操做類型的公開方法以下:編碼
attachLists(...)
:將列表元素添加到二維數組容器的開頭,注意到list_array_t
沒有定義構造函數,這是由於構造邏輯均在attachLists(...)
中,包含容器從空轉化爲保存一位數組,再轉化爲保存二維數組的處理邏輯;tryFree()
:釋放二維數組容器佔用內存;duplicate(...)
:複製二維數組容器;// 向二維數組容器中添加列表
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// 當容器中保存的是二維數組
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); // 分配新內存空間從新構建容器
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0])); // 轉移容器原內容到新內存空間
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0])); // 將須要新增的方法列表拷貝到新內存空間
}
else if (!list && addedCount == 1) {
// 當容器中無任何方法,直接將list成員指向addedLists
list = addedLists[0];
}
else {
// 當容器中保存的是一維數組
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
// 釋放容器佔用的內存空間
void tryFree() {
if (hasArray()) {
for (uint32_t i = 0; i < array()->count; i++) {
try_free(array()->lists[I]);
}
try_free(array());
}
else if (list) {
try_free(list);
}
}
// 拷貝容器
template<typename Result>
Result duplicate() {
Result result;
if (hasArray()) {
array_t *a = array();
result.setArray((array_t *)memdup(a, a->byteSize()));
for (uint32_t i = 0; i < a->count; i++) {
result.array()->lists[i] = a->lists[i]->duplicate();
}
} else if (list) {
result.list = list->duplicate();
} else {
result.list = nil;
}
return result;
}
複製代碼
類的方法列表信息保存在class_rw_t
結構體的methods
成員中,類型爲method_array_t
。method_array_t
是按list_array_tt
模板構建,以method_t
(方法)爲元素,以method_list_t
(方法列表)爲列表容器的二維數組容器,用於存儲類的方法列表。method_list_t
繼承自entsize_list_tt
順序表模板,entsize_list_tt
在 Runtime源代碼解讀2(類和對象) 介紹過,是具備固定類型元素的順序表容器,注意到FlagMask
指定爲0x03
,所以method_list_t
的entsizeAndFlags
成員最低兩位預留有特殊功能:若最低兩位均爲1
,表示該方法列表已排序。spa
由method_array_t
中擴展list_array_tt
的方法的方法名可見,method_array_t
是爲 category 量身定作的,顯然 category 中定義的全部方法都存儲在該容器中,每一個 category 定義的方法對應method_array_t
二維數組容器中的一個元素,也就是一個方法列表method_list_t
結構體的指針。擴展的方法以下:
beginCategoryMethodLists()
:指向容器的第一個數組;endCategoryMethodLists(Class cls)
:當類的class_rw_t
中不存在baseMethodList
時,直接返回容器最後一個數組,當存在baseMethodList
時,返回容器的倒數第二個數組。static uint32_t fixed_up_method_list = 3;
// 方法列表
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
// 查詢方法列表是否已排序
bool isFixedUp() const {
return flags() == fixed_up_method_list;
}
// 標記方法列表已排序
void setFixedUp() {
runtimeLock.assertLocked();
assert(!isFixedUp());
entsizeAndFlags = entsize() | fixed_up_method_list;
}
// 新增返回方法在順序表中的索引值的方法
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t I =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
assert(i < count);
return I;
}
};
// 方法列表二維數組容器
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls) {
method_list_t **mlists = beginLists();
method_list_t **mlistsEnd = endLists();
if (mlists == mlistsEnd || !cls->data()->ro->baseMethods())
{
return mlistsEnd;
}
return mlistsEnd - 1;
};
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
複製代碼
在 Runtime源代碼解讀2(類和對象) 中介紹過類的加載過程。從 class realizing 時調用的methodizeClass(...)
函數的處理邏輯能夠看出:class_rw_t
中的method_array_t
容器保存了類的完整方法列表,包括靜態編譯的類的基本方法、運行時決議的 category 中的方法以及運行時動態添加的方法。並且class_rw_t
中method_array_t
容器的最後一個數組實際上就是class_ro_t
的baseMethodList
。
再結合 1.1.2 介紹的list_array_tt
的attachLists(...)
方法邏輯,能夠基本瞭解方法列表容器的工做機制。當使用class_addMethod(...)
動態添加類,或者應用加載階段加載 category 時,均調用了該方法。因爲attachLists(...)
添加方法時,將方法添加到容器的開頭,將原有的method_list_t
集體後移,所以類的同名方法的IMP
的優先級從高到低排序以下:
class_addMethod(...)
動態添加的方法;類的方法列表的結構可總結以下圖所示。其中綠色表示method_list_t
容器,藍色表示保存method_t
結構體,黃色表示指向method_list_t
結構體的指針。上圖爲method_array_t
保存一維數組容器(method_list_t
)時的內存結構,此時method_array_t
的聯合體list
成員有效;下圖爲method_array_t
保存二維數組容器時的內存結構,此時method_array_t
的聯合體arrayAndFlag
成員有效。
運行時調用class_addMethod (...)
函數能夠給類動態添加方法,調用class_replaceMethod(...)
能夠替換方法的實現。實際上二者都調用了addMethod(...)
函數,原理是先根據傳入的方法名、方法IMP、類型編碼構建method_t
,而後新建method_list_t
方法列表容器將method_t
添加到其中,最後調用attachList(...)
將方法列表添加到類的class_rw_t
的methods
方法列表二維數組容器中。
class_addMethod (...)
的源代碼以下:
// 添加方法
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
rwlock_writer_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
// 替換方法實現
IMP
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
rwlock_writer_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
runtimeLock.assertWriting();
assert(types);
assert(cls->isRealized());
method_t *m;
if ((m = getMethodNoSuper_nolock(cls, name))) {
// 若方法已經存在
if (!replace) {
result = m->imp;
} else {
result = _method_setImplementation(cls, m, imp); // 設置方法IMP
}
} else {
// 若方法不存在,則構建方法列表容器
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 = strdup(types);
if (!ignoreSelector(name)) {
newlist->first.imp = imp;
} else {
newlist->first.imp = (IMP)&_objc_ignored_method;
}
prepareMethodLists(cls, &newlist, 1, NO, NO); // 後文 2.1 中詳細介紹
// 添加方法到類的class_rw_t的methods中
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls); // 後文 2.2中詳細介紹
result = nil;
}
return result;
}
// 設置方法IMP
static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertWriting();
if (!m) return nil;
if (!imp) return nil;
// 對於打開ARC的編譯環境,忽略retain/release等MRC方法
if (ignoreSelector(m->name)) {
return m->imp;
}
IMP old = m->imp;
m->imp = imp;
flushCaches(cls); // 清空cls類的方法緩衝
updateCustomRR_AWZ(cls, m); // 對retain/release等MRC方法,以及allocWithZone方法的特殊處理
return old;
}
複製代碼
向類動態添加方法,類的方法列表結構先後對比圖示以下。其中紅色標記部分爲新增結構。處理過程爲:將添加方法封裝到method_list_t
容器,而後插入到method_array_t
的外層一位數組開頭,其餘元素總體後移,外層一位數組長度增長1
。
addMethod(...)
函數中還調用了prepareMethodLists(...)
函數,該方法主要調用了fixupMethodList(...)
函數。fixupMethodList(...)
作了三件事情:
sel_registerNameNoLock
函數將方法列表中全部方法的SEL
註冊到內存,註冊的內存實際是以 方法名字符串 爲鍵值,將SEL
插入到系統維護的namedSelectors
哈希表中;isFixedUp
,表示方法列表已排序;之因此對方法列表排序,是爲了使方法列表支持二分查找,從而下降經過SEL
在方法列表中搜索IMP
的時間複雜度。prepareMethodLists(...)
函數相關代碼以下:
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
if (addedCount == 0) return;
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[I];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
}
}
static void
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
for (auto& meth : *mlist) {
const char *name = sel_cname(meth.name);
// 註冊方法列表中的方法名
SEL sel = sel_registerNameNoLock(name, bundleCopy);
meth.name = sel;
if (ignoreSelector(sel)) {
meth.imp = (IMP)&_objc_ignored_method;
}
}
// 按照SEL對方法列表排序
if (sort) {
method_t::SortBySELAddress sorter;
std::stable_sort(mlist->begin(), mlist->end(), sorter);
}
// 標記方法列表已經排好序並固定。
// method_list_t的entsizeAndFlags最低兩位均爲1表示方法列表已經排好序並固定
mlist->setFixedUp();
}
複製代碼
所謂方法緩衝(cache),是使用哈希表保存類的最近被調用的方法,利用哈希表的 O(1) 的查詢時間複雜度提升方法調用的效率。objc_class
的cache_t cache
成員就是類的方法緩衝。cache_t
結構體代碼以下。其實是指向一個能夠動態擴容的 Key-Value 形式的哈希表,以方法名SEL
爲Key,以方法IMP
爲Value,分別對應bucket_t
結構體的_sel
、_imp
成員。cache_t
結構體包含如下成員:
_buckets
:保存哈希表全部元素的數組,元素類型爲bucket_t
結構體,不是指向bucket_t
的指針; _mask
:間接表示哈希表的容量,爲所有位爲1
的二進制數。哈希表容量最小爲4,知足公式:哈希表容量 = _mask + 1
。擴容時設置_mask =(_mask + 1) & _mask
; _occupied
:哈希表實際緩存的方法個數;
前文addMethod(...)
的代碼中,調用flushCache(...)
時用於清空指定類的方法緩衝。之因此清空是由於addMethod(...)
可能會觸發方法IMP
變動,必須保證方法緩衝中保存的全部方法SEL
與IMP
映射的正確性。關於方法緩衝的具體細節見 Runtime 源代碼objc-cache.mm
不深刻介紹。
// 方法緩衝哈希表的元素,_sel爲key,_imp爲value
struct bucket_t {
private:
#if __arm64__
uintptr_t _imp;
SEL _sel;
#else
SEL _sel;
uintptr_t _imp;
#endif
...
public:
...
};
// 方法緩衝的數據結構
struct cache_t {
struct bucket_t *_buckets; //保存哈希表全部元素的數組
mask_t _mask; // 間接表示哈希表的容量
mask_t _occupied; //
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
複製代碼
調用方法經常使用的方式包括:
@interface
定義的公開接口;NSObject
類performSelector
系列方法;NSInvocation
類觸發;objc_msgSend(...)
消息發送函數;注意:
<objc/message.h>
公開的接口形式是無參的,能夠嘗試用相似BOOL (*msg)(Class, SEL, id) = (typeof(msg))objc_msgSend
處理,作強制轉換後再調用msg
函數指針以實現正常傳參,第一個參數是接收消息的對象(實例方法)/類(類方法),第二個參數是SEL
方法選擇器,後續可變長參數列表爲方法的參數列表
形式各有不一樣,可是本質上是作一樣的事情:首先轉化爲objc_msgSend(...)
消息發送的形式,而後經過 target
、selector
定位到方法的IMP
,最後調用IMP
指向的函數指針。因而關鍵點來到了方法IMP
定位上。從class_getMethodImplementation(Class cls, SEL sel)
源代碼能夠知道 Runtime 如何經過方法名SEL
定位 方法的實現IMP
。代碼以下:
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// 繼承鏈無該SEL對應的IMP,則從消息轉發流程中搜索IMP
if (!imp) {
return _objc_msgForward;
}
return imp;
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
複製代碼
最後定位到關鍵代碼lookUpImpOrForward(...)
,顧名思義是用於在類中查找IMP
,或者進入轉發。根據代碼邏輯,總結方法根據方法名SEL
定位方法的實現IMP
的主要處理流程以下:
class_rw_t
中的方法列表都有多是不完整的;initialize()
方法,由於其中可能包含動態添加方法邏輯;method_array_t
方法列表二維數組容器中的全部method_list_t
,在method_list_t
中搜索方法。若method_list_t
已排序,則使用二分法搜索,若method_list_t
未排序,則用簡單線性搜索。搜索到則馬上返回;true
,則回到第4步再次嘗試搜索;false
,則觸發消息轉發流程。返回_objc_msgForward_impcache
,返回該IMP
表示須要進入消息轉發流程;注意:
initialize()
方法和load()
方法的做用有點類似,都是用於類的初始化。但二者在行爲上有很大區別:一、load()
方法不具備繼承特性,繼承鏈上的全部類定義的全部load()
方法均會被調用,load()
方法中禁止使用super
關鍵字;intialize()
具備繼承特性,可以使用super
關鍵字,其行爲至關於普通的類方法。二、load()
方法在類加載時觸發,且 runtime 嚴格控制一個類的load()
方法只會被執行一次;initialize()
方法 在第一次調用類的方法時,在lookUpImpOrForward(...)
中檢查類是否初始化時觸發,所以若父類實現了initialize()
方法,可是其全部子類均未實現,則不管是第一次調用子類仍是父類的方法,都會觸發父類的initialize()
方法。
lookUpImpOrForward(...)
的源代碼看起來很長,可是基本上每一步都包含很關鍵的信息,對理解消息的響應流程很是重要。所以只刪除代碼中讀寫鎖、調試相關代碼,在代碼中作了詳細的註釋。
// Runtime預約義的統一的觸發方法動態解析的SEL,注意實際值應該並非NULL
SEL SEL_resolveInstanceMethod = NULL;
// Runtime預約義的標記進入消息轉發流程的IMP
extern void _objc_msgForward_impcache(void);
// 從cls類的`cache_t`方法緩存哈希表中查詢SEL對應的IMP
extern IMP cache_getImp(Class cls, SEL sel);
// 在cls類查詢SEL對應的IMP
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
// 在方法緩衝中搜索方法
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// 方法固定相關操做
if (!cls->isRealized()) {
realizeClass(cls);
}
// 若類的initialize()方法未調用,則先進行類的initialize初始化過程
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst)); //調用類的initialize()方法
}
retry:
// 若ARC編譯選項打開則須要忽略retain、release等方法,忽略
if (ignoreSelector(sel)) {
imp = _objc_ignored_method;
cache_fill(cls, sel, imp, inst);
goto done;
}
// 在類的cache_t方法緩存哈希表中查詢SEL對應的IMP
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 在類的method_array_t方法列表容器中查詢SEL對應的IMP
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// 沿着類的繼承鏈循環。在各級類及父類的方法緩衝、方法列表容器中查詢SEL對應的IMP
curClass = cls;
while ((curClass = curClass->superclass)) {
// 在類的方法緩存中查詢
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 若搜索到IMP,則將IMP寫入父類的方法緩存
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 在類的方法列表中查詢
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
// 嘗試動態解析方法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// 觸發消息轉發流程
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
return imp;
}
// 當前類的方法列表中搜索方法
static method_t *
getMethodNoSuper_nolock(Class cls, SEL 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;
}
// 簡單線性遍歷method_list_t搜索方法名爲sel的方法,查到馬上返回
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
//對於isFixedUp的方法列表用二分法搜索,這也是fixedUp時須要對方法列表排序的緣由
return findMethodInSortedMethodList(sel, mlist);
} else {
// 線性搜索,簡單遍歷
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
// 用二分查找從已排序的method_list_t中搜索方法名爲key的方法
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
複製代碼
當類及其繼承鏈的方法列表不存在SEL
對應的方法時,會進入方法動態解析 以及 消息轉發流程,Runtime 只公佈了觸發邏輯,實現細節則徹底隱藏在objc_msgSend
的實現中。觸發方法動態解析經過objc_msgSend
向類發送SEL_resolveInstanceMethod
消息,推測objc_msgSend
內部只是直接調用類的resolveInstanceMethod
、resolveClassMethod
方法查詢SEL
是否有動態解析,有則返回true
反之返回false
。
所謂動態方法解析實際上就是對不經常使用到的方法,在正式調用到該方法時纔將方法添加到類的方法列表。所以,resolveInstanceMethod
、resolveClassMethod
中的代碼通常是對須要動態解析的SEL
,調用class_addMethod(...)
動態添加該SEL
對應的方法,並返回YES
。若是隻是返回YES
的話,而不添加方法極可能形成的後果是responseToSelector
雖然能夠返回YES
,可是performSelector
時仍然拋出unrecognized selector
異常。
方法動態解析相關代碼以下。
// 動態解析方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// 動態解析實例方法
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// 動態解析類方法
_class_resolveClassMethod(cls, sel, inst);
// 這裏爲何要給元類再動態解析實例方法
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
// 動態解析類方法
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
// 若類未實現resolveClassMethod方法,則不走方法動態解析流程
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 觸發方法動態解析流程
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// lookUpImpOrNil觸發將動態解析的SEL與IMP的映射添加到類的方法緩存
// 這樣下次調用SEL時可直接從cache_t中查詢到IMP
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
}
}
// 動態解析實例方法
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 若類未實現resolveInstanceMethod方法,則不走方法動態解析流程
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 觸發方法動態解析流程
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// lookUpImpOrNil觸發將動態解析的SEL與IMP的映射添加到類的方法緩存
// 這樣下次調用SEL時可直接從cache_t中查詢到IMP
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
}
複製代碼
注意:完成動態解析後,又當即調用了
lookUpImpOrNil(...)
,其目的是將resolveInstanceMethod
、resolveClassMethod
方法實現代碼中爲響應SEL
而動態添加的IMP
加入到類的方法緩衝,則下一次調用可直接從方法緩衝中獲取。
關於方法動態解析和消息轉發流程的內部實現代碼,Runtime 公開的源代碼並很少。尤爲是消息轉發流程的實現,基本隱藏在objc_msgSend
函數的內部實現中。可是網上有不少關於 Runtime 消息轉發機制的博文,這裏只貼出方法動態解析及消息轉發流程的大體流程再也不贅述。圖中從左到右第一列是指,在類的繼承鏈上存在響應方法;第二列是進入方法動態解析流程(method resolution);第三列是進入消息快速轉發流程(fast forwarding);第四列是進入消息完整轉發流程(normal forwarding)。
class_ro_t
的baseMethodList
只保存了類的基本方法列表,只包含類定義時定義的方法,這些方法都是編譯時決議的; 類的class_rw_t
的methods
保存了類的完整方法列表,除了包含基本方法列表外,還包含運行時決議的,類的分類中定義的方法以及動態添加的方法;
class_rw_t
的methods
使用method_array_t
二維數組容器保存,包含一個或多個method_list_t
結構體,其中method_array_t
的外層一位數組容器的最後一個元素爲指向class_ro_t
的baseMethodList
的指針;
類的 class realizing 階段載入方法列表時,須要對全部方法列表根據SEL
進行排序,這是爲了在搜索方法時能根據SEL
進行二分法搜索提升方法搜索效率;
調用class_addMethod(...)
添加方法時,將方法封裝成method_list_t
,並添加到class_rw_t
的methods
的開頭,其餘元素總體後移;
類的方法緩衝緩存最近調用方法的SEL
和IMP
的映射,是可動態擴容的 Key-Value 形式的哈希表,可經過方法SEL
查找IMP
,引入方法緩衝可提升方法搜索的效率;
調用方法的本質是經過objc_msgSend
向 target 發送 selector 消息,target 響應消息時,前後在類的方法緩衝、類的方法列表、繼承鏈上全部類的方法緩衝和方法列表中搜索方法IMP
,若方法無響應,則觸發方法動態解析過程,執行resolveInstanceMethod
、resolveClassMethod
中的方法動態解析邏輯,若方法動態解析過程無響應,則進入消息轉發流程;
下一篇介紹屬性列表的實現。