Runtime-iOS運行時基礎篇

轉自:https://www.jianshu.com/p/d4b55dae9a0d  php

本文主要整理了Runtime的相關知識。對於一個iOS開發者來講,掌握Runtime的重要性早已不言而喻。OC可以做爲一門優秀的動態特性語言,在其背後默默工做着的就是Runtime。在網上也看過不少資料,最終我仍是但願在一些關鍵的知識點上可以融入本身的理解,從簡單的問題出發,一步一步理解和學以至用。html

 
iOS運行時Runtime.png

相關文章:iOS運行時Runtime應用java

目錄:

1、怎麼理解OC是動態語言,Runtime又是什麼?
2、理解消息機制的基本原理
3、與Runtime交互的三種方式
4、分析Runtime中的數據結構
5、深刻理解Rutime消息發送原理
6、多繼承的實現思路:Runtime
7、最後總結數據結構

1、怎麼理解OC是動態語言,Runtime又是什麼?

靜態語言:如C語言,編譯階段就要決定調用哪一個函數,若是函數未實現就會編譯報錯。app

動態語言:如OC語言,編譯階段並不能決定真正調用哪一個函數,只要函數聲明過即便沒有實現也不會報錯。ide

咱們常說OC是一門動態語言,就是由於它老是把一些決定性的工做從編譯階段推遲到運行時階段。OC代碼的運行不只須要編譯器,還須要運行時系統(Runtime Sytem)來執行編譯後的代碼。函數

Runtime是一套底層純C語言API,OC代碼最終都會被編譯器轉化爲運行時代碼,經過消息機制決定函數調用方式,這也是OC做爲動態語言使用的基礎。性能

2、理解消息機制的基本原理

OC的方法調用都是相似[receiver selector]的形式,其實每次都是一個運行時消息發送過程。學習

第一步:編譯階段
[receiver selector]方法被編譯器轉化,分爲兩種狀況:
1.不帶參數的方法被編譯爲:objc_msgSend(receiver,selector)
2.帶參數的方法被編譯爲:objc_msgSend(recevier,selector,org1,org2,…)測試

第二步:運行時階段
消息接收者recever尋找對應的selector,也分爲兩種狀況:
1.接收者能找到對應的selector,直接執行接收receiver對象的selector方法。
2.接收者找不到對應的selector,消息被轉發或者臨時向接收者添加這個selector對應的實現內容,不然崩潰。

說明:OC調用方法[receiver selector],編譯階段肯定了要向哪一個接收者發送message消息,可是接收者如何響應決定於運行時的判斷。

3、與Runtime的交互

Runtime的官方文檔中將OC與Runtime的交互劃分三種層次:OC源代碼NSObject方法Runtime 函數。這其實也是按照與Runtime交互程度從低到高排序的三種方式。

1.OC源代碼(Objec-C Source Code)

咱們已經說過,OC代碼會在編譯階段被編譯器轉化。OC中的類、方法和協議等在Runtime中都由一些數據結構來定義。因此,咱們平時直接使用OC編寫代碼,其實這已是在和Runtime進行交互了,只不過這個過程對於咱們來講是無感的。

2.NSObject方法(NSObject Methods)

Runtime的最大特徵就是實現了OC語言的動態特性。做爲大部分Objective-C類繼承體系的根類的NSObject,其自己就具備了一些很是具備運行時動態特性的方法,好比respondsToSelector:方法能夠檢查在代碼運行階段當前對象是否能響應指定的消息,因此使用這些方法也算是一種與Runtme的交互方式,相似的方法還有以下:

-description://返回當前類的描述信息 -class //方法返回對象的類; -isKindOfClass: 和 -isMemberOfClass: //檢查對象是否存在於指定的類的繼承體系中 -respondsToSelector: //檢查對象可否響應指定的消息; -conformsToProtocol: //檢查對象是否實現了指定協議類的方法; -methodForSelector: //返回指定方法實現的地址。 

3.使用Runtime函數(Runtime Functions)

Runtime系統是一個由一系列函數和數據結構組成,具備公共接口的動態共享庫。頭文件存放於/usr/include/objc目錄下。在咱們工程代碼裏引用Runtime的頭文件,一樣可以實現相似OC代碼的效果,一些代碼示例以下:

