一、什么是Runtime?
我們都知道,從源代碼到可執行文件需要經歷三個階段:編譯
、鏈接
、運行
。
Objective-C
是一門動態語言,會盡可能的將決定性的工作從編譯時和鏈接時推遲到運行時
,也就是說只有編譯器是不夠的,還需要一個運行時系統 (runtime system) 來執行編譯后的代碼。這就是 Objective-C Runtime 系統存在的意義,它是整個Objc運行框架的一塊基石。
Runtime
簡稱運行時。OC就是運行時機制,其中最主要的是消息機制。對于C語言,函數的調用在編譯的時候會決定調用哪個函數。對于OC的函數,屬于動態調用過程,在編譯的時候并不能決定真正調用哪個函數(事實證明,在編譯階段,OC可以調用任何函數,即使這個函數并未實現,只要申明過就不會報錯。而C語言在編譯階段就會報錯),只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
二、Runtime源碼
蘋果和GNU各自維護一個開源的Runtime
版本,這兩個版本之間都在努力的保持一致。
1.蘋果公司Runtime開源代碼
2.GNU Runtime開源代碼
三、Runtime底層解析
我們首先來看下runtime
中對象(object)
、類(class)
、方法(method)
等都是這么定義的
1. 對象(object)
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
// 對象
struct objc_object {
// 對象的isa指針指向類對象
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
從上面源碼中可以看到這里的 id
被定義為一個指向 objc_object
結構體 的指針。從中可以看出 objc_object 結構體
只包含一個 Class
類型的 isa
指針,而Class
是一個指向objc_class
結構體的指針。
由此可以得出對象的本質
是一個objc_object的結構體
,類的本質
是一個objc_class的結構體
2. 類(class)
// 類對象
struct objc_class {
// 類對象的isa指針指向元類對象
// 元類對象的isa指針指向的是根元類
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
// 指向父類的指針
Class _Nullable super_class OBJC2_UNAVAILABLE;
// 類的名稱
const char * _Nonnull name OBJC2_UNAVAILABLE;
// 類的版本信息,默認為 0
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;
從上面源碼可以看出objc_class
結構體定義了很多變量,其中包含了自身的所有實例變量(ivars)
、所有方法定義(methodLists)
、遵守的協議列表(protocols)
等。objc_class 結構體
存放的數據稱為元數據(metadata)
。
objc_class的第一個成員變量是isa指針
,此isa
指針指向的是本身的元類(meta class)
。
3. 元類(meta class)
那么什么是元類
呢?
元類
是編譯器在創建類的同時創建的一個虛擬的類,用來存儲類對象的類方法等信息的類。
類和元類的關系就和實例對象和類的關系一樣:類就是實例對象所屬的類,元類就是類對象所屬的類
。
元類
也是一個指向objc_class結構體
的指針,元類
的isa
指針指向的是根元類
。
4. 實例對象、類、元類的關系
下面用一張圖來總結下這三者之間的關系
實例對象
中有個isa
指針,這個isa
指針指向實例對象
所在的類,類對象
中也有個isa
指針,這個isa
指針指向類對象
所在的元類,元類對象還有個isa
指針,這個isa
指針指向根元類
,根元類
中的isa
指針指向的是本身
。
5. 方法(method)
/// An opaque type that represents a method in a class definition.
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;
}
其中method_name
和method_imp
分別是方法名稱
和方法實現
,那么method_types
是什么呢?
method_types
是類型編碼
,為了和運行時系統協作,編譯器將方法的返回類型和參數類型都編碼成一個字符串,并且和方法選標關聯 在一起。method_types
的類型編碼對照表如下:
四、消息傳遞
Objective-C
中方法的調用通常是這樣的[obj run]
,編譯器在編譯時都會轉化為objc_msgSend(obj, run)
進行消息發送;
如果obj
為實例對象則消息傳遞流程:
1.找到對象所在類
:通過obj
的isa
指針找到Class
類。
2.從緩存中查找
:從Class
類中的方法緩沖區cache
中查找方法(被調用過的方法都會存在方法緩沖區cache
中,以便下次更快的調用),如果沒有找到則進入下一步
3.從方法列表中查找
:如果cache
中沒有,則從methodLists
中查找。如果沒找到則進入下一步。
4.通過繼承鏈查找
:通過Class
的繼承鏈找到父類直到根類NSObject
,每次重復2,3步
,如果還找不到則進入下一步。
5.動態方法解析
:調用 + (BOOL)resolveInstanceMethod:(SEL)sel
方法來查看是否能夠返回一個selector
,如果存在則返回selector
。不存在進入下一步。
6.備用接收者
:- (id)forwardingTargetForSelector:(SEL)aSelector
這個方法來詢問是否有接收者
可以接收這個方法。如果有接收者
,則交給它處理,否則進入下一步。
7.消息的轉發
:如果到這一步還不能夠找到相應的selector
的話,就要進行完整的方法轉發過程。調用方法(void)forwardInvocation:(NSInvocation *)anInvocation
,如果這里還沒有處理則會進入下一步。
8.奔潰
:最后還是沒有找到的話就只有呵呵了,這時候unrecognized selector sent to instance 0x100111df0
的錯誤就來了。
動態方法解析
在上面方法傳遞過程中如果一直沒找到方法會進入動態消息解析
過程,在此過程中可以動態的添加方法實現。如果你添加了方法實現, 那運行時系統就會重新啟動一次消息發送的過程。
動態方法解析主要在+ (BOOL)resolveInstanceMethod:(SEL)sel
和+ (BOOL)resolveClassMethod:(SEL)sel
這兩個方法中進行,通過例子我們來了解一下
@interface ViewController ()
// 聲明run方法
- (void)run;
+ (void)walk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 調用run方法,但run方法并未被實現
[self run];
[ViewController walk];
}
// 對象方法未找到時調起此方法,可以再次方法中添加方法實現
+ (BOOL)resolveInstanceMethod:(SEL)sel{
// 如果沒有實現run方法
if (sel == @selector(run)) {
/**
* 可以在此添加一個方法實現
* @param cls 被添加方法的類
* @param name selector 方法名
* @param imp 實現方法的函數指針
* @param types imp 指向函數的返回值與參數類型
* @return 如果添加方法成功返回 YES,否則返回 NO
*/
return class_addMethod(self, sel, (IMP)runImp, "v@:");
}else if (sel == @selector(walk)) {
return class_addMethod(self, sel, (IMP)walkImp, "v@:");
}
return [super resolveInstanceMethod:sel];
}
// 類方法未找到時調起此方法,可以再次方法中添加方法實現
+ (BOOL)resolveClassMethod:(SEL)sel{
// 如果沒有實現run方法
if (sel == @selector(walk)) {
/**
* 可以在此添加一個方法實現
* @param cls 被添加方法的類的元類。??這是元類
* @param name selector 方法名
* @param imp 實現方法的函數指針
* @param types imp 指向函數的返回值與參數類型
* @return 如果添加方法成功返回 YES,否則返回 NO
*/
return class_addMethod(objc_getMetaClass(object_getClassName(self)), sel, (IMP)walkImp, "v@:");;
}
return [super resolveClassMethod:sel];
}
// 方法實現
void runImp(id obj, SEL sel){
NSLog(@"實例方法實現 %s",__func__);
}
// 方法實現
void walkImp(id obj, SEL sel){
NSLog(@"類方法實現 %s",__func__);
}
@end
這是打印的信息
2020-09-02 15:55:13.694867+0800 RuntimeDemo[5899:162375] 實例方法實現 runImp
2020-09-02 15:55:13.695411+0800 RuntimeDemo[5899:162375] 類方法實現 walkImp
備用接收者
如果在動態消息轉發過程
中沒有添加方法的實現,那么此時Runtime
就會調用- (id)forwardingTargetForSelector:(SEL)aSelector
這個方法來返回一個備用接收者
,然后由這個備用接收者
來實現這個方法。下面通過一個例子我們來了解一下
@interface Person : NSObject
@end
@implementation Person
- (void)run{
NSLog(@"%s",__func__);
}
+ (void)walk{
NSLog(@"%s",__func__);
}
@end
@interface ViewController ()
// 聲明run方法
- (void)run;
+ (void)walk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 調用run方法,但run方法并未被實現
[self run];
[ViewController walk];
}
// 返回一個備用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"instance method : %@", NSStringFromSelector(aSelector));
if (aSelector == @selector(run)) {
return [[Person alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
+ (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"class method : %@", NSStringFromSelector(aSelector));
if (aSelector == @selector(walk)) {
return [Person class];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
下面是此次運行打印的結果
2020-09-02 18:07:40.491838+0800 RuntimeDemo[6821:230239] instance method : run
2020-09-02 18:07:40.492687+0800 RuntimeDemo[6821:230239] -[Person run]
2020-09-02 18:07:40.493125+0800 RuntimeDemo[6821:230239] class method : walk
2020-09-02 18:07:40.493510+0800 RuntimeDemo[6821:230239] +[Person walk]
可以看到雖然ViewController
沒有實現這兩個方法,動態方法解析
也沒有添加這個兩個方法實現,但是我們通過 forwardingTargetForSelector
把當前 ViewController
的方法轉發給了 Person
對象去執行了。打印結果也證明我們成功實現了轉發。
我們通過forwardingTargetForSelector
可以修改消息的接收者,該方法返回參數是一個對象,如果這個對象是不是 nil,也不是 self,系統會將運行的消息轉發給這個對象執行。否則,繼續進行下一步:消息轉發(重定向)流程
。
消息轉發(重定向)
如果經過前面兩步Runtime
系統還是找不到相應的方法實現而無法響應消息,那么就會進入消息轉發流程:
首先它會發送-methodSignatureForSelector:
消息獲得函數的參數和返回值類型。如果 methodSignatureForSelector:
返回了一個 NSMethodSignature
對象(函數簽名),Runtime
系統就會創建一個 NSInvocation
對象,并通過 forwardInvocation:
消息通知當前對象,給予此次消息發送最后一次尋找 IMP
的機會。如果 methodSignatureForSelector:
返回 nil。則 Runtime
系統會發出doesNotRecognizeSelector:
消息,程序也就崩潰了。
下面我們通過一個例子來了解一下
@interface Person : NSObject
@end
@implementation Person
- (void)run{
NSLog(@"%s",__func__);
}
+ (void)walk{
NSLog(@"%s",__func__);
}
- (void)run:(NSString *)type{
NSLog(@"%s %@",__func__, type);
}
@end
@interface ViewController ()
// 聲明run方法
- (void)run;
- (void)run:(NSString *)type;
+ (void)walk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 調用run方法,但run方法并未被實現
[self run];
[self run:@"slowly"];
[ViewController walk];
}
// 獲取方法函數的參數和返回值類型,返回簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(run)) {
//簽名,進入forwardInvocation
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}else if (aSelector == @selector(run:)) {
//簽名,進入forwardInvocation
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
// 消息轉發(重定向)
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
NSLog(@"- forwardInvocation %@", NSStringFromSelector(sel));
Person *p = [[Person alloc] init];
// 第一種方式 調用時候傳的是什么參數就是什么參數
if ([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}else {
// 若仍然無法響應,則報錯:找不到響應方法
[self doesNotRecognizeSelector:sel];
}
// // 第二種方式 可以自定義傳參
// NSMethodSignature *signature = [p methodSignatureForSelector:sel];
// NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
// invocation.target = p;
// invocation.selector = sel;
// if (sel == @selector(run:)) {
// NSString *runType = @"fast";
// //注意:設置參數的索引時不能從0開始,因為0已經被self占用,1已經被_cmd占用
// [invocation setArgument:&runType atIndex:2];
// }
// [invocation invoke];
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(walk)) {
//簽名,進入forwardInvocation
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL sel = anInvocation.selector;
NSLog(@"+ forwardInvocation %@", NSStringFromSelector(sel));
if ([Person respondsToSelector:sel]) {
[anInvocation invokeWithTarget:objc_getClass(object_getClassName([Person class]))];
}else {
// 若仍然無法響應,則報錯:找不到響應方法
[self doesNotRecognizeSelector:sel];
}
}
消息轉發的實現有兩種方式
,第一種
調用時候傳的是什么參數轉發的就是什么參數,第二種
可以自定義參數值,你想要什么參數就傳什么參數。讓我們來看下兩種方式的打印結果
第一種方式
2020-09-06 11:36:44.051691+0800 RuntimeDemo[1377:38366] - forwardInvocation run
2020-09-06 11:36:44.052208+0800 RuntimeDemo[1377:38366] -[Person run]
2020-09-06 11:36:44.052624+0800 RuntimeDemo[1377:38366] - forwardInvocation run:
2020-09-06 11:36:44.052965+0800 RuntimeDemo[1377:38366] -[Person run:] slowly
2020-09-06 11:36:44.053331+0800 RuntimeDemo[1377:38366] + forwardInvocation walk
2020-09-06 11:36:44.053691+0800 RuntimeDemo[1377:38366] +[Person walk]
可以看到第四行這里打印的是slowly。
第二種方式
2020-09-06 11:43:33.036825+0800 RuntimeDemo[1404:40952] - forwardInvocation run
2020-09-06 11:43:33.037358+0800 RuntimeDemo[1404:40952] -[Person run]
2020-09-06 11:43:33.037811+0800 RuntimeDemo[1404:40952] - forwardInvocation run:
2020-09-06 11:43:33.038203+0800 RuntimeDemo[1404:40952] -[Person run:] fast
2020-09-06 11:43:33.039525+0800 RuntimeDemo[1404:40952] + forwardInvocation walk
2020-09-06 11:43:33.040117+0800 RuntimeDemo[1404:40952] +[Person walk]
可以看到第四行這里打印的是fast。
所以,可以根據實際開發中的需求來確定使用哪種方式。