探秘Runtime - Runtime介紹

該文章屬于劉小壯原創(chuàng)盟庞,轉載請注明:劉小壯


RuntimeiOS系統(tǒng)中重要的組成部分普筹,面試也是必問的問題冒版,所以Runtime是一個iOS工程師必須掌握的知識點。

現(xiàn)在市面上有很多關于Runtime的學習資料歌殃,也有不少高質(zhì)量的乔妈,但是大多數(shù)質(zhì)量都不是很高,而且都只介紹某個點氓皱,并不全面路召。

這段時間正好公司內(nèi)部組織技術分享,我分享的主題就是Runtime波材,我把分享的資料發(fā)到博客股淡,大家一起學習交流。

文章都是我的一些筆記廷区,和平時的技術積累唯灵。個人水平有限,文章有什么問題還請各位大神指導隙轻,謝謝早敬!??


描述

OC語言是一門動態(tài)語言忌傻,會將程序的一些決定工作從編譯期推遲到運行期。由于OC語言運行時的特性搞监,所以其不只需要依賴編譯器水孩,還需要依賴運行時環(huán)境。

OC語言在編譯期都會被編譯為C語言的Runtime代碼琐驴,二進制執(zhí)行過程中執(zhí)行的都是C語言代碼俘种。而OC的類本質(zhì)上都是結構體,在編譯時都會以結構體的形式被編譯到二進制中绝淡。Runtime是一套由C宙刘、C++、匯編實現(xiàn)的API牢酵,所有的方法調(diào)用都叫做發(fā)送消息悬包。

根據(jù)Apple官方文檔的描述,目前OC運行時分為兩個版本馍乙,ModernLegacy布近。二者的區(qū)別在于Legacy在實例變量發(fā)生改變后,需要重新編譯其子類丝格。Modern在實例變量發(fā)生改變后撑瞧,不需要重新編譯其子類。

Runtime不只是一些C語言的API显蝌,其由Class预伺、Meta ClassInstance曼尊、Class Instance組成酬诀,是一套完整的面向對象的數(shù)據(jù)結構。所以研究Runtime整體的對象模型骆撇,比研究API是怎么實現(xiàn)的更有意義瞒御。

使用Runtime

Runtime是一個共享動態(tài)庫,其目錄位于/usr/include/objc艾船,由一系列的C函數(shù)和結構體構成葵腹。和Runtime系統(tǒng)發(fā)生交互的方式有三種高每,一般都是用前兩種:

  1. 使用OC源碼
    直接使用上層OC源碼屿岂,底層會通過Runtime為其提供運行支持,上層不需要關心Runtime運行鲸匿。
  2. NSObject
    在OC代碼中絕大多數(shù)的類都是繼承自NSObject的爷怀,NSProxy類例外。RuntimeNSObject中定義了一些基礎操作带欢,NSObject的子類也具備這些特性运授。
  3. Runtime動態(tài)庫
    上層的OC源碼都是通過Runtime實現(xiàn)的烤惊,我們一般不直接使用Runtime,直接和OC代碼打交道就可以吁朦。

使用Runtime需要引入下面兩個頭文件柒室,一些基礎方法都定義在這兩個文件中。

#import <objc/runtime.h>
#import <objc/message.h>

對象模型

下面圖中表示了對象間isa的關系逗宜,以及類的繼承關系雄右。

對象模型

Runtime源碼可以看出,每個對象都是一個objc_object的結構體纺讲,在結構體中有一個isa指針擂仍,該指針指向自己所屬的類,由Runtime負責創(chuàng)建對象熬甚。

類被定義為objc_class結構體逢渔,objc_class結構體繼承自objc_object,所以類也是對象乡括。在應用程序中肃廓,類對象只會被創(chuàng)建一份。在objc_class結構體中定義了對象的method list粟判、protocol亿昏、ivar list等,表示對象的行為档礁。

既然類是對象角钩,那類對象也是其他類的實例。所以Runtime中設計出了meta class呻澜,通過meta class來創(chuàng)建類對象递礼,所以類對象的isa指向對應的meta class。而meta class也是一個對象羹幸,所有元類的isa都指向其根元類脊髓,根原類的isa指針指向自己。通過這種設計栅受,isa的整體結構形成了一個閉環(huán)将硝。

// 精簡版定義
typedef struct objc_class *Class;

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
}

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