//至關於:Class class = [UIView class]; Class viewClass = objc_getClass("UIView"); //至關於:UIView *view = [UIView alloc]; UIView *view = ((id (*)(id, SEL))(void *)objc_msgSend)((id)viewClass, sel_registerName("alloc")); //至關於:UIView *view = [view init]; ((id (*)(id, SEL))(void *)objc_msgSend)((id)view, sel_registerName("init")); 

3、分析Runtime中數據結構

OC代碼被編譯器轉化爲C語言,而後再經過運行時執行,最終實現了動態調用。這其中的OC類、對象和方法等都對應了C中的結構體,並且咱們均可以在Rutime源碼中找到它們的定義。

那麼,咱們如何來查看Runtime的代碼呢?其實很簡單,只須要咱們在當前代碼文件中引用頭文件:

#import <objc/runtime.h> #import <objc/message.h> 

而後,咱們須要使用組合鍵"Command +鼠標點擊",便可進入Runtime的源碼文件,下面咱們繼續來一一分析OC代碼在C中對應的結構。

1.id—>objc_object

id是一個指向objc_object結構體的指針,即在Runtime中:

///A pointer to an instance of a class. typedef struct objc_object *id; 

下面是Runtime中對objc_object結構體的具體定義:

///Represents an instance of a class. struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; }; 

咱們都知道id在OC中是表示一個任意類型的類實例,從這裏也能夠看出,OC中的對象雖然沒有明顯的使用指針,可是在OC代碼被編譯轉化爲C以後,每一個OC對象其實都是擁有一個isa的指針的。

2.Class - >objc_classs

class是一個指向objc_class結構體的指針,即在Runtime中:

typedef struct objc_class *Class; 

下面是Runtime中對objc_class結構體的具體定義:

//usr/include/objc/runtime.h struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !OBJC2 Class Nullable super_class OBJC2UNAVAILABLE; const char * Nonnull name OBJC2UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * Nullable ivars OBJC2UNAVAILABLE; struct objc_method_list * Nullable * _Nullable methodLists OBJC2UNAVAILABLE; struct objc_cache * Nonnull cache OBJC2UNAVAILABLE; struct objc_protocol_list * Nullable protocols OBJC2UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; 

理解objc_class定義中的參數:

isa指針:

咱們會發現objc_class和objc_object一樣是結構體,並且都擁有一個isa指針。咱們很容易理解objc_object的isa指針指向對象的定義,那麼objc_class的指針是怎麼回事呢?
其實,在Runtime中Objc類自己同時也是一個對象。Runtime把類對象所屬類型就叫作元類,用於描述類對象自己所具備的特徵,最多見的類方法就被定義於此,因此objc_class中的isa指針指向的是元類,每一個類僅有一個類對象,而每一個類對象僅有一個與之相關的元類。

super_class指針:

super_class指針指向objc_class類所繼承的父類,可是若是當前類已是最頂層的類(如NSProxy),則super_class指針爲NULL

cache:

爲了優化性能,objc_class中的cache結構體用於記錄每次使用類或者實例對象調用的方法。這樣每次響應消息的時候,Runtime系統會優先在cache中尋找響應方法,相比直接在類的方法列表中遍歷查找,效率更高。

ivars:

ivars用於存放全部的成員變量和屬性信息,屬性的存取方法都存放在methodLists中。

methodLists:

methodLists用於存放對象的全部成員方法。

3.SEL

SEL是一個指向objc_selector結構體的指針,即在Runtime中:

/// An opaque type that represents a method selector. typedef struct objc_selector *SEL; 

SEL在OC中稱做方法選擇器,用於表示運行時方法的名字,然而咱們並不能在Runtime中找到它的結構體的詳細定義。Objective-C在編譯時,會依據每個方法的名字、參數序列,生成一個惟一的整型標識(Int類型的地址),這個標識就是SEL。

注意
1.不一樣類中相同名字的方法對應的方法選擇器是相同的。
2.即便是同一個類中,方法名相同而變量類型不一樣也會致使它們具備相同的方法選擇器。

一般咱們獲取SEL有三種方法:
1.OC中,使用@selector(「方法名字符串」)
2.OC中,使用NSSelectorFromString(「方法名字符串」)
3.Runtime方法,使用sel_registerName(「方法名字符串」)

