runtime
runtime又叫運行時,是一套底層的c語言api,其為iOS內部核心之一。OC是動態運行時語言,它會將一些工作放在代碼運行時去處理,而非編譯時,比如動態的遍歷屬性和方法,動態的添加屬性和方法,動態的修改屬性和方法等。
了解runtime,首先要先了解它的核心--消息傳遞。
消息傳遞
消息直到運行時才會與方法實踐綁定起來。
一個實例對象調用實例方法,像這樣[obj doSomething];,編譯器轉成消息發送objc_msgSend(obj, @selector(doSomething),,);,
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
runtime時的運行流程如下:
- 首先通過調用對象的isa找到class;
- 在class的method_list里面找該方法,這里如果是實例對象,則去實例對象的類的方法列表中找,如果是類對象調用類方法,則去元類的方法列表中找,具體下面解釋;
- 如果class里沒找到,繼續往它的superClass里找;
- 一旦找到doSomething這個函數,就去執行它的實現IMP;
下面介紹一下對象(object),類(class),方法(method)的結構體:
//對象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//類
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
類對象(objc_class)
OC中類是Class來表示,實際上是一個指向objc_class結構體的指針。
//對象
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
//類
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
觀察一下對象的結構體和類對象的結構體,可以看到里面都有一個isa指針,對象的isa指針指向類,類的isa指針指向元類(metaClass),元類也是類,元類的isa指針最終指向根元類(rootMetaClass),根元類的isa指針指向自己,最終形成一個閉環。
可以看到類結構體中有一個methodLists,也就解釋了上文提到的成員方法記錄在class method-list中,類方法記錄在metaClass中。即Instance-object的信息記錄在class-object中,而class-object的信息記錄在meta-class中。
結構體中有一個ivars指針指向objc_ivar_list結構體,是該類的屬性列表,因為編譯器編譯順序是父類,子類,分類,所以這也就是為什么分類category不能添加屬性,因為類在編譯的時候已經注冊在runtime中了,屬性列表objc_ivar_list和instance_size內存大小都已經確定了,同時runtime會調用class_setIvarLayout和class_setWeakIvarLayout來處理strong和weak引用。可以通過runtime的關聯屬性來給分類添加屬性(原因是category結構體中有一個instanceProperties,下文會講到)。因為編譯順序是父類,子類,分類,所以消息遍歷的順序是分類,子類,父類,先進后出。
objc_cache結構體,是一個很有用的方法緩存,把經常調用的方法緩存下來,提高遍歷效率。將方法的method_name作為key,method_imp作為value保存下來。
Method(objc_method)
結構體如下:
//方法
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
可以看到里面有一個SEL和IMP,這里講一下兩者的區別。
SEL是selector的OC表示,數據結構為:typedef struct objc_selector *SEL;是個映射到方法的c字符串;不同于函數指針,函數指針直接保存了方法地址,SEL只是一個編號;也是objc_cache中的key。
ps.這也帶來了一個弊端,函數重載不適用,因為函數重載是方法名相同,參數名不同,但是SEL只記了方法名,沒有參數,所以沒法區分不同的method。
ps.在不同的類中,相同的方法名,方法選擇器也是相同的。
IMP是函數指針,數據結構為typedef id (IMP)(id,SEL,**);保存了方法地址,由編譯器綁定生成,最終方法執行哪段代碼由IMP決定。IMP指向了方法的實現,一組id和SEL可以確定唯一的實現。
有了SEL這個中間過程,我們可以對一個編號和方法實現做些中間操作,也就是說我們一個SEL可以指向不同的函數指針,這樣就可以完成一個方法名在不同的時候執行不同的函數體。另外可以將SEL作為參數傳遞給不同的類執行,也就是我們某些業務只知道方法名但需要根據不同的情況讓不同的類執行。個人理解,消息轉發就是利用了這個中間過程。
runtime是如何通過selector找到對應的IMP的?
上文講了類對象中有實例方法的列表,元類對象中有類方法的列表,列表中記錄著方法的名稱,參數和實現。而selector本質就是方法名稱也就是SEL,通過方法名稱可以在列表中找到方法實現。
在尋找IMP的時候,runtime提供了兩種方法:
- IMP class_getMethodImplementation(Class cls, SEL name);
- IMP method_getImplementation(Method m);
對于第一種方法來說,實例方法和類方法都是調用這個方法來找到IMP,不同的是第一個參數,實例方法傳的參數是[obj class];,而類方法傳的參數是objc_getMetaClass("obj");
對于第二種方法來說,傳入的參數只有Method,區分類方法和實例方法在于封裝Method的函數,類方法:Method class_getClassMethod(Class cls, SEL name);實例方法:Method class_getInstanceMethod(Class cls, SEL name);
Category(objc_category)
category是表示指向分類的一個結構體指針,結構體如下:
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
name:是指 class_name 而不是 category_name。
cls:要擴展的類對象,編譯期間是不會定義的,而是在Runtime階段通過name對應到對應的類對象。
instanceMethods:category中所有給類添加的實例方法的列表。
classMethods:category中所有添加的類方法的列表。
protocols:category實現的所有協議的列表。
instanceProperties:表示Category里所有的properties,這就是我們可以通過objc_setAssociatedObject和objc_getAssociatedObject增加實例變量的原因,不過這個和一般的實例變量是不一樣的。
從上面的結構體可以看出,分類category可以添加實例方法,類方法,協議,以及通過關聯對象添加屬性,不可以添加成員變量。
runtime消息轉發
前文講到,到一個方法被執行,也就是發送消息,會去相關的方法列表中尋找對應的方法實現IMP,如果一直到根類都沒找到就會進入到消息轉發階段,下面介紹一下消息轉發的最后三個集會。
- 動態方法解析
- 備用接收者
- 完整消息轉發
動態方法解析
首先,當消息傳遞到根類都找不到方法實現時,運行時runtime會調用+resolveInstanceMethod:或者+resolveClassMethod:,讓你有機會提供一個函數實現。如果你添加了函數,并返回了yes,那運行時就會重新走一步消息發送的過程。
實現一個動態方法解析的例子如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//執行foo函數
[self performSelector:@selector(foo:)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(foo:)) {//如果是執行foo函數,就動態解析,指定新的IMP
class_addMethod([self class], sel, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing foo");//新的foo函數
}
可以看到雖然沒有實現foo這個函數,但是我們通過class_addMethod動態的添加了一個新的函數實現fooMethod,并返回了yes。
如果返回no,就會進入下一步,- forwardingTargetForSelector:。
備用接收者
實現的例子如下:
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函數
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//執行foo函數
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,進入下一步轉發
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(foo)) {
return [Person new];//返回Person對象,讓Person對象接收這個消息
}
return [super forwardingTargetForSelector:aSelector];
}
@end
可以看到我們通過-forwardingTargetForSelector:方法將當前viewController的foo函數轉發給了Person的foo函數去執行了。
如果在這一步還不能處理未知的消息,則進入下一步完整消息轉發。
完整消息轉發
首先會發送-methodSignatureForSelector:消息獲得函數的參數和返回值類型。如果-methodSignatureForSelector:返回nil,runtime會發出-doseNotRecognizeSelector消息,程序會掛掉;如果返回一個函數標簽,runtime就會創建一個NSInvocation對象,并發送-forwardInvocation:消息給目標對象。
實現例子如下:
#import "ViewController.h"
#import "objc/runtime.h"
@interface Person: NSObject
@end
@implementation Person
- (void)foo {
NSLog(@"Doing foo");//Person的foo函數
}
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//執行foo函數
[self performSelector:@selector(foo)];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;//返回NO,進入下一步轉發
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;//返回nil,進入下一步轉發
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"foo"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];//簽名,進入forwardInvocation
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Person *p = [Person new];
if([p respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p];
}
else {
[self doesNotRecognizeSelector:sel];
}
}
@end
通過簽名,runtime生成了一個anInvocation對象,發送給了forwardInvocation:,我們再forwardInvocation:里面讓Person對象去執行了foo函數。
以上就是runtime的三次函數轉發流程。