在對象的繼承體系中,類和元類都有各自的繼承體系屏镊,但它們都有共同的根父類NSObject依疼,而NSObject的父類指向nil。需要注意的是而芥,上圖中Root Class(Class)NSObject類對象律罢,而Root Class(Meta)NSObject的元類對象。

基礎定義

objc-private.h文件中棍丐,有一些項目中常用的基礎定義误辑,這是最新的objc-723中的定義沧踏,可以來看一下。

typedef struct objc_class *Class;
typedef struct objc_object *id;

typedef struct method_t *Method;
typedef struct ivar_t *Ivar;
typedef struct category_t *Category;
typedef struct property_t *objc_property_t;

IMP

RuntimeIMP本質(zhì)上就是一個函數(shù)指針巾钉,其定義如下翘狱。在IMP中有兩個默認的參數(shù)idSELid也就是方法中的self砰苍,這和objc_msgSend()函數(shù)傳遞的參數(shù)一樣盒蟆。

typedef void (*IMP)(void /* id, SEL, ... */ );

Runtime中提供了很多對于IMP操作的API,下面就是不分IMP相關的函數(shù)定義师骗。我們比較常見的是method_exchangeImplementations函數(shù)历等,Method Swizzling就是通過這個API實現(xiàn)的。

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

OBJC_EXPORT IMP _Nullable
class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
// ....

獲取IMP

通過定義在NSObject中的下面兩個方法辟癌,可以根據(jù)傳入的SEL獲取到對應的IMP寒屯。methodForSelector:方法不只實例對象可以調(diào)用,類對象也可以調(diào)用黍少。

- (IMP)methodForSelector:(SEL)aSelector;
+ (IMP)instanceMethodForSelector:(SEL)aSelector;

例如下面創(chuàng)建C函數(shù)指針用來接收IMP寡夹,獲取到IMP后可以手動調(diào)用IMP,在定義的C函數(shù)中需要加上兩個隱藏參數(shù)厂置。

void (*function) (id self, SEL _cmd, NSObject object);

function = (id self, SEL _cmd, NSObject object)[self methodForSelector:@selector(object:)];

function(instance, @selector(object:), [NSObject new]);

性能優(yōu)化

通過這些API可以進行一些優(yōu)化操作菩掏。如果遇到大量的方法執(zhí)行,可以通過Runtime獲取到IMP昵济,直接調(diào)用IMP實現(xiàn)優(yōu)化智绸。

TestObject *object = [[TestObject alloc] init];
void(*function)(id, SEL) = (void(*)(id, SEL))class_getMethodImplementation([TestObject class], @selector(testMethod));
function(object, @selector(testMethod));

在獲取和調(diào)用IMP的時候需要注意,每個方法默認都有兩個隱藏參數(shù)访忿,所以在函數(shù)聲明的時候需要加上這兩個隱藏參數(shù)瞧栗,調(diào)用的時候也需要把相應的對象和SEL傳進去,否則可能會導致Crash海铆。

IMP for block

Runtime還支持block方式的回調(diào)迹恐,我們可以通過RuntimeAPI,將原來的方法回調(diào)改為block的回調(diào)卧斟。

// 類定義
@interface TestObject : NSObject
- (void)testMethod:(NSString *)text;
@end

// 類實現(xiàn)
@implementation TestObject
- (void)testMethod:(NSString *)text {
    NSLog(@"testMethod : %@", text);
}
@end

// runtime
IMP function = imp_implementationWithBlock(^(id self, NSString *text) {
    NSLog(@"callback block : %@", text);
});
const char *types = sel_getName(@selector(testMethod:));
class_replaceMethod([TestObject class], @selector(testMethod:), function, types);
    
TestObject *object = [[TestObject alloc] init];
[object testMethod:@"lxz"];

// 輸出
callback block : lxz

Method

Method用來表示方法殴边,其包含SELIMP,下面可以看一下Method結構體的定義珍语。

typedef struct method_t *Method;

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

在運行過程中是這樣锤岸。

Method

Xcode進行編譯的時候,只會將XcodeCompile Sources.m聲明的方法編譯到Method List廊酣,而.h文件中聲明的方法對Method List沒有影響能耻。

Property

Runtime中定義了屬性的結構體赏枚,用來表示對象中定義的屬性亡驰。@property修飾符用來修飾屬性晓猛,修飾后的屬性為objc_property_t類型,其本質(zhì)是property_t結構體凡辱。其結構體定義如下戒职。