4.Ivar

Ivar表明類中實例變量的類型,是一個指向ojbcet_ivar的結構體的指針,即在Runtime中:

/// An opaque type that represents an instance variable. typedef struct objc_ivar *Ivar; 

下面是Runtime中對objc_ivar結構體的具體定義:

struct objc_ivar { char * Nullable ivar_name OBJC2UNAVAILABLE; char * Nullable ivar_type OBJC2UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef LP64 int space OBJC2_UNAVAILABLE; #endif } 

咱們在objc_class中看到的ivars成員列表,其中的元素就是Ivar,我能夠經過實例查找其在類中的名字,這個過程被稱爲反射,下面的class_copyIvarList獲取的不只有實例變量還有屬性:

Ivar *ivarList = class_copyIvarList([self class], &count); for (int i= 0; i<count; i++) { Ivar ivar = ivarList[i]; const char *ivarName = ivar_getName(ivar); NSLog(@"Ivar(%d): %@", i, [NSString stringWithUTF8String:ivarName]); } free(ivarList); 

5.Method

Method表示某個方法的類型,即在Runtime中:

/// An opaque type that represents a method in a class definition. typedef struct objc_method *Method; 

咱們能夠在objct_class定義中看到methodLists,其中的元素就是Method,下面是Runtime中objc_method結構體的具體定義:

struct objc_method { SEL Nonnull method_name OBJC2UNAVAILABLE; char * Nullable method_types OBJC2UNAVAILABLE; IMP Nonnull method_imp OBJC2UNAVAILABLE; } OBJC2_UNAVAILABLE; 

理解objc_method定義中的參數:
method_name:方法名類型SEL
method_types: 一個char指針,指向存儲方法的參數類型和返回值類型
method_imp:本質上是一個指針,指向方法的實現
這裏其實就是SEL(method_name)與IMP(method_name)造成了一個映射,經過SEL,咱們能夠很方便的找到方法實現IMP。

5.IMP

IMP是一個函數指針,它在Runtime中的定義以下:

/// A pointer to the function of a method implementation. typedef void (IMP)(void / id, SEL, ... */ ); 

IMP這個函數指針指向了方法實現的首地址,當OC發起消息後,最終執行的代碼是由IMP指針決定的。利用這個特性,咱們能夠對代碼進行優化:當須要大量重複調用方法的時候,咱們能夠繞開消息綁定而直接利用IMP指針調起方法,這樣的執行將會更加高效,相關的代碼示例以下:

void (*setter)(id, SEL, BOOL); int i; setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)]; for ( i = 0 ; i < 1000 ; i++ ) setter(targetList[i], @selector(setFilled:), YES); 

注意:這裏須要注意的就是函數指針的前兩個參數必須是id和SEL。

4、深刻理解Rutime消息發送

咱們在分析了OC語言對應的底層C結構以後,如今能夠進一步理解運行時的消息發送機制。先前講到,OC調用方法被編譯轉化爲以下的形式:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...) 

其實,除了常見的objc_msgSend,消息發送的方法還有objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret等,若是消息傳遞給超類就使用帶有super的方法,若是返回值是結構體而不是簡單值就使用帶有stret的值。

運行時階段的消息發送的詳細步驟以下

  1. 檢測selector 是否是須要忽略的。好比 Mac OS X 開發,有了垃圾回收就不理會retain,release 這些函數了。
  2. 檢測target 是否是nil 對象。ObjC 的特性是容許對一個 nil對象執行任何一個方法不會 Crash,由於會被忽略掉。
  3. 若是上面兩個都過了,那就開始查找這個類的 IMP,先從 cache 裏面找,若能夠找獲得就跳到對應的函數去執行。
  4. 若是在cache裏找不到就找一下方法列表methodLists。
  5. 若是methodLists找不到,就到超類的方法列表裏尋找,一直找,直到找到NSObject類爲止。
  6. 若是還找不到,Runtime就提供了以下三種方法來處理:動態方法解析消息接受者重定向消息重定向,這三種方法的調用關係以下圖:
     
    消息轉發流程圖.png

1.動態方法解析(Dynamic Method Resolution)

所謂動態解析,咱們能夠理解爲經過cache和方法列表沒有找到方法時,Runtime爲咱們提供一次動態添加方法實現的機會,主要用到的方法以下:

