OC-Runtime的理解和簡單使用

Runtime是OC里面非常重要的一個概念,它是OC的底層實現(xiàn)两残,也正是因為Runtime,OC成為一個動態(tài)語言把跨,并且擁有了面向對象的能力人弓。這篇文章,將詳細說明Runtime的各種知識着逐,并且能夠實際運用崔赌。

  • 什么是Runtime

    Runtime即運行時,也就是程序在運行的時候做的事情耸别。在iOS中健芭,有一套底層的C語言API,它是iOS內部核心之一秀姐,我們編寫的OC代碼慈迈,底層都是基于它實現(xiàn)的,OC語言在編譯后省有,都是Runtime形式的C語言代碼痒留。

學習Runtime可以使我們更加清楚地了解OC語言的底層實現(xiàn),從而可以運用它去實現(xiàn)很多OC語言實現(xiàn)不了的功能(比如給Category添加屬性)锥咸。

  • 類和對象

在沒有接觸Runtime之前狭瞎,我們對OC的類和對象只有概念上的理解,并不知道它本質上是什么〔瑁現(xiàn)在我們來看看它們的底層定義熊锭,我們先從我們經(jīng)常使用的id入手,在<objc/objc.h>中,我們找到這一段定義:

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

這里說明id是一個指向objc_object結構體的指針碗殷,而注釋又說精绎,這個指針指向一個類的實例對象,所以我們知道了锌妻,objc_object結構體就代表了一個類的實例對象:

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

objc_object這個結構體里面代乃,只有一個isa,這個isa的類型是Class仿粹,并且是不能為空的搁吓。顧名思義,這個肯定就是這個對象的類型了吭历。
然后我們再去找找Class又是什么東西:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

這里我們可以看到堕仔,Class是一個指向objc_class結構體的指針。所以我們知道了晌区,isa是一個指向objc_class的指針摩骨,即通過它可以找到一個對象的類。所以朗若,id類型其實就是一個指向任意類型實例的指針恼五。接著,我們再去看看objc_class是什么東西:

struct objc_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;  // 協(xié)議鏈表
#endif

} OBJC2_UNAVAILABLE;

我們先從第一行可以得知,OC的類里面银伟,也有一個isa指針你虹,而這個指針指向了這個類的類型,由此我們可以推斷出來彤避,其實OC的類本質上也是一個對象,因為它也有自己的類夯辖。
在OC里面琉预,每一個類的isa指針都指向它的元類,最終指向NSObject蒿褂,NSObject的元類是它自己圆米。而NSObject的父類則是nil。這張圖很好的說明了isasuper_class的區(qū)別:


接著啄栓,我們可以看到娄帖,一個類的結構體里面,還有它的父類昙楚,類名近速,版本號,類信息,變量大小削葱,變量列表奖亚,方法列表,方法緩存析砸,協(xié)議列表這些東西昔字。
這里也解釋了為什么前面的objc_object里面只有一個isa指針,是因為只要有了這個指針首繁,就能夠找到這個類里面的所有方法和屬性作郭。
看了這些,我們就對OC的類和對象有了更深刻的理解弦疮。接下來所坯,我們再去探究一下OC是怎么調用方法的。

  • OC的消息發(fā)送

OC的方法調用底層是給某個對象發(fā)送某個方法挂捅。并且芹助,在編譯的時候并沒有確定具體調用哪個方法,只有在運行時才能確定闲先。我們來驗證一下状土,首先隨便創(chuàng)建一個空的命令行項目:



然后創(chuàng)建一個Person類,然后寫一個空的方法:

.h
@interface Person : NSObject

- (void)run;

@end

.m
- (void)run {
    
}

然后打開終端伺糠,cd到剛剛創(chuàng)建的main.m文件所在的文件夾下蒙谓,執(zhí)行clang -rewrite-objc main.m
這時候,就可以在文件夾里面看到一個main.cpp文件训桶,打開累驮,拉到最下面,就可以看到這一段代碼:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        Person *p = ((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)p, sel_registerName("run"));
    }
    return 0;
}

可以看到我們剛剛寫的方法編譯過之后全部變成了objc_msgSend的方式舵揭,甚至allocinit方法也變成了這樣谤专。這就說明OC方法的底層調用都是objc_msgSend實現(xiàn)的,而objc_msgSend就是消息發(fā)送午绳。同時置侍,也驗證了OC底層就是用Runtime實現(xiàn)的C語言代碼。

接下來我們來研究一下這個objc_msgSend拦焚。要使用它蜡坊,得首先導入#import <objc/message.h>,然后就可以使用了赎败。但是秕衙,我們打出來這個發(fā)現(xiàn)沒有任何參數(shù)提示:


