Runtime知識點整理

阿多發表於2018-08-04

Runtime介紹

Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個執行時系統來動態得建立類和物件、進行訊息傳遞和轉發。 Runtime就是這個執行時系統。

Runtime 基本是用C和彙編寫的,OC並不能直接編譯為組合語言,而是要先轉寫為純C語言再進行編譯和彙編的操作,從OC到C語言的過渡就是由runtime來實現的。

Runtime知識點整理

你可以在 這裡下到蘋果維護的開原始碼。蘋果和GNU各自維護一個開源的 runtime 版本,這兩個版本之間都在努力的保持一致。

平時的業務中主要是使用官方Api,解決我們框架性的需求。

Runtime訊息傳遞

呼叫一個物件的方法: [obj foo]
編譯器轉成訊息傳送:objc_msgSend(obj, foo)

一個簡單的demo,在main.m檔案中

Person * person = [[Person alloc]init];
[person run];
複製程式碼

clang命令編譯 clang -rewrite-objc main.m

開啟編譯後的main.cpp檔案,一直拉到最後可以看見我們剛剛寫的兩行程式碼的編譯結果

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Person * person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("run"));
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

複製程式碼

可以清晰的看到[person run] 被編譯成了objc_msgSend(person,run),我們常說在OC中呼叫一個物件,就是像一個物件傳送一個方法指令。

要了解objc_msgSend訊息傳遞的原理,先來了解幾個概念:

1、例項(objc_object)

在objc.h中

typedef struct objc_object *id; // 指向 objc_object 結構體的指標
複製程式碼
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;  // isa指標 - 指向類物件:類物件中儲存了建立一個例項的資訊
};
複製程式碼

2、類物件(objc_class)

objc.h 中 calss 的定義

typedef struct objc_class *Class; // 類物件是一個指向 objc_class 結構體的指標
複製程式碼

runtime.h 中 objc_class 結構體的定義

struct objc_class {
    // isa指標 - 指向元類:元類儲存了建立類物件以及類方法的所有資訊
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    // 父類指標
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    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;
複製程式碼

3、元類(Meta Class)

所有的類自身也是一個物件,我們可以向這個物件傳送訊息(即呼叫類方法)。 為了呼叫類方法,這個類的isa指標必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念。

例項中的isa指標指向類物件,類中儲存了建立一個例項物件及例項方法所需的所有資訊,類物件的isa指標指向元類,元類中儲存了建立類物件以及類方法所需的所有資訊。

基類的meta-class的isa指標是指向它自己

Runtime知識點整理

通過上圖我們可以看出整個體系構成了一個自閉環

4、Method方法(objc_method)

runtime.h 檔案中

typedef struct objc_method *Method; // 指向 objc_method 結構體的指標
複製程式碼
struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE; // 方法名
    char * _Nullable method_types                            OBJC2_UNAVAILABLE; // 方法型別
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE; // 方法實習
}
複製程式碼

5、SEL方法(objc_selector)

objc.h 中

typedef struct objc_selector *SEL;
複製程式碼
@property SEL selector;
SEL 是 selector 的表示型別,
selector是方法選擇器,是區分方法的ID,這個ID的資料結構是 objc_selector 結構體
複製程式碼
  • 原始碼中沒有objc_selector結構體的具體定義
  • 其實就是個對映到方法的C字串,命名規則是 className+methodName
  • 導致不能像C語言一樣寫過載函式,就是函式名相同,引數不同。因為select只記了方法名沒有引數,所以沒有辦法區分不同引數的方法。

6、 IMP 指標 - 指向最終實現程式的記憶體地址

objc.h 中

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif
複製程式碼

Method通過 selector 和 IMP 兩個屬性,實現了快速查詢方法及實現

7、 類快取(objc_cache)

基於理論:如果你在類上呼叫一個訊息,你可能以後會再次呼叫該訊息。

為了加快訊息分發,系統會對方法和對應的地址進行快取,放在objc_cache中,大部分常用的方法都是會被快取起來的,Runtime系統實際上非常快,接近直接執行記憶體地址的程式速度。

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method _Nullable buckets[1]                              OBJC2_UNAVAILABLE;
};
複製程式碼

