目錄
- 前言
- iOS編譯流程
- runtime介紹
-
-
- 為何要有runtime
- 何為runtime
- runtime原理
-
-
- 1. id --> objc_object
- 2. Class --> objc_class
- 3. Meta Class 元類
- 4. Ivar 成員變量 和 objc_property_t 屬性
- 5. Method / SEL / IMP 方法
- 6. Category
-
-
- 消息發送流程
- 消息轉發流程
-
-
- 1. 動态方法解析
- 2. 接收者重定向
- 3. 消息重定向
-
- Method Swizzling
- 參考文檔
前言
關于runtime的文章, 網上實在太多了, 内容層次深淺不一. 誠然, 要想把runtime讨論明白, 講得深入徹底, 沒有相當功力是不行的. 故, 本文退而求其次, 希望能把runtime講得"知其然", 想必也是挺好的.
iOS編譯流程
我們編寫的所有代碼, 最終都是要轉換成二進制機器指令去執行的. 如圖
C/C++/OC編譯流程:
Swift編譯流程:
注:
例圖是以模拟器為例, 是以編譯結果為 x86-64 CPU 機器語言; 如果是真機, 則最終編譯成 ARM CPU 機器語言
解析
- iOS 開發中 Objective-C 是 Clang / LLVM 來編譯的。
- Swift 是 Swift / LLVM,其中 Swift 前端會多出 SIL optimizer,它會把 .swift 生成的中間代碼 .sil 屬于 High-Level IR, 因為 swift 在編譯時就完成了方法綁定直接通過位址調用屬于強類型語言,方法調用不再是像OC那樣的消息發送,這樣編譯就可以獲得更多的資訊用在後面的後端優化上。
- 不管編譯的語言時 Objective-C 還是 Swift 也不管對應機器是什麼,亦或是即時編譯,LLVM 裡唯一不變的是中間語言 LLVM IR。
引申
進階程式設計語言想要成為可執行檔案需要先編譯為彙編語言再彙編為機器語言,但是OC并不能直接編譯為彙編語言,而是要先轉寫為純C語言再進行編譯和彙編的操作,從OC到C語言的過渡就是由runtime來實作的。
關于iOS中的彙編, 詳見深入iOS系統底層之彙編語言.
結論
在整個iOS編譯過程中, runtime處在LLVM的前端部分(Frontend). 更具體點, 可以說是runtime将OC轉換成C.
runtime介紹
為何要有runtime
-
: 靜态語言. 編譯階段就要決定調用哪個函數, 如果函數未實作就會編譯報錯.C
-
: 動态語言(得益于runtime機制). 運作時才決定調用哪個函數, 隻要函數聲明過即使沒有實作也不會報錯.OC
-
: 靜态語言. 其對象方法的調用基本上是在編譯連結時刻就被确定的. 詳見Swift5.0的Runtime機制淺析.Swift
Swift基本上取消了runtime機制, 故本文還是主要讨論OC下的runtime. 當然, 通過Swift與OC混編, 我們也可以在Swift檔案中調用OC的runtime接口.
總所周知, OC 擴充自 C 語言,然後擁有了面向對象性質和消息傳遞機制, 成為了動态語言。而這個擴充的核心就是我們今天的主角—— runtime。
何為runtime
runtime 其實是一個系統動态共享庫, 具有一個公共接口, 該公共接口由頭檔案中的一組函數和資料結構組成 (純C語言API). 由于所有的OC代碼終将轉換成C代碼, 使得 runtime 的API調用非常頻繁, 是以新版runtime裡面對應的實作基本上都是用C++和彙編語言混合來寫的, 以便提高系統效率.
runtime原理
這一部分的讨論, 将圍繞
類
、
執行個體
、
屬性
、
方法
以及
類别
等在runtime中的表現形式來展開.
1. id --> objc_object
id是一個指向類執行個體的指針, 它在runtime中的定義如下:
而objc_object在objc-private.h中定義如下:
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
... 内容太多故省略
}
objc_object
結構體包含一個
isa
指針, 根據
isa
就可以順藤摸瓜找到對象所屬的類. 而
isa
的類型
isa_t
使用
union
實作, 可能表示多種形态, 既可以當成是指針, 也可以存儲标志位. 這是蘋果提出的Tagged Pointer類型對象的概念, 目的是為了減少記憶體資源的浪費. 畢竟用 64 bit 存儲一個記憶體位址顯然是種浪費.
Tagged Pointer類型的對象采用一個跟機器字長一樣長度的整數來表示一個OC對象,而為了跟普通OC對象區分開來,每個Tagged Pointer類型對象的最高位為1而普通的OC對象的最高位為0.
小結: OC中的對象終将轉換成C中的結構體objc_object.
2. Class --> objc_class
我們在Xcode中輸入基類NSObject, 然後 ⌘+單擊 這個NSObject, 檢視它的定義:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
其中省略了#pragma clang部分, 有興趣可查閱這篇博文#pragma
可以看到, NSObject有且僅有一個Class類型的isa屬性, 也就是說, 一個類對象唯一儲存的資訊就是它的 Class 的位址. 由于OC中幾乎所有的類(NSProxy等除外)都直接或間接地繼承于NSObject類, 可以說, OC中的類都有一個isa屬性. 那麼這個isa又是什麼呢?
我們繼續 ⌘+單擊 Class, 可以看到他在runtime中的定義:
此時我們發現, Class在runtime中是一個指向objc_class結構體的指針.
isa, 意思是is a, 這是一個…
繼續檢視objc_class結構體, 我們看到它在Xcode的runtime.h裡定義如下:
/**
* objc1.0
*/
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
注意後面這個OBJC2_UNAVAILABLE
其實上面這個是相容
版本的定義, 而目前我們使用的是
objc1.0
版本,
objc2.0
中沒有暴露出
2.0
所定義的詳細内容.
bjc_class
你可以在https://opensource.apple.com/source/objc4/objc4-723/中下載下傳和檢視開源的最新版本的Runtime庫源代碼。Runtime庫的源代碼是用彙編和C++混合實作的,你可以在頭檔案objc-runtime-new.h中看到關于
struct objc_class
結構的詳細定義。
/**
* objc2.0
*/
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();
}
... 内容太多故省略
}
objc_class結構體中太多字段了是以這裡省略掉. 其内容主要有:包括類的名字、所繼承的基類、類中定義的方法清單描述、屬性清單描述、實作的協定描述、定義的成員變量描述等等資訊。如圖:
objc_class
繼承于
objc_object
, 也就是說一個OC類本身同時也是一個對象. 既然說類也是對象, 那麼類的類型是什麼呢?這裡就引出了另外一個概念 —— Meta Class (元類).
小結: OC中的類終将轉換成C中的結構體objc_class.
3. Meta Class 元類
為了處理類和對象的關系, runtime 庫建立了一種叫做元類 (Meta Class) 的東西.
其實觀察
objc_class
和
objc_object
的定義, 會發現兩者本質相同(都包含
isa
指針), 隻是
objc_class
多了一些額外的字段. 這些字段包括了建立一個類執行個體所需的資訊, 以及這些執行個體的方法等. 那麼類的資訊和類方法儲存在哪呢? 答案是在元類裡.
我們來看下這張著名的圖:
小結:
- 執行個體的isa指針指向類, 類的isa指針指向元類.
- 類所對應的objc_class裡儲存了執行個體的方法, 元類所對應的objc_class裡儲存了類方法.
- 元類的isa指針指向自己, 形成閉環.
4. Ivar 成員變量 和 objc_property_t 屬性
Ivar
Ivar: instance variable
Ivar 代表類執行個體的變量或屬性(帶下劃線"_"), 其在runtime中定義如下:
ivar_t最終嵌套在objc_class裡, 在objc-rentime-new.h中的結構體層級關系如下:
ivar_t -> ivar_list_t -> class_ro_t -> class_rw_t -> class_data_bits_t -> objc_class
我們可以周遊一個類的成員變量和屬性(加"_"):
// 列印成員變量清單
- (void)logIvarList {
unsigned int count;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (int i=0; i<count; i++) {
Ivar myIvar = ivarList[i];
const char *ivarName = ivar_getName(myIvar);
NSLog(@"成員變量%d: %@", i, [NSString stringWithUTF8String:ivarName]);
}
free(ivarList);
}
objc_property_t
@property 标記了類中的屬性, 它是一個指向objc_property 結構體的指針:
周遊屬性:
// 列印屬性清單
- (void)logPropertyList {
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([self class], &count);
for (unsigned int i=0; i<count; i++) {
const char *propertyName = property_getName(propertyList[i]);
NSLog(@"屬性%d: %@", i, [NSString stringWithUTF8String:propertyName]);
}
free(propertyList);
}
5. Method / SEL / IMP 方法
Method
Method在runtime中定義如下:
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
}
SEL: selector
IMP: implementation
Method中存儲了這三樣東西:
- SEL類型的方法名.
- char指針的方法類型, 指向存儲方法的參數類型和傳回值類型.
- IMP類型的方法實作位址.
SEL
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.
SEL是一個方法選擇器, 在runtime中其實是一個C字元串, 用來表示方法名稱.
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel);
// 列印: viewDidLoad
SEL是由編譯器在裝載類的時候自動生成的, 是以我們不能強制将一個C字元串轉化為SEL. 我們可以使用OC編譯器指令
@selector()
或者runtime系統的
sel_registerName
函數來獲得一個 SEL 類型的方法選擇器. 例如:
SEL sel1 = @selector(viewWillAppear:);
SEL sel2 = sel_registerName("init");
不同類中相同名字的方法所對應的方法選擇器是相同的, 即使方法名字相同而變量類型不同也會導緻它們具有相同的方法選擇器. 比如, 同一個類中, 相同方法名不同參數類型也是會報錯的:
// 同一個類中, 相同方法名不同參數類型, 報錯
- (void)caculate(NSInteger)num;
- (void)caculate(CGFloat)num;
IMP
IMP在runtime中的定義如下:
/// A pointer to the function of a method implementation.
typedef void (IMP)(void / id, SEL, ... */ );
IMP是一個函數指針, 指向了方法實作的首位址.
這裡要注意, IMP 指向的函數的前兩個參數是預設參數, id 和 SEL 。這裡的 SEL 好了解,就是函數名。而 id ,對于執行個體方法來說, self 儲存了目前對象的位址;對于類方法來說, self 儲存了目前對應類對象的位址。後面的省略号即是參數清單。
小結:
Method
/
SEL
/
IMP
這三個概念之間關系: 在運作時, 類(Class)維護了一個消息分發清單來解決消息的正确發送. 每一個消息清單的入口是一個方法(Method), 這個方法映射了一對鍵值對, 其中鍵值是這個方法的名字 selector (SEL), 值是指向這個方法實作的函數指針 implementation (IMP).
6. Category
Category 為現有的類提供了拓展性, 它是 objc_category 結構體的指針.
typedef struct objc_category *Category;
struct objc_category {
char * _Nonnull category_name OBJC2_UNAVAILABLE;
char * _Nonnull class_name OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
}
雖然在OC2中不是這樣定義, 但是都有這些内容, 為簡潔起見, 姑且這樣分析讨論吧.
其中包含對象方法清單、類方法清單、協定清單等. 從這裡我們也可以看出, Category 支援添加對象方法、類方法、協定, 但不能儲存成員變量.
注意:在 Category 中是可以添加屬性的,但不會生成對應的成員變量、 getter 和 setter 。是以,調用 Category 中聲明的屬性時會報錯。
關聯對象
我們可以通過關聯對象的方式來添加可用的屬性:
- (void)setXxx:(NSString *)xxx {
objc_setAssociatedObject(self, &xxx, xxx, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)xxx {
return objc_getAssociatedObject(self, &xxx);
}
消息發送流程
當我們在OC中執行一個方法:
編譯器會編譯成運作時的C代碼:
如果消息含有參數, 則為:
其實有四個消息發送方法:,
objc_msgSend
,
objc_msgSend_stret
和
objc_msgSendSuper
。如果消息是傳遞給超類,那麼會調用名字帶有”Super”的函數;如果消息傳回值是資料結構而不是簡單值時,那麼會調用名字帶有”stret”的函數。“stret”可分為“st”+“ret”兩部分,分别代表“struct”和“return”。
objc_msgSendSuper_stret
對于[receiver message], 在編譯階段确定了要向接收者 receiver 發送 message 這條消息,而 receive 将要如何響應這條消息, 那就要看運作時發生的情況來決定了.
以下是objc_msgSend消息發送流程:
- 檢測這個selector是不是要被忽略的。 比如 Mac OS X 開發,有了垃圾回收就不理會 retain, release 這些函數了。
- 檢測這個target對象是不是nil對象。(nil對象執行任何一個方法都不會Crash,因為會被忽略掉)
- 首先會根據target(objc_object)對象的isa指針擷取它所對應的類(objc_class)。
-
檢視緩存cache中是否存在方法。
如果有,則找到objc_method中的IMP類型(函數指針)的成員method_imp去找到實作内容,并執行;
如果沒有,那麼到該類的方法表(methodLists)查找該方法,依次從後往前查找。
- 如果沒有在類(class)找到,再到父類(super_class)查找,直至根類。
- 一旦找到與選擇子(selector)名稱相符的方法,就跳至其實作代碼。
- 如果沒有找到,就會執行消息轉發(message forwarding)的第一步動态解析。
消息轉發流程
先來看看這張圖:
向不處理該消息的對象發送消息是錯誤的. 但是, 在宣布錯誤之前, 運作時系統會給接收對象第二次處理消息的機會. 這個機會分三步走:
- 動态方法解析
- 接收者重定向
- 消息重定向
1. 動态方法解析
我們可以通過分别重載
+resolveInstanceMethod:
和
+resolveClassMethod:
方法, 分别添加執行個體方法實作和類方法實作. 然後傳回YES, 運作時系統就會重新啟動一次消息發送的過程.
🌰
// Person.h
@interface Person : NSObject
+ (void)eat;
- (void)work;
@end
// Person.m
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel{
if(sel == @selector(eat)){
class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(kk_eat)), "[email protected]");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
// 添加執行個體方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if(sel == @selector(work)){
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(kk_work)), "[email protected]");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 新的類方法
+ (void)kk_eat {
NSLog(@"%s", __func__);
}
// 新的執行個體方法
- (void)kk_work {
NSLog(@"%s", __func__);
}
@end
// 類方法
[Person eat];
// 對象方法
Person *person = [Person new];
[person work];
// --------------------------------------------------------------
列印:
+[Person kk_eat]
-[Person kk_work]
注: 添加的方法必須實作, 否則報錯! 比如上述代碼中的與
kk_eat
必須實作.
kk_work
關于
class_addMethod
的最後一個參數types
我們使用C函數來說明會比較好了解, 比如:
void eat(id self, SEL _cmd, NSString *str)
{
NSLog(@"%@", str);
}
那麼types參數為"v @ : @“, 按順序分别表示:
-
: 傳回值類型void, 若是i則表示intv
-
: 參數id(self)@
-
: SEL(_cmd):
-
: id(str)@
更多類型詳見Type Encodings
2. 接收者重定向
如果動态方法解析部分中,
+resolveInstanceMethod:
和
+resolveClassMethod:
都傳回了NO, 則會分别調用重定向類方法
+forwardingTargetForSelector:
和重定向執行個體方法
-forwardingTargetForSelector:
. 在這兩個方法中, 我們可以指定新的消息接收者, 但要注意的是新的接受者必須實作了該消息.
🌰 🌰
建立一個類Alien.
// Alien.h
@interface Alien : NSObject
+ (void)eat;
- (void)work;
@end
// Alien.m
#import "Alien.h"
@implementation Alien
+ (void)eat {
NSLog(@"%s", __func__);
}
- (void)work {
NSLog(@"%s", __func__);
}
@end
在原來的Person類的
+resolveInstanceMethod:
和
+resolveClassMethod:
方法裡傳回NO. 然後重寫重定向類方法
+forwardingTargetForSelector:
和重定向執行個體方法
-forwardingTargetForSelector:
.
#import "Person.h"
#import <objc/runtime.h>
#import "Alien.h"
@implementation Person
// 添加類方法
+ (BOOL)resolveClassMethod:(SEL)sel{
if(sel == @selector(eat)){
return NO;
// class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(kk_eat)), "[email protected]:");
// return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
// 添加執行個體方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if(sel == @selector(work)){
return NO;
// class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(kk_work)), "[email protected]:");
// return YES;
}
return [super resolveInstanceMethod:sel];
}
// 重定向類方法:傳回一個類
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(eat)) {
return [Alien class];
}
return [super forwardingTargetForSelector:aSelector];
}
// 重定向執行個體方法:傳回一個執行個體
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(work)) {
Alien *alien = [Alien new];
return alien;
}
return [super forwardingTargetForSelector:aSelector];
}
// 新的類方法
+ (void)kk_eat {
NSLog(@"%s", __func__);
}
// 新的執行個體方法
- (void)kk_work {
NSLog(@"%s", __func__);
}
列印的不是Person的方法, 而是Alien的方法:
log:
+[Alien eat]
-[Alien work]
3. 消息重定向
如果
對于類方法,
+resolveClassMethod:
傳回NO,
+forwardingTargetForSelector:
傳回nil;
對于執行個體方法,
+resolveInstanceMethod:
傳回NO,
-forwardingTargetForSelector:
傳回nil.
那麼
進入第三步也是最後一步 —— 消息重定向.
消息重定向又分為兩個小步驟:
- runtime系統會向對象發送
消息, 并取到傳回的方法簽名用于生成NSInvocation對象;-methodSignatureForSelector
- 将生成的NSInvocation對象作為參數調用
-forwardInvocation:
🌰 🌰 🌰
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ViewController沒有work方法, 将會走轉發
[self performSelector:@selector(work)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES; // 傳回YES,進入下一步轉發
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil; // 傳回nil,進入下一步轉發
}
// 傳回一個NSInvocation對象
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if (!methodSignature) {
// 生成方法簽名
methodSignature = [NSMethodSignature signatureWithObjCTypes:"[email protected]:"];
}
return methodSignature;
}
// 消息轉發
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
// // 類方法
// if ([[Alien class] respondsToSelector:sel]) {
// [anInvocation invokeWithTarget:[Alien class]];
// }else{
// // 若無法響應, 則報錯: 找不到響應方法
// [self doesNotRecognizeSelector:sel];
// }
// 執行個體方法
Alien *alien = [Alien new];
if ([alien respondsToSelector:sel]) {
[anInvocation invokeWithTarget:alien];
}else{
// 若無法響應, 則報錯: 找不到響應方法
[self doesNotRecognizeSelector:sel];
}
}
log:
Method Swizzling
消息轉發雖然功能強大,但需要我們了解并且能更改對應類的源代碼,因為我們需要實作自己的轉發邏輯。當我們無法觸碰到某個類的源代碼,卻想更改這個類某個方法的實作時,該怎麼辦呢?可能繼承類并重寫方法是一種想法,但是有時無法達到目的。這裡介紹的是 Method Swizzling ,它通過重新映射方法對應的實作來達到“偷天換日”的目的。跟消息轉發相比,Method Swizzling 的做法更為隐蔽,甚至有些冒險,也增大了debug的難度。
這裡摘抄一個 NSHipster 的例子:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];
// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);
BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
參考文檔
Objective-C Runtime
深入解構objc_msgSend函數的實作
Runtime-iOS運作時基礎篇
iOS Runtime詳解
runtime開源