iOS runtime 理解消息傳遞、轉發機制和使用案例

關于runtime,網上的資料都很全了,這里是根據自己的理解寫一個學習總結報告。主要借鑒文章如下:
http://www.lxweimin.com/p/db6dc23834e3 神經病院Objective-C Runtime出院第三天——如何正確使用Runtime
https://www.cnblogs.com/ioshe/p/5489086.html [iOS開發-Runtime詳解]

1.runtime是用來做什么的?

1.1 與runtime相關最大的,就是OC語言的動態綁定機制。

動態綁定是指一個對象發送消息后,該消息的實現(實際執行的函數)根據運行環境的不同而不同(此處只針對OC,Swift中已經不是運行時加載方法,而是和C語言類似,在編譯階段就確定了)。實現該機制,常用的就是分類(categor)、類擴展(extension)、子類(subclass)繼承等我們每個人都會使用的設計模式。
正常情況下,我們使用OC的這些特性就能夠解決大部分問題。但是有些情況下,為了優雅、高效的解決問題,我們有時候希望從更底層的層面進行操縱。


1.2. 一個經典案例

有一個業務需求,我們希望統計某個頁面viewController被點擊的次數,或者在進入某些頁面的時候添加引導圖。常規的做法是在這些對應的頁面的viewDidLoad中進行對應的需求作業。但是,如果項目比較大,頁面非常多,或者層級很復雜,這樣操作就效率很低,需要到不同的界面去進行分散的操作,日后新增、修改、維護或者調整也很麻煩。一個比較高效、優雅的做法是在基類UIViewContoller的viewDidLoad中實現該方法,因為所有的頁面都會繼承基類的viewDidLoad方法,在該基類中實現之后我們只需要在此處維護和新增就夠了。
所以我們需要給UIViewContoller基類添加一個category分類,在分類中重寫viewDidLoad方法。但是如果直接在分類中重寫,會導致基類代碼中的viewDidLoad不執行。此時,我們就需要使用runtime相關的方法來解決該問題(具體方式見第4節,第2條方法交換,如果需要深入了解,可以看看動態埋點統計的實例)。


1.3 常用runtime實現的強大功能

OC本質上是C的擴展和封裝。我們的OC代碼運行時,底層調用的實際上是c語言的代碼。runtime(翻譯過來即運行時)就是蘋果暴露給用戶的一個偏底層的可以操作底層代碼的API接口,是對常用的設計模式的一個必要補充。通過該接口的一些函數,我們可以直接干預消息發送過程,從而實現很多強大的功能。比如

  • (1) 實現多繼承Multiple Inheritance (利用消息轉發機制)
  • (2) 在分類中重寫原類方法而又不失去原類方法中的功能 (利用class的Method Swizzling)
  • (3) Aspect Oriented Programming (切片編程)
  • (4) 重寫class方法(Isa Swizzling)
  • (5) 給分類添加屬性變量( 利用Associated Object給分類添加關聯對象)
  • (6) 動態的增加方法 (利用消息轉發機制,在運行時實現方法)
  • (7) NSCoding的自動歸檔和自動解檔(利用類底層的結構查詢函數,批量給所有屬性自動添加相同的解檔、歸檔方法)

2. runtime API中主要內容

2.1 對象、類的定義

從下表可以看到,本質上類是一個指向類結構體的指針,而對象是一個指向對象結構體的指針,對象結構體中存儲有一個isa類,它動態的指向該對象的類。類結構體中存儲有類的名字,父類名字,類的成員變量(無論是通過@property還是直接定義的成員變量都存儲在這里),類的實例變量大小(我們定義實例變量的時候變量空間大小就已經確定了),類的方法鏈表(普通類里面存儲著該類的實例方法,元類中存儲中該類的類方法),協議鏈表,方法緩存表(我們發送消息時第一個查詢的結構體)等。

//對象結構體中存儲有一個isa類
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;//Isa標識對象的類;它指向一個結構的類定義編譯。
};
//所有的對象本質上都是一個id,而id是一個指向對象結構體的指針
typedef struct objc_object *id;
//類是一個指向類結構體的指針
typedef struct objc_class *Class;` 
`//類結構體中存儲有該類定義的所有相關數據;` 
`struct objc_class {`
`Class isa  OBJC_ISA_AVAILABILITY;//Isa標識對象的類;它指向一個結構的類定義編譯。`
`#if !__OBJC2__`
`Class super_class;``//父類`
`const char *name;``//類名`
`long version;``//類的版本信息,默認為0`
`long info;``//類信息,供運行期使用的一些位標識`
`long instance_size;``//類的實例變量大小`
`struct objc_ivar_list *ivars;``// 類的成員變量鏈表`
`struct objc_method_list **methodLists;``// 方法鏈表`
`struct objc_cache *cache;``//方法緩存`
`struct objc_protocol_list *protocols;``//協議鏈表#`
`endif} `
`OBJC2_UNAVAILABLE;`