//OC方法: //類方法未找到時調起,可於此添加類方法實現 + (BOOL)resolveClassMethod:(SEL)sel //實例方法未找到時調起,可於此添加實例方法實現 + (BOOL)resolveInstanceMethod:(SEL)sel //Runtime方法: /** 運行時方法:向指定類中添加特定方法實現的操做 @param cls 被添加方法的類 @param name selector方法名 @param imp 指向實現方法的函數指針 @param types imp函數實現的返回值與參數類型 @return 添加方法是否成功 */ BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

下面使用一個示例來講明動態解析:Perosn類中聲明方法卻未添加實現,咱們經過Runtime動態方法解析的操做爲其餘添加方法實現,具體代碼以下:

//Person.h文件 @interface Person : NSObject //聲明類方法,但未實現 + (void)haveMeal:(NSString *)food; //聲明實例方法,但未實現 - (void)singSong:(NSString *)name; @end 
//Person.m文件 #import "Person.h" #import <objc/runtime.h> @implementation Person //重寫父類方法:處理類方法 + (BOOL)resolveClassMethod:(SEL)sel{ if(sel == @selector(haveMeal:)){ class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(zs_haveMeal:)), "v@"); return YES; //添加函數實現,返回YES } return [class_getSuperclass(self) resolveClassMethod:sel]; } //重寫父類方法:處理實例方法 + (BOOL)resolveInstanceMethod:(SEL)sel{ if(sel == @selector(singSong:)){ class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(zs_singSong:)), "v@"); return YES; } return [super resolveInstanceMethod:sel]; } + (void)zs_haveMeal:(NSString *)food{ NSLog(@"%s",__func__); } - (void)zs_singSong:(NSString *)name{ NSLog(@"%s",__func__); } 
//TestViewController.m文件 //測試:Peson調用並未實現的類方法、實例方法,並無崩潰 Person *ps = [[Person alloc] init]; [Person haveMeal:@"Apple"]; //打印:+[Person zs_haveMeal:] [ps singSong:@"紙短情長"]; //打印:-[Person zs_singSong:] 

注意1:咱們注意到class_addMethod方法中的特殊參數「v@」,具體可參考這裏
注意2:成功使用動態方法解析還有個前提,那就是咱們必須存在能夠處理消息的方法,好比上述代碼中的zs_haveMeal:與zs_singSong:

2.消息接收者重定向

咱們注意到動態方法解析過程當中的兩個resolve方法都返回了布爾值,當它們返回YES時方法便可正常執行,可是若它們返回NO,消息發送機制就進入了消息轉發(Forwarding)的階段了,咱們可使用Runtime經過下面的方法替換消息接收者的爲其餘對象,從而保證程序的繼續執行。

//重定向類方法的消息接收者,返回一個類 - (id)forwardingTargetForSelector:(SEL)aSelector //重定向實例方法的消息接受者,返回一個實例對象 - (id)forwardingTargetForSelector:(SEL)aSelector 

下面使用一個示例來講明消息接收者的重定向:
咱們建立一個Student類,聲明並實現takeExam:、learnKnowledge:兩個方法,而後在視圖控制器TestViewController(一個繼承了UIViewController的自定義類)裏測試,關鍵代碼以下:

//Student.h文件 @interface Student : NSObject //類方法:參加考試 + (void)takeExam:(NSString *)exam; //實例方法:學習知識 - (void)learnKnowledge:(NSString *)course; @end 
// Student.m文件 @implementation Student + (void)takeExam:(NSString *)exam{ NSLog(@"%s",__func__); } - (void)learnKnowledge:(NSString *)course{ NSLog(@"%s",__func__); } @end 
//TestViewConroller.m文件 //重定向類方法:返回一個類對象 + (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(takeExam:)) { return [Student class]; } return [super forwardingTargetForSelector:aSelector]; } //重定向實例方法:返回類的實例 - (id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(learnKnowledge:)) { return self.student; } return [super forwardingTargetForSelector:aSelector]; } //在TestViewConroller的viewDidLoad中測試: //調用並未聲明和實現的類方法 [TestViewController performSelector:@selector(takeExam:) withObject:@"語文"]; //調用並未聲明和實現的類方法 self.student = [[Student alloc] init]; [self performSelector:@selector(learnKnowledge:) withObject:@"天文學知識"]; //正常打印: // +[Student takeExam:] // -[Student learnKnowledge:] 