typedef struct property_t *objc_property_t;

struct property_t {
    const char *name;
    const char *attributes;
};

可以通過下面兩個函數(shù),分別獲取實例對象的屬性列表透乾,和協(xié)議的屬性列表洪燥。

objc_property_t * class_copyPropertyList(Class cls,unsigned int * outCount)
objc_property_t * protocol_copyPropertyList(Protocol * proto乳乌,unsigned int * outCount)

可以通過下面兩個方法捧韵,傳入指定的ClasspropertyName,獲取對應的objc_property_t屬性結構體汉操。

objc_property_t class_getProperty(Class cls再来,const char * name)
objc_property_t protocol_getProperty(Protocol * proto,const char * name磷瘤,BOOL isRequiredProperty芒篷,BOOL isInstanceProperty)

分析實例變量

對象間關系

OC中絕大多數(shù)類都是繼承自NSObject的(NSProxy例外),類與類之間都會存在繼承關系采缚。通過子類創(chuàng)建對象時针炉,繼承鏈中所有成員變量都會存在對象中。

例如下圖中扳抽,父類是UIViewController篡帕,具有一個view屬性。子類UserCenterViewController繼承自UIViewController贸呢,并定義了兩個新屬性赂苗。這時如果通過子類創(chuàng)建對象,就會同時包含著三個實例變量贮尉。

對象間關系

但是類的結構在編譯時都是固定的拌滋,如果想要修改類的結構需要重新編譯。如果上線后用戶安裝到設備上猜谚,新版本的iOS系統(tǒng)中更新了父類的結構败砂,也就是UIViewController的結構,為其加入了新的實例變量魏铅,這時用戶更新新的iOS系統(tǒng)后就會導致問題昌犹。

對象間關系

原來UIViewController的結構中增加了childViewControllers屬性,這時候和子類的內(nèi)存偏移就發(fā)生沖突了览芳。只不過斜姥,Runtime有檢測內(nèi)存沖突的機制,在類生成實例變量時,會判斷實例變量是否有地址沖突铸敏,如果發(fā)生沖突則調(diào)整對象的地址偏移缚忧,這樣就在運行時解決了地址沖突的問題。

內(nèi)存布局

類的本質(zhì)是結構體杈笔,在結構體中包含一些成員變量闪水,例如method listivar list等蒙具,這些都是結構體的一部分球榆。method、protocol禁筏、property的實現(xiàn)這些都可以放到類中持钉,所有對象調(diào)用同一份即可,但對象的成員變量不可以放在一起篱昔,因為每個對象的成員變量值都是不同的右钾。

創(chuàng)建實例對象時,會根據(jù)其對應的Class分配內(nèi)存旱爆,內(nèi)存構成是ivars+isa_t舀射。并且實例變量不只包含當前Classivars,也會包含其繼承鏈中的ivars怀伦。ivars的內(nèi)存布局在編譯時就已經(jīng)決定脆烟,運行時需要根據(jù)ivars內(nèi)存布局創(chuàng)建對象,所以Runtime不能動態(tài)修改ivars房待,會破壞已有內(nèi)存布局邢羔。

內(nèi)存布局

(上圖中,x表示地址對其后的空位)

以上圖為例桑孩,創(chuàng)建的對象中包含所屬類及其繼承者鏈中拜鹤,所有的成員變量。因為對象是結構體流椒,所以需要進行地址對其敏簿,一般OC對象的大小都是8的倍數(shù)。

也不是所有對象都不能動態(tài)修改ivars宣虾,如果是通過runtime動態(tài)創(chuàng)建的類惯裕,是可以修改ivars的。這個在后面會有講到绣硝。

ivar讀寫

實例變量的isa_t指針會指向其所屬的類蜻势,對象中并不會包含methodprotocol鹉胖、property握玛、ivar等信息够傍,這些信息在編譯時都保存在只讀結構體class_ro_t中。在class_ro_tivarsconst只讀的挠铲,在image loadcopyclass_rw_t中時冕屯,是不會copy ivars的,并且class_rw_t中并沒有定義ivars的字段市殷。

