runtime 小結
OC被稱之為動態執行時語言,最主要的原因就是因為兩個特性,一個是執行時也就是runtime,一個是多型。
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 isaOBJC_ISA_AVAILABILITY; }; //類 struct objc_class { Class isaOBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_classOBJC2_UNAVAILABLE; const char *nameOBJC2_UNAVAILABLE; long versionOBJC2_UNAVAILABLE; long infoOBJC2_UNAVAILABLE; long instance_sizeOBJC2_UNAVAILABLE; struct objc_ivar_list *ivarsOBJC2_UNAVAILABLE; struct objc_method_list **methodListsOBJC2_UNAVAILABLE; struct objc_cache *cacheOBJC2_UNAVAILABLE; struct objc_protocol_list *protocolsOBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; //方法列表 struct objc_method_list { struct objc_method_list *obsoleteOBJC2_UNAVAILABLE; int method_countOBJC2_UNAVAILABLE; #ifdef __LP64__ int spaceOBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1]OBJC2_UNAVAILABLE; }OBJC2_UNAVAILABLE; //方法 struct objc_method { SEL method_nameOBJC2_UNAVAILABLE; char *method_typesOBJC2_UNAVAILABLE; IMP method_impOBJC2_UNAVAILABLE; }
類物件(objc_class)
OC中類是Class來表示,實際上是一個指向objc_class結構體的指標。
//物件 struct objc_object { Class isaOBJC_ISA_AVAILABILITY; }; //類 struct objc_class { Class isaOBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_classOBJC2_UNAVAILABLE; const char *nameOBJC2_UNAVAILABLE; long versionOBJC2_UNAVAILABLE; long infoOBJC2_UNAVAILABLE; long instance_sizeOBJC2_UNAVAILABLE; struct objc_ivar_list *ivarsOBJC2_UNAVAILABLE; struct objc_method_list **methodListsOBJC2_UNAVAILABLE; struct objc_cache *cacheOBJC2_UNAVAILABLE; struct objc_protocol_list *protocolsOBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; //方法列表 struct objc_method_list { struct objc_method_list *obsoleteOBJC2_UNAVAILABLE; int method_countOBJC2_UNAVAILABLE; #ifdef __LP64__ int spaceOBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1]OBJC2_UNAVAILABLE; }
觀察一下物件的結構體和類物件的結構體,可以看到裡面都有一個isa指標,物件的isa指標指向類,類的isa指標指向元類(metaClass),元類也是類,元類的isa指標最終指向根元類(rootMetaClass),根元類的isa指標指向自己,最終形成一個閉環。

1552095614854.jpg
可以看到類結構體中有一個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_nameOBJC2_UNAVAILABLE; char *method_typesOBJC2_UNAVAILABLE; IMP method_impOBJC2_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的三次函式轉發流程。
Better Late Than Never!
努力是為了當機會來臨時不會錯失機會。
共勉!