目錄
前言
本文會詳細描述Objective-C運行時的各對象數底層據結構、類和原類、消息傳遞與轉發、動態方法等技術方案. 文中底層代碼實現均來自Apple open source ; 本文篇幅較長, 文中描述加之有個人的一點理解, 主要用作記錄和學習之用, 文筆粗陋, 技術菜雞, 如有錯誤或不妥之處, 萬望各位大佬不吝指教, 不勝感激!
runtime是什么
Objective-C runtime是一個動態運行庫, 它給Objective-C語言的動態性提供了支撐. 所有的應用都會鏈接到該運行時庫.
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.
三個重要概念
在下述講述過程中, 你應該注意三個非常重要的概念.即Class、SEL、IMP, 在這里我先把他們列出來, 后面我們會一一的深入講到其內部結構和之間的關系.
typedef struct objc_class *Class;
typedef struct objc_object {
Class isa;
} *id;
typedef struct objc_selector *SEL;
typedef id (*IMP)(id, SEL, ...);
一 各主要對象數據結構
1 objc_object
objc_object表示實例對象底層是結構體, 內部有一個私有的isa指針, 該指針指向了其類對象
struct objc_object {
private:
isa_t isa;
...
// isa相關操作
// 弱引用, 關聯對象, 內存管理等等相關的操作
// 都是在此結構體中, 篇幅太長, 不再全部貼出
}
2 objc_class
objc_class繼承自objc_object(所以肯定有isa指針), 表示類對象, 底層仍然是結構體, 其內部的isa指針, 指向了該類的元類對象. 同時, 內部的superclass指向了自身的父類對象, NSObject對象superclass指向了nil, cache是一個方法緩存結構體, bits是存儲變量、屬性、方法等的結構體
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() {
return bits.data();
}
...
// 類相關的數據操作都是在此結構體中, 不再全部貼出
}
2.1 cache_t
緩存方法, 消息傳遞時, 會先通過哈希查找算法, 在此數據結構中查詢是否有要執行的方法緩存, 如果有則快速執行該方法函數, 這樣提高了消息傳遞的效率;
方法緩存策略, 是局部性原理 的最佳應用;
本質是一個可增量的哈希表 , 其內部維護了一個由bucket_t組成的結構體列表
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
public:
struct bucket_t *buckets();
...
};
bucket_t內部存儲了方法緩存key和無類型函數指針地址的映射關系, 在查找緩存時, 通知指定的key查找到具體的bucket_t, 再從bucket_t中查詢到函數IMP地址, 進而去執行函數
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
public:
inline cache_key_t key() const { return _key; }
inline IMP imp() const { return (IMP)_imp; }
inline void setKey(cache_key_t newKey) { _key = newKey; }
inline void setImp(IMP newImp) { _imp = newImp; }
void set(cache_key_t newKey, IMP newImp);
};
2.2 class_data_bits_t
class_data_bits_t結構主要是對class_rw_r的封裝
class_rw_r又是對class_ro_r的封裝
struct class_rw_t {
// class_rw_t部分代碼
uint32_t flags;
uint32_t version;
// 指向只讀的結構體, 存儲類初始內容
const class_ro_t *ro;
/*
三個可讀寫二維數組, 存儲了類的初始化信息, 內容
*/
method_array_t methods; // 方法列表
property_array_t properties; // 屬性列表
protocol_array_t protocols; // 協議列表
// 第一個子類
Class firstSubclass;
// 下一個同級類
Class nextSiblingClass;
};
class_ro_t結構
struct class_ro_t {
// class_ro_t部分代碼
const char * name;
// class_ro_t存儲的是類在編譯期就確定的方法, 屬性, 協議等
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
需要注意的是, class_ro_t存儲的是類在編譯期就確定的內容信息, 而class_rw_t不僅包含了類在編譯期的內容信息(其實是把class_ro_t的內容合并), 還包含了在運行時動態添加的類內容, 如分類添加的方法, 屬性, 協議等內容; 一張圖來表示上述結構之間的關系:
3 isa
在arm64為架構之前, isa指針存儲了類或元類對象的地址信息, 從arm64架構開始對isa指針(非指針型指針)進行了優化, 用位域存儲了除類或元類地址信息以外的其他信息, 如has_assoc表示是否設置關聯對象
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
// 標記位, 0 代表指針型isa, 1代表非指針型isa
uintptr_t indexed : 1;
// 是否有關聯對象
uintptr_t has_assoc : 1;
// 是否有C 析構函數
uintptr_t has_cxx_dtor : 1;
// 存儲當前對象的類或元類的內存地址
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
// 判斷對象是否已經初始化
uintptr_t magic : 6;
// 對象是否有弱引用指針
uintptr_t weakly_referenced : 1;
// 當前對象是否有dealloc操作
uintptr_t deallocating : 1;
// 當前isa指針是否有外掛引用表
// 引用計數值大于isa所能存儲最大值時
// 就會綁定一個sidetable散列表屬性, 來存儲更多的引用計數信息
uintptr_t has_sidetable_rc : 1;
// 額外的引用計數值
uintptr_t extra_rc : 19;
};
}
這里需要注意
isa所屬對象是實例對象, 則其指向實例對象的類對象
isa所屬對象是類對象, 則其指向類對象的元類對象
4 method_t
method_t是函數的底層數據結構, 是對函數的封裝, Apple對函數的介紹在這里
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; }
};
};
4.1 函數的四要素
4.2 types
Apple使用了Type Encodings 技術, 來實現類型編碼, Objective-C 運行時庫內部利用類型編碼幫助加快消息分發.
結構是個列表, 包含了函數的返回值, 參數1, 參數2, 參數3 …, 其中函數的返回值存儲在第0個位置, 因為函數只有一個返回值(Go支持多返回值), 而參數可以有多個.
對于一個無類型無參數的函數, 其types值為 “V@:”
- (void)method {
// 其中
// V對應返回值, 代表返回值類型為void
// @對應第一個參數, id類型代表一個對象, 默認第一個參數是對象本身(self), 且該參數是固定的
// :對應SEL, 代表該參數是個方法選擇器, 且該參數是默認的第二個固定參數
}
5 一張圖表明各數據結構之間的關系
二 實例對象、類對象和元類對象
一大佬(膜拜)畫的一張圖, 足以說明三者之間的關系.( Apple官網 也有類似的描述,但是個人感覺沒有下面這張圖更精彩)
實例對象的isa指針指向其類對象
類對象的isa指針指向其元類對象
任何元類對象的isa指針都指向根元類對象
類對象的superclass指針指向其父類對象, 根類對象指向nil
元類對象的superclass指針指向其父元類對象, 根元類對象指向根類
其中, 根類在Objective-C中即為NSObject. 實例對象其實就是objc_object(), 類對象就是objc_class(); 上面講到, objc_class()是繼承自objc_object(), 因此類對象中也有isa指針
typedef struct objc_object {
Class isa;
} *id;
從底層數據結構可以看出, 類對象中存儲了實例對象方法列表, 成員變量等內容; 同時, 元類對象中存儲了類對象的類方法列表等內容;
1 實例方法調用時是如何查找的
當一個實例對象調用一個實例方法時
首先會根據該對象的isa指針, 查到到其類對象, 在類對象方法列表中查詢是否有所調用方法的實現;
如果沒有, 則類對象會根據自身的superclass指針查找其父類對象, 在父類對象方法列表中查詢是否有所調用方法的同名方法實現;
遞歸調用直至根類對象, 如果中間有任何一步查詢到了具體的方法實現, 就去執行具體的函數調用;
如果直至根類, 仍然沒有找到方法實現, 則會調用系統兩個方法, 然后走系統調用流程;
(BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
(BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
具體的消息傳遞流程請往下看
2 self和super
self是當前類的因此參數, 指向類的實例對象, 進行方法調用時, 代表從當前類開始進行查找
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
super本質是個編譯器標識符, 僅代表方法查找時從當前對象所屬父類開始查找方法實現
OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
三 消息傳遞與消息轉發
在開發中, 我們經常會碰見這樣子一個錯誤 unrecognized selector sent to instance xx
大致意思是, 你調用了一個并不存在的方法. 現在, 我們會深入的探究一下為什么會出現這個異常.
其實上面這個異常會正好就是我們要講的, 在Objective-C的消息機制中, 用OC消息機制來說: 如果消息在傳遞的過程中找不到具體的IMP, 內部就觸發了消息轉發機制, 而系統的消息轉發機制默認實現是拋出上述的異常. 接下來, 我們分別講述消息的傳遞和轉發.
我們知道Objective-C是動態語言, 方法的調用并不像C的靜態綁定一樣, 在編譯的時候就確定了程序運行時該調用哪個函數(C中沒有方法實現會報錯), 而是在運行時基于runtime這個動態運行時庫通過一系列的查找才決定調用哪個函數, 這樣的調用方式更加靈活, 我們甚至可以在運行時動態的修改某個方法的實現, 與當下流行的"熱更新"技術有些類似. 而這個查找過程就是Objective-C的消息機制.
1 消息傳遞流程
在Objective-C中, 方法調用其實就是給某個對象發送消息, 在編譯后的文件中我們發現, 底層都轉變為函數調用
// 返回值, 參數1: 固定self, 參數2: 固定SEL, 后面是參數3, 參數4....
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
從代碼中可以看到, 消息發送函數有兩個默認的參數, 第一個是消息接受者receiver, 默認就是當前對象self; 第二個默認參數是SEL, SEL的本質的方法選擇器selector; (閱讀運行時的文檔你會發現, 幾乎所有方法調用都和selector有關系)! 所以, 我們的方法調用可以這樣表示** [receiver selector] **, 那么這個selector究竟是何方神圣, 遺憾的是我在Apple和GNU 提供的runtime代碼中,都只找到了這一行代碼.
typedef struct objc_selector *SEL;
不過Apple 給了說明, 方法選擇器selector就是個映射到C中的字符串. 根據我翻閱的各種資料都顯示, selector就是個C字符串類型的方法名稱.
Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.
說了半天, 跟我們的運行時有什么關系(objc_msgSend()是[receiver selector]編譯階段實現的)?那么, objc_msgSend()函數在運行時是如何進一步調用的呢?
首先, 通過 recevier的isa指針尋找到recevier的class(類);
其次, 先在class中的cache list(緩存列表)查找是否有對應的緩存selector;
如果在緩存列表中查找到, 那么就根據selector(key)直接執行方法對應的IMP(value);
否則, 繼續在 class的method list(方法列表)中查找對應的 selector;
如果沒有找到對應的selector, 就繼續在它的 superclass(父類)中尋找;
最后, 如果找到對應的 selector, 直接執行 recever 對應 selector 方法實現的 IMP(方法實現)
否則, 系統進入默認消息轉發機制.
我們用一張圖來表示上述流程
有時候, 我們會通過super調用, 其實道理是一樣的, 編譯后會生成objc_msgSendSuper()函數
OBJC_EXPORT id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
__OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
而objc_super結構體內部, 消息接受者仍然是receiver當前實例對象, 與上面不唯一不同的是, self是從當前對象的類對象中開始查找對應實現, 而super則是跨過當前對象的類對象直接從類對象的父類對象開始查找方法實現;
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
};
2 消息轉發流程
在消息的傳遞過程中, 我們講到, 如果receiver 找不到對應的selector的IMP實現, 則會進入系統的默認消息轉發流程. 而系統默認處理消息轉發的機制就會拋出unrecognized selector sent to instance xx 異常, 然后結束整個消息轉發. 如果想要避免這種情況的發生, 我們就需要在如果selector找不到的情況下在運行時動態的給receiver添加實現.
幸運的是雖然系統默認默認流程是拋異常, 但是在拋異常的方法調用過程中, 系統給我們開了口子, 讓我們可以通過 動態解析、receiver重定向、消息重定向等對消息進行處理, 流程如下圖:
2.1 消息動態解析
在系統處理消息轉發的過程中, 首先會根據調用對象類型不同分別調用如下兩個api, 我們可以通過重載在這兩個方法內部動態添加方法, 進而避免crash
// 找不到類方法, 重載此類方法添加類方法實現
(BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// 找不到實例方法, 重載此類方法添加實例方法實現
(BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
我們以實例方法為例舉個??
// 此處實例方法test沒有方法實現
(BOOL)resolveInstanceMethod:(SEL)sel {
// 判斷是否是test方法
if (sel == @selector(test)) {
NSLog(@"resolveInstanceMethod:");
// 動態添加test方法的實現
class_addMethod(self, @selector(test), testImp, "v@:");
}
return [super resolveInstanceMethod:sel];
}
void testImp (void) {
NSLog(@"test invoke");
}
2.2 消息接受者重定向
如果在resolveInstanceMethod:SEL中沒有處理消息(即返回NO), 則系統會給我們第二次機會, 調用forwardingTargetForSelector:SEL! 方法返回值是個id類型, 告訴系統這個實例方法調用轉由哪個對象(如果是類方法調用則返回類對象; 如果是實例方法調用, 則返回實例對象)來接受處理, 如果我們指定了新的receiver, 就把消息重新交給新的receiver處理.
同樣的, 我們舉個??
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
NSLog(@"forwardingTargetForSelector:");
// 重定向, 讓ForwardObj對象作為receiver, 接收處理這個消息
return [[ForwardObj alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
2.3 消息重定向
如果系統給我們第二次機會時, 我們返回的對象是nil, 或者self, 那系統會最后一次給我們避免crash的機會, 即消息重定向流程, 調用methodSignatureForSelector方法, 返回值是個方法簽名
繼續舉個??
// 定義函數參數和返回值類型, 并返回函數簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
NSLog(@"methodSignatureForSelector:");
// v代表方法簽名的返回值void, @ id類型代表self
// : SEL類型, 代表方法選擇器, 其實就是@selector(test)
return [NSMethodSignature signatureWithObjCTypes:"@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息重定向
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation:");
ForwardObj *obj = [[ForwardObj alloc] init];
if ([obj performSelector:anInvocation.selector]) {
// 如果obj對象可以響應, 則消息轉發給obj對象處理
[anInvocation invokeWithTarget: obj];
} else {
// 否則, 拋異常找不到方法對應的實現
[self doesNotRecognizeSelector:anInvocation.selector];
}
}
四 動態方法
1 動態添加方法
我們在消息轉發的過程中已經用到了動態添加方法
// 動態添加底層實現
// cls: 為哪個動態添加方法
// name: 要添加的方法名稱(方法選擇器selector)
// IMP: 無類型函數指針地址
// types: Type Encodings 函數參數和返回值
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);
}
2 動態方法解析
同樣的, 在消息轉發過程中, 其實就是對方法的動態解析. 現在我們要講述另一個方法動態解析的類型, @dynamic
dynamic: 這個詞中文意思是動態. 什么動態? 動態運行時的動態, 動態方法的動態, 動態解析的動態, 動態語言的動態!
被@dynamic標記的屬性, 在編譯時并沒有對其getter和setter方法做實現, 而是
動態運行時, 把其實現推遲到了運行時, 即將函數決議推遲到運行時
而靜態語言, 是在編譯期就進行了函數決議
轉載請注明作者和鏈接哦!
參考資料:
GNU
NS類型編碼
運行時編程指南
Objective-C 運行時
Objective-C 編程
iOS 開發:『Runtime』詳解(一)基礎知識
來源:https://www./content-4-650651.html