這是因為蘋果公司不建議我們這么用了。我們可以在Build Setting里面僵刮,找到

改成NO据忘,再回去敲入objc_msgSend鹦牛,就可以看到提示了:

具體的參數(shù)含義是:
id _Nullable selfid類型我們前面知道,它可以指向任意OC對象若河,這地方就代表著給誰發(fā)消息能岩,也就是調用誰的方法。
...:三個點代表參數(shù)列表/可擴展參數(shù)萧福。
SEL _Nonnull op:SEL又是什么呢拉鹃?到這里,我們就得提一下OC里面的方法了鲫忍。老規(guī)矩膏燕,我們先去找定義,在<objc/runtime.h>中:

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

這里我們可以看到悟民,OC的方法里面坝辫,有三個東西,一個是SEL射亏,一個是char * 近忙,一個是IMP,這個char *我們知道是C語言的字符串智润,這個地方是一組描述方法的參數(shù)類型的字符數(shù)組及舍,后面我們會詳細了解這個東西,這里先不管它窟绷。SEL這個地方通過命名我們可以看出來是方法名锯玛,IMP我們就完全看不出了,它們具體是怎么定義的呢兼蜈?還是在<objc/objc.h>中:

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

/// 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

這里攘残,我們可以看到,IMP其實就是一個指向方法實現(xiàn)的指針为狸,而SEL則是一個objc_selector結構體歼郭,源碼中我們找不到SEL的定義,經(jīng)過查閱資料得知钥平,它完全可以理解為一個char *实撒,也就是說,其實它就是方法名的字符串涉瘾,也就是一個方法的標簽。
知道這些以后捷兰,我們就可以用消息發(fā)送來改寫以前的OC代碼立叛,比如:
前面的Person類的run方法的調用:

objc_msgSend(p, @selector(run));

為了方便測試,我們給run方法寫一個簡單的實現(xiàn):

- (void)run {
    NSLog(@"跑了");
}

然后運行一下:



完美贡茅!

甚至Person類對象的聲明都可以用發(fā)送消息的方式來完成(解耦合):

Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p,@selector(init));

再運行一下秘蛇,一樣可以得到之前的結果其做,這里就不貼圖了,跟上面那個圖一樣赁还。
Runtime為我們提供了直接通過類名獲取類的函數(shù):

objc_getClass(char * _Nonnull name);

和得到一個SEL的函數(shù):

sel_registerName(const char * _Nonnull str);

這樣我們可以繼續(xù)改進之前的代碼:

id p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")),sel_registerName("init"));
objc_msgSend(p,sel_registerName("run"));;

一樣可以運行得到結果妖泄。
到了這一步,是不是就跟我們之前看到的編譯后的OC代碼一樣了艘策?我們甚至不需要導入Person.h頭文件蹈胡,就可以直接獲取創(chuàng)建它的實例,并且執(zhí)行方法朋蔫,完成了解耦罚渐。

  • OC的消息轉發(fā)(message forwarding)

看了上面的一些代碼,不知道你有沒有考慮過一個問題驯妄。發(fā)送消息的時候荷并,我們只需要填一些字符串參數(shù)之類的就可以了,完全不知道有沒有這個方法青扔,如果沒有這個方法會發(fā)生什么事情呢源织?
接下來,我們做個試驗:



直接crash掉了微猖。調用方法時谈息,如果在方法在對象的類繼承體系中沒有找到,那怎么辦励两?一般情況下黎茎,程序在運行時就會Crash掉,拋出 unrecognized selector sent to …類似這樣的異常信息当悔。但在拋出異常之前傅瞻,還有三次機會按以下順序讓你拯救程序。這就涉及到以下4個方法:

  • 動態(tài)方法解析(dynamic method resolution)

首先會調用+ resolveInstanceMethod:(對應實例方法)或+ resolveClassMethod:(對應類方法)方法盲憎,讓你添加方法的實現(xiàn)嗅骄。如果你添加方法并返回YES,那系統(tǒng)在運行時就會重新啟動一次消息發(fā)送的過程饼疙。
我們這里測試一下溺森,增加一個wahaha方法的實現(xiàn),看看是否可以順利運行窑眯,首先屏积,我們在Person.m中導入#import <objc/message.h>,然后磅甩,利用

class_addMethod(Class  _Nullable __unsafe_unretained cls,SEL  _Nonnull name,IMP  _Nonnull imp, const char * _Nullable types)