mask 可以理解為當前能達到的最大的index
occupied 被佔用的槽位
buckets 用陣列表示的Hash表

runtime會根據這三項找到快取的位置、經過一些計算在 bukets陣列中找到buket、每一個bucket包含一個selector和一個IMP,通過對比selector來判斷是否有快取

在回過頭來看objc_msgSend 的執行流程:

首先,判斷接收物件 person 是否為nil;
根據person物件的isa指標找到它的class類;
從類的快取中找run,找到則分發
如果快取中沒有,在 class 類的 method list 找 run ;
如果 class 中沒到 run,繼續往它的 superClass 中找 ;直到基類;
都沒有找到,報錯,丟擲異常;

逐行剖析objc_msgSend彙編原始碼文章對objc_msgSend的彙編指令進行分析,快取詳細的分析了是怎麼在方法中找到快取。

訊息轉發

如果在一個物件的類和父類基類中都沒有找到要執行的方法,程式會crash;控制檯會顯示類似錯誤資訊:unrecognized selector,訊息被髮送給了不能處理它的物件。

OC是一門動態語言,我們可以在執行期做一些事來讓crash不發生,訊息轉發機制就是用來解決這個問題的,在執行期通過3分 【接盤俠】方法,給物件和訊息更多的機會來完成成功的呼叫,而不是直接 crash。

Runtime知識點整理

在一個函式找不到時,OC提供了三種方式去補救:

一號接盤俠:
動態解析階段:執行期新增方法

+(BOOL)resolveInstanceMethod:(SEL)sel   (例項方法呼叫)
+(BOOL)resolveClassMethod:(SEL)sel  (類方法呼叫)
複製程式碼

通過class_addMethod動態新增一個方法

二號接盤俠:
備援接受者:轉發給另1個物件、改變方法時

-(id)forwardingTargetForSelector:(SEL)aSelector
複製程式碼

詢問是否把訊息轉發給其他接受者處理

三號接盤俠:
完整訊息轉發:需要轉發給多個物件時

-(void)forwardInvocation:(NSInvocation *)anInvocation
複製程式碼

如果都不中,呼叫doesNotRecognizeSelector丟擲異常。

Runtime的應用

1. 方法交換

使用 method_exchangeImplementations

Method m1 = class_getClassMethod([M1 class], @selector(method1name));
Method m2 = class_getClassMethod([M2 class], @selector(method2name));
method_exchangeImplementations(m1, m2);
複製程式碼

runtime的原始碼,在runtime.h中方法的宣告

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
複製程式碼

objc-runtime-new.mm 檔案中方法的實現,可以看到核心的程式碼實現,是交換了方法 m1 和 m2 的imp指標,所以當我們呼叫方法 m1 時,實際呼叫的是 m2 的imp,也就實現了方法的交換。這也能體現 OC 執行時語言的特點。

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;

    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}
複製程式碼

2. 為category新增屬性

我們都知道category中不能新增屬性,準確的說是隻能宣告屬性,而不能為我們增加屬性的實現。category的實現原理,以及為什麼不能新增屬性,請移步這裡有詳細的介紹。

3. 其他

1、動態的新增一個類
KVO的實現就是利用runtime動態的新增類,系統是在程式執行的時候根據你要監聽的類,動態新增一個新類繼承自該類,然後重寫原類的setter方法並在裡面通知observer的;

2、通過 Runtime 獲取一個類的所有屬性
YYModel 等資料解析的框架都有用到,獲取類的多有屬性,屬性名稱,屬性型別,利用遞迴的方式和 json 資料一一賦值;

3、動態變數控制,動態增加方法

4、自動歸檔和解檔

5、外掛開發
XCode官方不支援外掛開發,通過標頭檔案方法名猜測方法的作用,swizzle 這些方法,插入自己的程式碼實現外掛邏輯。

6、JSPatch 熱更新,其根本原理都是利用OC的動態語言特性去動態修改類的方法實現。

小結

runtime的應用還有很多,沒一個點深入研究都是一個 topc,大家有興趣和時間的時候可以逐一去研究其中的原理和實現。總之,runtime 的應用,就是利用 OC 動態語言的特性,在執行時做一些 ‘手腳’,去完成一些功能。

相關文章