因為類也有一個isa 指針,所以類本質上也是一個對象,稱為類對象。類對象Isa指針標識的類為該類的元類(meta class),每一個類都是這個元類的唯一實例對象。元類對象Isa指針標識的類為根元類,根元類(root meta Class)在整個系統中只有一個,所有的元類的isa指針都指向根元類,根元類的Isa指針標識的類為自己。具體如下所示,圖中虛線代表類的isa指針指向,實線代表類的父類。根元類的父類是根類,同時根元類的實例對象也是根類(root class),這里形成了一個閉環。


isa、superclass指針.png

isa指針指向:實例對象->類->元類->(不經過父元類)直接到根元類(NSObject的元類),根元類的isa指向自己;

2.2 Method、IMP、SEL的定義

把他們拿出來說,是因為容易他們之間存在相關性和差異,非常容易產生誤解,而且他們對我們理解消息機制很有幫助,我們可以看一下方法Method的定義如下:

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 是一個指向結構體的指針,它包含了IMP和SEL,還包含了方法類型定義、方法的參數等。
SEL是方法的指針,但不同于C語言中的函數指針,函數指針直接保存了方法的地址,但SEL只是方法編號;
IMP是方法的具體實現函數指針,在runtime里,我們可以使用函數改變或者設置IMP來更改一個函數的具體實現,例如:

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) //交換兩個方法的實現
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) // 給一個方法設置實現 
2.3 runtime中常用的的一些函數

runtime中的函數,一般按照結構體的層級結構來操縱。對類中成員進行操作的,以class開頭,對方法中成員進行操作的以method開頭,其他的以此類推。常見的函數如下:

class_getProperty(Class _Nullable cls, const char * _Nonnull name)   //獲取類的所有屬性列表
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)      //給類添加方法
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)  //替換類方法
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size, 
              uint8_t alignment, const char * _Nullable types)    //增加類變量
method_getImplementation(Method _Nonnull m)   //獲取方法的實現
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)  //設置方法的實現
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)  //交換方法的實現
imp_implementationWithBlock(id _Nonnull block) //使用一個block創建一個實現
sel_getName(SEL _Nonnull sel) //獲取方法的名稱
sel_registerName(const char * _Nonnull str) //注冊一個方法

3.消息傳遞、轉發機制

想要合理的利用runtime中相關API接口,必須理解runtime中的消息傳遞、轉發機制。
(1)當一個對象發送消息時,首先,底層會執行一個消息發送函數,函數長這樣

 objc_msgSend(void /* id self, SEL op, ... */ 

如果是使用super發送消息,函數長這樣:

 objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ 

(2)底層會從該對象所屬的類中(isa指針所指的類)的方法緩存列表查找對應的實現
(3)如果2找不到,會從該類的方法鏈表中繼續查找
(4)如果3找不到,會跳轉到該類的父類查找,父類步驟和子類一樣,具體如下圖所示。


isa方法傳遞.gif

(5)一直向上到根類,如果根類仍然找不到,就開始準備進行消息轉發。轉發第一步:動態消息解析。查看當前類是否實現了resolveInstanceMethod方法(如果是類方法,會看是否實現了resolveClassMethod方法)。如果該方法返回了YES,消息轉發終止。我們可以在這個方法中動態添加方法實現,不實現也不要緊,只要返回YES消息發送就不會報錯。

+(BOOL)resolveClassMethod:(SEL)sel
{
    NSString * selStr = NSStringFromSelector(sel);
    if ([selStr isEqualToString:@"runTest"]) {
//注意,想要給類添加方法,必須添加到它的metaClass上,所以在class_addMethod中添加的類都要是原類!!!
//  確定metaClass的方法是objc_getMetaClass(object_getClassName(self));
        if (class_addMethod(objc_getMetaClass(object_getClassName(self)), sel,class_getMethodImplementation(objc_getMetaClass(object_getClassName(self)), @selector(runTestFunction)), "s@:")) {
            return YES;
        }
        return [super resolveClassMethod:sel];
    }
    return [super resolveClassMethod:sel];
}

(6)如果第5步返回NO,就開始消息重定向。查看是否指定了其他對象來執行該方法。具體是查看當前類是否實現了forwardingTargetForSelector方法;如果該方法返回了一個對象,就在該對象上執行該selctor方法(該對象上執行該方法時步驟與本對象一致);

-(id)forwardingTargetForSelector:(SEL)aSelector

(7)如果第6步返回nil,就需要進行真正的消息轉發機制。具體是查看當前類是否實現了methodSignatureForSelector方法,如果該方法返回不為nil,就執行forwardInvocation方法。如果forwardInvocation實現了,消息轉發終止(但不見得消息轉發完成,forwardInvocation只是一個消息的分發中心,將這些不能識別的消息轉發給不同的接收對象,或者轉發給同一個對象,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息,因此沒有響應也不會報錯。)。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

(8)上述步驟,如果到第7部都沒有實現,系統就會報錯,提示unrecognized selector sent to instance
注意:上述任何一步,都要在前一步驟沒有完成的基礎上 。

消息轉發機制.png

4.如何使用runtime

基于runtime提供的數據結構,以及上述消息傳遞、轉發機制,runtime提供了豐富的函數讓我們來實現我們第1節中提到的強大的功能,我們這里簡單梳理下實現方式:

  • (1) 實現多繼承Multiple Inheritance (利用消息轉發機制),如下圖所示。


    1330553-c7ef6392ecc9ee9d.gif

我們在Warrior中頭文件中定義一個方法negotiate,但是不實現它,而在forwardingTargetForSelector方法中,針對該selecotr,指定一個Diplomat對象,就可以將該方法實現交給diplomat類來實現。看起來就像是Warrior也繼承了了Diplomat的方法一樣(注意,像respondsToSelector:isKindOfClass: 這類方法只會考慮繼承體系,不會考慮轉發鏈。也就是說如果[Warrior respondsToSelector:negotiate]會返回NO)。

  • (2) 在分類中重寫原類方法而又不失去原類方法中的功能 (利用class的Method Swizzling)

    實現方式是通過runtime中的實現交換函數method_exchangeImplementations。首先,在本類中定義另一個待交換的方法exchage_ViewDidLoad;待交換的方法中需要調用原方法,然后添加需要額外實現的功能(例如第1節中提到的數據統計方法)。在恰當的時機(一般是在load方法中),交換該兩個方法的實現。實際執行代碼的使用,調用原類方法會執行待交換的方法的實現,待交換的方法實現中又會調用原來的方法實現,從而保留了原來的方法的實現。

+(void)exchangeOriginMethodWithMethodExchangeMethod
{
//    防止方法被多次調用后交換失效;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originSEL =  @selector(viewDidLoad);
        SEL swizzSEL = @selector(exchage_ViewDidLoad);
        Method viewDidLoad = class_getInstanceMethod([self class], originSEL);
        Method exchang_viewDidLoad = class_getInstanceMethod([self class], swizzSEL);
//        測試原來的選擇子是否已經添加了方法(是否已經交換了方法);
        Boolean didAddMethod = class_addMethod([self class], originSEL,method_getImplementation(exchang_viewDidLoad),method_getTypeEncoding(exchang_viewDidLoad));
        if (!didAddMethod) {
//
            //        如果沒有添加方法,就直接交換
            method_exchangeImplementations(viewDidLoad, exchang_viewDidLoad);
        }else{
//            如果已經添加了,就同時更換交換后的方法實現;
            class_replaceMethod([self class], swizzSEL, method_getImplementation(viewDidLoad), method_getTypeEncoding(viewDidLoad));
        }
    });
}
-(void)exchage_ViewDidLoad
{
    NSLog(@"%@ did load",self);
    [self exchage_ViewDidLoad];//注意,exchage_ViewDidLoad的實現現在是viewDidLoad了,所以沒有循環調用
}
  • (3) Aspect Oriented Programming (切片編程,內容太多,暫不展開,可以看這里)

  • (4) 重寫class方法(Isa Swizzling)

    蘋果著名的KVO技術和NSNotificationCenter就使用的該方法,在我們給一個對象添加了KVO鍵值觀察方法后

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)contex。