注意:動態方法解析階段返回NO時,咱們能夠經過forwardingTargetForSelector能夠修改消息的接收者,該方法返回參數是一個對象,若是這個對象是非nil,非self,系統會將運行的消息轉發給這個對象執行。不然,繼續查找其餘流程。

3.消息重定向

當以上兩種方法沒法生效,那麼這個對象會由於找不到相應的方法實現而沒法響應消息,此時Runtime系統會經過forwardInvocation:消息通知該對象,給予這次消息發送最後一次尋找IMP的機會:

- (void)forwardInvocation:(NSInvocation *)anInvocation; 

其實每一個對象都從NSObject類中繼承了forwardInvocation:方法,可是NSObject中的這個方法只是簡單的調用了doesNotRecongnizeSelector:方法,提示咱們錯誤。因此咱們能夠重寫這個方法:對不能處理的消息作一些默認處理,也能夠將消息轉發給其餘對象來處理,而不拋出錯誤。

咱們注意到anInvocation是forwardInvocation惟一參數,它封裝了原始的消息和消息參數。正是由於它,咱們還不得不重寫另外一個函數:methodSignatureForSelector。這是由於在forwardInvocation: 消息發送前,Runtime系統會向對象發送methodSignatureForSelector消息,並取到返回的方法簽名用於生成NSInvocation對象。

下面使用一個示例來從新定義轉發邏輯:在上面的TestViewController添加以下代碼:

-(void)forwardInvocation:(NSInvocation *)anInvocation{ //1.從anInvocation中獲取消息 SEL sel = anInvocation.selector; //2.判斷Student方法是否能夠響應應sel if ([self.student respondsToSelector:sel]) { //2.1若能夠響應,則將消息轉發給其餘對象處理 [anInvocation invokeWithTarget:self.student]; }else{ //2.2若仍然沒法響應,則報錯:找不到響應方法 [self doesNotRecognizeSelector:sel]; } } //須要從這個方法中獲取的信息來建立NSInvocation對象,所以咱們必須重寫這個方法,爲給定的selector提供一個合適的方法簽名。 - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{ NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector]; if (!methodSignature) { methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"]; } return methodSignature; } 

而後再在視圖控制器裏直接調用Student的方法以下:

//self是當前的TestViewController,調用了本身並不存在的learnKonwledge:方法 [self performSelector:@selector(learnKnowledge:) withObject:@"天文學」]; //正常打印: //-[Student learnKnowledge:] 

總結:

1.從以上的代碼中就能夠看出,forwardingTargetForSelector僅支持一個對象的返回,也就是說消息只能被轉發給一個對象,而forwardInvocation能夠將消息同時轉發給任意多個對象,這就是二者的最大區別。

2.雖然理論上能夠重載doesNotRecognizeSelector函數實現保證不拋出異常(不調用super實現),可是蘋果文檔着重提出「必定不能讓這個函數就這麼結束掉,必須拋出異常」。(If you override this method, you must call super or raise an invalidArgumentException exception at the end of your implementation. In other words, this method must not return normally; it must always result in an exception being thrown.)

3.forwardInvocation甚至可以修改消息的內容,用於實現更增強大的功能。

6、多繼承的實現思路:Runtime

咱們會發現Runtime消息轉發的一個特色:一個對象能夠調起它自己不具有的方法。這個過程與OC中的繼承特性很類似,其實官方文檔中圖示也很好的說明了這個問題:

 
forwarding.png

圖中的Warrior經過forwardInvocation:將negotiate消息轉發給了Diplomat,這就好像是Warrior使用了超類Diplomat的方法同樣。因此從這個思路,咱們能夠在實際開發需求中模擬多繼承的操做。

7、最後總結:

以上就是iOS運行時的基礎知識部分了,理解Runtime的工做原理,下一篇iOS運行時Runtime應用,將總結其在實際開發中的使用。

做者:梧雨北辰 連接:https://www.jianshu.com/p/d4b55dae9a0d 來源:簡書 簡書著做權歸做者全部,任何形式的轉載都請聯繫做者得到受權並註明出處。
相關文章
相關標籤/搜索