iOS Runtime詳解

一、什么是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指針指向根元類根元類中的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_namemethod_imp分別是方法名稱方法實現,那么method_types是什么呢?
method_types類型編碼,為了和運行時系統協作,編譯器將方法的返回類型和參數類型都編碼成一個字符串,并且和方法選標關聯 在一起。method_types的類型編碼對照表如下:

類型編碼對照表

四、消息傳遞

Objective-C中方法的調用通常是這樣的[obj run],編譯器在編譯時都會轉化為objc_msgSend(obj, run)進行消息發送;
如果obj為實例對象則消息傳遞流程:
1.找到對象所在類:通過objisa指針找到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。
所以,可以根據實際開發中的需求來確定使用哪種方式。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370