來增加一個方法及實現(xiàn)炊林,其中,第一個參數(shù)填self卷要,第二個參數(shù)填wahaha的SEL渣聚,可以用@selector(wahaha),也可以用之前用過的sel_registerName("wahaha"),第三個參數(shù)需要一個imp独榴,我們知道IMP是指向方法實現(xiàn)的指針,這里我們可以用imp_implementationWithBlock(id _Nonnull block)來實現(xiàn)奕枝,最后一個參數(shù)我們之前也見過棺榔,就是方法定義里面的的method_types,這個東西該怎么寫呢隘道?我們先去查一下官方文檔:

types
An array of characters that describe the types of the arguments to >the method. For possible values, see Objective-C Runtime >Programming Guide > Type Encodings. >Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).

這里面說明這個是一組描述方法的參數(shù)類型的字符數(shù)組症歇,并且,每個方法都有兩個被隱含的參數(shù)薄声,一個是self(代表當前對象)当船,一個是_cmd(代表當前對象的SEL),所以第二個和第三個字符必須是@:默辨,而第一個字符是返回值德频,所以一個沒有參數(shù)的方法,它的types就是"v@:"缩幸。至于什么類型對應什么字符壹置,可以去上面的鏈接中找。所以我們這里可以直接用"v@:"表谊。

//如果增加了方法并返回YES钞护,就會重新發(fā)送消息并處理,返回NO爆办,則進入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == sel_registerName("wahaha")) {
        class_addMethod(self, sel_registerName("wahaha"), imp_implementationWithBlock(^(){
            NSLog(@"wahaha");
        }), "v@:");
    }
    return YES;
}

運行結果:


怎么樣难咕,是不是很神奇?如果上面返回NO距辆,則會進入完整的消息轉發(fā)機制(full forwarding mechanism)余佃,這里又分為兩個步驟:

  • 快速消息轉發(fā) (Fast Forwarding)

這個時候,如果實現(xiàn)了- forwardingTargetForSelector:方法跨算,系統(tǒng)就會進入該方法繼續(xù)處理消息爆土,這個方法的作用是把之前沒辦法處理的消息轉發(fā)給別的對象去處理:

//返回一個對象繼續(xù)處理消息
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == sel_registerName("wahaha")) {
        return [Dog new];
    }
    return nil;
}

這里我們新建了一個Dog類,實現(xiàn)了wahaha方法诸蚕,所以我們直接返回一個Dog的實例步势,最后運行結果如上,這里就不貼圖了背犯。

  • 普通消息轉發(fā)(Normal Forwarding)

如果上一步也沒有對消息進行處理坏瘩,則會進入最后一步,這里涉及到兩個方法漠魏。它首先調用methodSignatureForSelector:方法來獲取函數(shù)的參數(shù)和返回值桑腮,如果返回為nil,程序會Crash掉蛉幸,并拋出unrecognized selector sent to instance異常信息破讨。如果返回一個函數(shù)簽名,系統(tǒng)就會創(chuàng)建一個NSInvocation對象并調用-forwardInvocation:方法奕纫。我們同樣在這里對之前的消息進行處理一次:

//返回方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == sel_registerName("wahaha")) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//轉發(fā)消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    Dog *dog = [Dog new];
    if ([dog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:dog];
    }
}

以上就是OC消息傳遞過程中發(fā)生的事情提陶,利用這些我們可以在很多地方對一個消息做處理。但是我們該怎么選擇呢匹层?

  1. 動態(tài)方法解析:由于Method Resolution不能像消息轉發(fā)那樣可以交給其他對象來處理隙笆,所以只適用于在原來的類中代替掉。
  2. 快速消息轉發(fā):其他對象升筏,使用范圍更廣撑柔,不只是限于原來的對象。
  3. 普通消息轉發(fā):它一樣可以消息轉發(fā)您访,但它能通過NSInvocation對象獲取更多消息發(fā)送的信息铅忿,例如:target、selector灵汪、arguments和返回值等信息檀训。

同時需要注意的是,消息轉發(fā)過程中享言,步驟越往后峻凫,處理消息的代價就越大,最好能在第一步就處理完览露,這樣的話荧琼,運行期系統(tǒng)可以將此方法緩存。如果這個類的實例還會再接收到同名選擇子差牛,那么根本無須再次啟動消息轉發(fā)流程命锄。
通過消息發(fā)送,我們其實已經(jīng)對Runtime做了一個簡單的運用了多糠。接下來累舷,我們再多一些探討。

  • Runtime可以做什么夹孔?

  1. 在程序運行的時候動態(tài)添加一個類
  2. 在程序運行的時候動態(tài)的修改一個類的屬性和方法
  3. 在程序運行的時候遍歷一個類的所有屬性