在訪問某個成員變量時,直接通過isa_t找到對應的objc_class刹衫,并通過其class_ro_tivar list做地址偏移醋寝,查找對應的對象內(nèi)存。正是由于這種方式带迟,所以對象的內(nèi)存地址是固定不可改變的音羞。

方法傳參

當調(diào)用實例變量的方法時,會通過objc_msgSend()發(fā)起調(diào)用仓犬,調(diào)用時會傳入selfSEL嗅绰。函數(shù)內(nèi)部通過isa在類的內(nèi)部查找方法列表對應的IMP,傳入對應的參數(shù)并發(fā)起調(diào)用搀继。如果調(diào)用的方法時涉及到當前對象的成員變量的訪問窘面,這時候就是在objc_msgSend()內(nèi)部,通過類的ivar list判斷地址偏移叽躯,取出ivar并傳入調(diào)用的IMP中的财边。

調(diào)用super的方式時則調(diào)用objc_msgSendSuper()函數(shù)實現(xiàn),調(diào)用時將實例變量的父類傳進去点骑。但是需要注意的是酣难,調(diào)用objc_msgSendSuper函數(shù)時傳入的對象,也是當前實例變量黑滴,所以是在向自己發(fā)送父類的消息憨募。具體可以看一下[self class][super class]的結果,結果應該都是一樣的袁辈。

在項目中經(jīng)常會通過[super xxx]的方式調(diào)用父類方法菜谣,這是因為需要先完成父類的操作,當然也可以不調(diào)用晚缩,視情況而定葛菇。以經(jīng)常見到的自定義init方法中,經(jīng)常會出現(xiàn)if (self = [super init])的調(diào)用橡羞,這是在完成自己的初始化之前先對父類進行初始化眯停,否則只初始化自身可能會存在問題。在調(diào)用[super init]時如果返回nil卿泽,則表示父類初始化失敗莺债,這時候初始化子類肯定會出現(xiàn)問題滋觉,所以需要做判斷。

參考資料

Apple Runtime Program Guild
維基百科-Objective-C
維基百科-Clang
維基百科-GCC(GNU)

蘋果開源代碼不建議去Github齐邦,上面的版本一般更新不及時椎侠,建議去蘋果的開源官網(wǎng)。
Apple Opensource


簡書由于排版的問題措拇,閱讀體驗并不好我纪,布局、圖片顯示丐吓、代碼等很多問題浅悉。所以建議到我Github上,下載Runtime PDF合集券犁。把所有Runtime文章總計九篇术健,都寫在這個PDF中,而且左側有目錄粘衬,方便閱讀荞估。

Runtime PDF

下載地址:Runtime PDF
麻煩各位大佬點個贊,謝謝稚新!??

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勘伺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子褂删,更是在濱河造成了極大的恐慌娇昙,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笤妙,死亡現(xiàn)場離奇詭異冒掌,居然都是意外死亡,警方通過查閱死者的電腦和手機蹲盘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門股毫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人召衔,你說我怎么就攤上這事铃诬。” “怎么了苍凛?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵趣席,是天一觀的道長。 經(jīng)常有香客問我醇蝴,道長宣肚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任悠栓,我火速辦了婚禮霉涨,結果婚禮上按价,老公的妹妹穿的比我還像新娘。我一直安慰自己笙瑟,他們只是感情好楼镐,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著往枷,像睡著了一般框产。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上错洁,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天秉宿,我揣著相機與錄音,去河邊找鬼墓臭。 笑死蘸鲸,一個胖子當著我的面吹牛妖谴,可吹牛的內(nèi)容都是我干的窿锉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼膝舅,長吁一口氣:“原來是場噩夢啊……” “哼嗡载!你這毒婦竟也來了?” 一聲冷哼從身側響起仍稀,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤洼滚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后技潘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遥巴,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年享幽,在試婚紗的時候發(fā)現(xiàn)自己被綠了铲掐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡值桩,死狀恐怖摆霉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情奔坟,我是刑警寧澤携栋,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站咳秉,受9級特大地震影響婉支,放射性物質(zhì)發(fā)生泄漏澜建。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望碍彭。 院中可真熱鬧庇忌,春花似錦皆疹、人聲如沸略就。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛔翅。三九已至山析,卻和暖如春盖腿,著一層夾襖步出監(jiān)牢的瞬間翩腐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留处渣,地道東北人罐栈。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓琅翻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棠众。 傳聞我的和親對象是個殘疾皇子闸拿,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344