后臺會重新創建一個NSKVONotifying_Object類,然后偷偷將原來的類的isa指針指向該類。該類中會在屬性變量修改時候,調用

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context

方法,并發出相應的通知

  • (5) 給分類添加屬性變量( 利用Associated Object給分類添加關聯對象)

    我們可以給分類添加屬性,但是分類不會自動給我們生成成員變量。因為類的成員變量在編譯器已經決定了(寫入了類的結構體中,具體見前面結構體的定義),但是category是在運行期才決議的。所以如果要給分類添加成員變量,需要用runtime里面函數在運行期實現。一般使用objc_setAssociatedObject和objc_getAssociatedObject函數來實現。這兩個函數都是成對的出現,一個給對象添加關聯對象,一個獲取關聯對象。具體代碼如下。

  @property(nonatomic,strong)id associatedObjcet;
-(void)setAssociatedObjcet:(id)associatedObjcet{
    objc_setAssociatedObject(self, @selector(associatedObjcet), associatedObjcet, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(id)associatedObjcet{
    return objc_getAssociatedObject(self, @selector(associatedObjcet));
}
  • (6) 動態的增加方法 (利用消息轉發機制,在運行時實現方法)

    動態的增加方法和多重繼承有些類似,都是調用的方法在類中并沒有實現代碼,而是在消息轉發機制的某一步才動態的添加實現代碼。消息轉發機制本身有多步驟,所以根據需要,可以在不同的步驟實現動態添加,常見的一般在方法動態解析resolveInstanceMethod或者在消息轉發forwardInvocation的時候進行。

  • (7) NSCoding的自動歸檔和自動解檔(利用類底層的結構查詢函數,批量給所有屬性自動添加相同的解檔、歸檔方法)

NSCoding其實就是對所有的屬性調用encode和decode方法。使用手動操作有一個缺陷,如果屬性多起來,要寫好多行相似的代碼,雖然功能是可以完美實現,但是看上去不是很優雅。用runtime實現的思路就比較簡單,我們循環依次找到每個成員變量的名稱,然后利用KVC讀取和賦值就可以完成encodeWithCoder和initWithCoder了,部分代碼如下:

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

推薦閱讀更多精彩內容