Runtime有很多方法被盈,可以在文檔中一一查看,不同功能的方法通過前綴區(qū)分搭伤,比如說class_就是對類的操作只怎,objc_就是對對象的操作,等等怜俐,都比較好理解身堡。

了解這些以后,我們再回頭看之前提過的一個問題拍鲤,就是給Category增加屬性贴谎。
我們先看一下汞扎,如果直接給Category增加屬性會發(fā)生什么,我們給Person類創(chuàng)建一個Play分類擅这,然后添加一個屬性:

@interface Person (Play)

@property (nonatomic, strong) NSString *gameName;

@end

但是我們使用的時候會發(fā)現(xiàn)澈魄,直接就Crash了:


我們現(xiàn)在應該就能明白,這是因為找不到Getter仲翎,同時也沒有Setter痹扇,我們可以給它添加這兩個方法,但是我們在給它添加的時候溯香,發(fā)現(xiàn)在Category里面根本就沒有_gameName這個變量鲫构,所以沒辦法像別的類那樣直接添加,這時候玫坛,我們就可以利用Runtime的objc_setAssociatedObjectobjc_getAssociatedObject來實現(xiàn):

- (NSString *)gameName{
    return objc_getAssociatedObject(self, _cmd);
}
- (void)setGameName:(NSString *)gameName {
    objc_setAssociatedObject(self, @selector(gameName), gameName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

再次運行结笨,就可以正常使用這個屬性了:

通過以上簡單的例子,我們可以看得出來昂秃,Runtime是OC代碼的底層實現(xiàn)禀梳,所以很多OC代碼不支持的事情,我們都可以通過Runtime自己去實現(xiàn)肠骆,具體在什么場景下使用要根據(jù)實際需求做具體分析算途。接下來屁使,我會用Runtime和之前Block種提到的相關技術自己實現(xiàn)KVO舌菜,并且進行一些改造。另外轴猎,本篇文章的代碼可以在我的github上查看莉钙,點擊前往廓脆。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市磁玉,隨后出現(xiàn)的幾起案子停忿,更是在濱河造成了極大的恐慌,老刑警劉巖蚊伞,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件席赂,死亡現(xiàn)場離奇詭異,居然都是意外死亡时迫,警方通過查閱死者的電腦和手機颅停,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來掠拳,“玉大人癞揉,你說我怎么就攤上這事。” “怎么了喊熟?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵柏肪,是天一觀的道長。 經(jīng)常有香客問我逊移,道長预吆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任胳泉,我火速辦了婚禮,結果婚禮上岩遗,老公的妹妹穿的比我還像新娘扇商。我一直安慰自己,他們只是感情好宿礁,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布案铺。 她就那樣靜靜地躺著,像睡著了一般梆靖。 火紅的嫁衣襯著肌膚如雪控汉。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天返吻,我揣著相機與錄音姑子,去河邊找鬼。 笑死测僵,一個胖子當著我的面吹牛街佑,可吹牛的內容都是我干的。 我是一名探鬼主播捍靠,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼沐旨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了榨婆?” 一聲冷哼從身側響起磁携,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎良风,沒想到半個月后谊迄,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡拖吼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年鳞上,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吊档。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡篙议,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情鬼贱,我是刑警寧澤移怯,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站这难,受9級特大地震影響舟误,放射性物質發(fā)生泄漏。R本人自食惡果不足惜姻乓,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一嵌溢、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蹋岩,春花似錦赖草、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扣囊,卻和暖如春乎折,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背侵歇。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工骂澄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盒至。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓酗洒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親枷遂。 傳聞我的和親對象是個殘疾皇子樱衷,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內容

  • 轉至元數(shù)據(jù)結尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,725評論 0 9
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡介 Runt...
    樂樂的簡書閱讀 2,137評論 0 9
  • Runtime是什么 Runtime 又叫運行時酒唉,是一套底層的 C 語言 API矩桂,其為 iOS 內部的核心之一,我...
    SuAdrenine閱讀 879評論 0 3
  • 寶寶痪伦,冥王星又要中斷一下下了侄榴,由于生病體虛,晚上基本不能寫网沾,今天又忙著來還書癞蚕,都已經(jīng)過期了,要交罰款2刀辉哥。 發(fā)現(xiàn)圖...
    Hayeknz閱讀 163評論 0 0
  • 本文通過解析Class文件中字節(jié)碼的結構桦山,來加深對Java類文件結構的理解攒射。建議先閱讀Java類文件結構解析這篇文...
    tianbin閱讀 409評論 0 0