iOS開發Runtime的理解與應用

weixin_34162695發表於2018-04-09

一、Runtime概念

  • RunTime簡稱執行時,其中最主要的是訊息機制。

  • 對於C語言,函式的呼叫在編譯的時候會決定呼叫哪個函式,編譯完成之後直接順序執行,無任何二義性。

  • OC的函式呼叫成為訊息傳送,屬於動態呼叫過程。在編譯的時候並不能決定真正呼叫哪個函式(事實證明,在編譯階段,OC可以呼叫任何函式,即使這個函式並未實現,只要宣告過就不會報錯。而C語言在編譯階段就會報錯)。

  • 只有在真正執行的時候才會根據函式的名稱找到對應的函式來呼叫。

    其動態性體現在三個方面:

1.動態型別:
即執行時再決定物件的型別。簡單說就是id型別,任何物件都可以被id指標所指,只有在執行時才能決定是什麼型別。像內建的明確的基本型別都屬於靜態型別(int、NSString等)。靜態型別在編譯的時候就能被識別出來。所以,若程式發生了型別不對應,編譯器就會發出警告。而動態型別就編譯器編譯的時候是不能被識別的,要等到執行時(run time),即程式執行的時候才會根據語境來識別。所以這裡面就有兩個概念要分清:編譯時跟執行時。
2.動態繫結:
基於動態型別,在某個例項物件被確定後,其型別便被確定了。該物件對應的屬性和響應的訊息也被完全確定,這就是動態繫結。比如我們一般向一個NSObject物件傳送-respondsToSelector:或者 -instancesRespondToSelector:等來確定物件是否可以對某個SEL做出響應,而在OC訊息轉發機制被觸發之前,對應的類 的+resolveClassMethod:和+resolveInstanceMethod:將會被呼叫,在此時有機會動態地向類或者例項新增新的方 法,也即類的實現是可以動態繫結的;isKindOfClass也是一樣的道理。
3.動態載入:
所謂動態載入就是我們做開發的時候icon圖片的時候在Retina裝置上要多新增一個張@2x的圖片,當裝置更換的時候,圖片也會自動的替換。

二、Runtime資料結構

1.Class

Objective-C類是由Class型別來表示,Class 其實是指向 objc_class 結構體的指標

typedef struct objc_class *Class;

我們可以從<objc/runtime.h>裡面看到類的定義

struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父類
     const char *name                         OBJC2_UNAVAILABLE;  // 類名
     long version                             OBJC2_UNAVAILABLE;  // 類的版本資訊,預設為0
     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;
2.id

id 是一個結構體指標型別,它可以指向 Objective-C 中的任何物件。從<objc/objc.h>中可以看出id其實是指向objc_object結構體的指標。objc_object只有一個成員變數 isa,物件可以通過 isa 指標找到其所屬的類,這也是為什麼id可以表示任意物件的原因。isa 是一個 Class 型別的成員變數。
注意:在KVO中,isa在執行時會被修改,指向一箇中間類,對於編譯器而言,isa的指向才是最真實的型別。

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

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

當我們向一個Objective-C物件傳送訊息時,執行時庫會根據
例項物件的isa指標找到這個例項物件所屬的類。Runtime庫會在類的方法列表由super_class指標找到父類的方法列表直至根類NSObject中去尋找與訊息對應的selector指向的方法,找到後即執行這個方法。

3.元類(meta-Class)

類自身也是一種物件,可以叫做“類物件”。類物件包含一個指向其類的一個isa指標( Class _Nonnull isa )

為了呼叫類方法,這個類的isa指標必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念,meta-class中儲存著一個類的所有類方法。
所以,呼叫類方法的這個類物件的isa指標指向的就是meta-class。當我們向一個物件傳送訊息時,runtime會在這個物件所屬的這個類的方法列表中查詢方法;而向一個類傳送訊息時,會在這個類的meta-class的方法列表中查詢。

下圖是一個經典的類及相應meta-class類的一個繼承體系圖解:

2638830-80d346ffe2ad7f3c.png
image.png

從圖中可以看出:
1.每個例項物件的類都是類物件,每個類物件的類都是元類物件,每個元類物件的類都是根元類(root meta class的isa指向自身)。即任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指標是指向它自己。
2.類物件的父類最終繼承自根類物件NSObject,NSObject的父類為nil
3.元類物件(包括根元類)的父類最終繼承自根類物件NSObject

三、方法和訊息

1.SEL
typedef struct objc_selector *SEL;
SEL sel1 = @selector(func1);
SEL sel2 = NSSelectorFromString(func2);

SEL本質上是一個指向方法的指標。Objective-C在編譯時,會依據每一個方法的名字、引數序列,生成一個唯一的整型標識即SEL,每一個方法都對應著一個SEL。所以即使返回值型別或引數型別不同,方法名相同也會報錯。

2.IMP
id (*IMP)(id, SEL,...)

IMP的本質是函式指標,指向方法實現的地址,直接通過IMP就可以找到各個方法。這樣效率更高,因為繞過了訊息傳遞階段,直接定位。
SEL就是為了查詢方法的最終實現IMP的。由於每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的IMP。

訊息傳送和轉發流程可以概括為:訊息傳送(Messaging)是 Runtime 通過 selector 快速查詢 IMP 的過程,有了函式指標就可以執行對應的方法實現;訊息轉發(Message Forwarding)是在查詢 IMP 失敗後執行一系列轉發流程的慢速通道,如果不作轉發處理,則會打日誌和丟擲異常。

3.Method
typedef struct objc_method *Method
struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法實現
}

可以看出method包含SEL和IMP,在實現方法交換時,主要原理就是交換SEL和IMP的對映關係。

參考:https://www.jianshu.com/p/46dd81402f63
https://www.jianshu.com/p/adf0d566c887
https://www.jianshu.com/p/13457a27624c

相關文章