OC-Runtime-Class結(jié)構(gòu)和OC消息機(jī)制

OC - Runtime - Class 結(jié)構(gòu) 和 OC 消息機(jī)制

Runtime 源碼中 Class 結(jié)構(gòu)如下:

// Class 其實(shí)就是一個(gè) struct objc_class *
typedef struct objc_class *Class;

// struct objc_class 繼承 objc_object
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

// objc_object 結(jié)構(gòu)如下
struct objc_object {
    isa_t isa;
}

所以Class本身結(jié)構(gòu)如下:

struct objc_class {
    isa_t isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

Class 內(nèi)部 (bits & FAST_DATA_MASK) 可以得到 (class_rw_t *) 類型恤左,
其內(nèi)部結(jié)構(gòu)如下

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

其中結(jié)構(gòu) class_ro_t 結(jié)構(gòu)如下:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    // strong修飾的ivars
    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    
    // weak修飾的ivars
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

這幾個(gè)類型關(guān)系如下圖

Class

class_rw_t 和 class_ro_t

class_rw_t 里面的 methods辐啄、properties、protocols 是二維數(shù)組屈溉、是可讀可寫的礼殊,包含了類的初始內(nèi)容秦躯,分類的內(nèi)容等


class_rw_t

class_ro_t 里面同樣也包含了 baseMethodList砌函、baseProtocols帜消、baseProperties 信息猪勇,他們是一維數(shù)組设褐,是不可讀寫的,他們包含的是 Class 最原始的方法列表信息


class_ro_t

method_t

在 class_rw_t 和 class_ro_t 中都包含 class 的各種方法協(xié)議信息泣刹,以方法為例助析,其類型為 method_t.

  • method_t 是對方法/函數(shù)的封裝,結(jié)構(gòu)如下
struct method_t {
    SEL name;               // 函數(shù)名
    const char *types;      // 編碼(返回值類型椅您,參數(shù)類型)
    IMP imp;                // 指向函數(shù)的指針(函數(shù)地址)
}
  • IMP 是指向函數(shù)具體實(shí)現(xiàn)的指針外冀。定義id (*IMP)(id, SEL, ...)

在 OC 底層的方法調(diào)用掀泳,都會(huì)轉(zhuǎn)化為 C 語言的函數(shù)調(diào)用雪隧,所有的函數(shù)都有兩個(gè)默認(rèn)的參數(shù) self、SEL 其他參數(shù)才是用戶定義方法設(shè)置的參數(shù)员舵,這也是在對象方法內(nèi)我們能直接使用 self 的原因脑沿。

  • SEL 代表方法、函數(shù)名,定義typedef struct objc_selector *SEL;马僻,通常叫做選擇器庄拇,底層結(jié)構(gòu)和 char* 類似【碌耍可以通過 @selector()sel_registerName 獲得SEL

type encode

runtime 會(huì)對函數(shù)的返回值參數(shù)進(jìn)行編碼措近。具體來說溶弟,OC 對象的方法,在Runtime底層會(huì)轉(zhuǎn)化為id (*IMP)(id, SEL, ...)類型指針熄诡。

以get方法為例- (NSString *)name; 其編碼為@16@0:8
以無返回值方法為例- (void)name;其編碼為v16@0:8

以上兩個(gè)方法對應(yīng)的轉(zhuǎn)換為IMP函數(shù):NSString* name(id self, SEL _cmd)void name(id self, SEL _cmd) 其編碼都是一樣的可很。

編碼的規(guī)則示例如下:

type encode

官方說明: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html

方法緩存 cache_t

Class 內(nèi)部結(jié)構(gòu)有個(gè)方法緩存cache_t, 用散列表來緩存曾經(jīng)調(diào)用過的方法诗力,可以提高查找速度凰浮。

cache_t

散列表有一個(gè)起始長度值,會(huì)使用使用到的@selector(sel) & _mask來緩存具體的方法地址苇本。

當(dāng)列表的容量不夠的時(shí)候會(huì)擴(kuò)容袜茧,直接擴(kuò)大為原來的2倍。

objc_msgSend執(zhí)行流程

消息發(fā)送機(jī)制的執(zhí)行流程主要分為3大階段

  1. 消息發(fā)送階段 -> 對象方法在Class中找瓣窄,類方法在 MetaClass 中找笛厦,一層層往上找
  2. 動(dòng)態(tài)方法解析 -> 系統(tǒng)找不到方法,會(huì)給一個(gè)機(jī)會(huì)添加動(dòng)態(tài)方法
  3. 消息轉(zhuǎn)發(fā)俺夕,如果此步再?zèng)]有操作裳凸,就會(huì)報(bào)錯(cuò) unrecognized selector sent to instance

objc_msgSend 函數(shù)在 Runtime 的代碼中是直接使用混編語言實(shí)現(xiàn)的,可以從源碼中查到其查找方法流程: 緩存 -> 自己的方法列表 -> 遍歷父類的緩存 & 方法列表劝贸。整個(gè)流程如果都沒有姨谷,就進(jìn)入查找動(dòng)態(tài)方法階段。

impLookupOrForword

如果都沒找到映九,進(jìn)入動(dòng)態(tài)方法解析,動(dòng)態(tài)方法解析流程如下梦湘。動(dòng)態(tài)方法會(huì)根據(jù) + (BOOL)resolveInstanceMethod:(SEL)sel; 查看內(nèi)部有沒有給對應(yīng)的 selector 動(dòng)態(tài)添加方法實(shí)現(xiàn),如果有就重新走消息發(fā)送流程件甥。否則進(jìn)入下一階段:消息轉(zhuǎn)發(fā)捌议。

動(dòng)態(tài)解析

如果動(dòng)態(tài)解析還是什么都沒有操作,就會(huì)進(jìn)入消息轉(zhuǎn)發(fā)階段引有。消息轉(zhuǎn)發(fā)階段流程如下瓣颅,這里是閉源的無法從源碼上查看,但是可以從代碼角度驗(yàn)證流程譬正。


消息轉(zhuǎn)發(fā)
#pragma mark -  消息轉(zhuǎn)發(fā)階段 - 1
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [NSObject objectSpecifier];
    }
    return [super forwardingTargetForSelector:aSelector];
}


#pragma mark -  消息轉(zhuǎn)發(fā)階段 - 2
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    // 這里如果沒有操作就會(huì)走找不到方法宫补。
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    // 對 invocation 做需要做的事情
}

面試題

  • 簡述 OC 對象的消息機(jī)制。
1. OC 中的方法調(diào)用其實(shí)都是轉(zhuǎn)化成了 objc_msgSend 函數(shù)的調(diào)用导帝,給receiver發(fā)送一條消息
2. objc_msgSend 函數(shù)內(nèi)部有3大階段守谓。
    1. 消息查找階段
    2. 動(dòng)態(tài)消息解析階段
    3. 消息轉(zhuǎn)發(fā)階段
  • 消息轉(zhuǎn)發(fā)機(jī)制流程
1. 調(diào)用 - (id)forwardingTargetForSelector:(SEL)aSelector 方法,如果有實(shí)現(xiàn)您单,且返回轉(zhuǎn)發(fā)的對象斋荞,就將消息轉(zhuǎn)發(fā)給該對象。反之虐秦,進(jìn)入第二步驟
2. 調(diào)用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 方法平酿,如過有針對 aSelector 的方法簽名就繼續(xù)調(diào)用 - (void)forwardInvocation:(NSInvocation *)anInvocation 方法去實(shí)現(xiàn)任何自己想實(shí)現(xiàn)的邏輯凤优。反之進(jìn)入第三步
3. 調(diào)用 doesNotRecognizeedSelector 方法,向外拋出異常蜈彼,經(jīng)典的 unrecognized selector sent to instance 錯(cuò)誤
  • Runtime 是什么筑辨,項(xiàng)目中用到過嗎?
是什么:
OC 是一門動(dòng)態(tài)性比較強(qiáng)的語言幸逆,允許很多操作推遲到程序運(yùn)行時(shí)候再進(jìn)行棍辕。
OC 的動(dòng)態(tài)性就是由 Runtime 來支撐和實(shí)現(xiàn)的,Runtime 是一套C語言API还绘,封裝了很多動(dòng)態(tài)性相關(guān)的函數(shù)楚昭。
平時(shí)編寫的OC代碼,底層都是轉(zhuǎn)換成了 Runtime API 進(jìn)行調(diào)用拍顷。

具體應(yīng)用:
1. 利用關(guān)聯(lián)對象 (AssociatedObject) 給分類添加屬性抚太,創(chuàng)建便利分類,如按鈕直接通過block處理事件
2. 遍歷類的成員變量 (修改textfield的站位文字顏色昔案、字典轉(zhuǎn)模型尿贫、自動(dòng)歸檔等)
3. 交換方法實(shí)現(xiàn)、例如交換系統(tǒng)方法踏揣,eg:自己寫過的一個(gè)監(jiān)聽工具庆亡,hook系統(tǒng)實(shí)現(xiàn),添加自己邏輯
4. 利用消息轉(zhuǎn)發(fā)機(jī)制解決方法找不到的問題

下面代碼輸出什么?

@implementation Son : Father
- (id)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
    return self;
}
@end

// 答案: 都是 Son
// 原因: super 是編譯器特性括享,會(huì)被轉(zhuǎn)成 objc_msgSendSuper 函數(shù)搂根,其真實(shí)接收者仍是 self,class 方法是在 NSObject 中實(shí)現(xiàn)的,最終在消息查找會(huì)走到基類的 class 方法铃辖,返回的是消息接收者的類型剩愧,即 self 的類型。
  • super 關(guān)鍵字詳解
一個(gè)Person 類娇斩,其 init 方法如下:
- (instancetype)init
{
    self = [super init];
    return self;
}

// 使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person.m -o Person-arm64.cpp 轉(zhuǎn)化為 C++ 代碼如下

static instancetype _I_Person_init(Person * self, SEL _cmd) {
    self = ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"));
    return self;
}

// [super init]; 簡化如下
struct __rw_objc_super { 
    struct objc_object *object;        // 真正的消息接受者 
    struct objc_object *superClass;    // 接收者父類仁卷,作為第一個(gè)查找的類。
};

// __rw_objc_super 結(jié)構(gòu)體
__rw_objc_super objc_super = (__rw_objc_super){
    (id)self,
    (id)class_getSuperclass(objc_getClass("Person"))
};
    
// 實(shí)際上轉(zhuǎn)化為此類型函數(shù)指針 (Person *(*)(__rw_objc_super *, SEL))
objc_msgSendSuper(objc_super, sel_registerName("init"));

super 關(guān)鍵字總結(jié)

  1. super 關(guān)鍵字會(huì)轉(zhuǎn)化為 objc_msgSendSuper()犬第,函數(shù).
  2. 其中會(huì)指定锦积,消息接受者,從哪個(gè)類開始查找歉嗓,和要發(fā)送的消息丰介。
  3. super 就是要在當(dāng)前對象的父類開始查找消息的實(shí)現(xiàn)。

--- end ---

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市哮幢,隨后出現(xiàn)的幾起案子带膀,更是在濱河造成了極大的恐慌,老刑警劉巖橙垢,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垛叨,死亡現(xiàn)場離奇詭異,居然都是意外死亡柜某,警方通過查閱死者的電腦和手機(jī)嗽元,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來莺琳,“玉大人还棱,你說我怎么就攤上這事载慈〔训龋” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵办铡,是天一觀的道長辞做。 經(jīng)常有香客問我,道長寡具,這世上最難降的妖魔是什么秤茅? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮童叠,結(jié)果婚禮上框喳,老公的妹妹穿的比我還像新娘。我一直安慰自己厦坛,他們只是感情好五垮,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杜秸,像睡著了一般放仗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上撬碟,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天诞挨,我揣著相機(jī)與錄音,去河邊找鬼呢蛤。 笑死惶傻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的其障。 我是一名探鬼主播银室,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了粮揉?” 一聲冷哼從身側(cè)響起巡李,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扶认,沒想到半個(gè)月后侨拦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辐宾,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年狱从,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叠纹。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡季研,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出誉察,到底是詐尸還是另有隱情与涡,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布持偏,位于F島的核電站驼卖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鸿秆。R本人自食惡果不足惜酌畜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卿叽。 院中可真熱鬧桥胞,春花似錦、人聲如沸考婴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蕉扮。三九已至整胃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喳钟,已是汗流浹背屁使。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奔则,地道東北人蛮寂。 一個(gè)月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像易茬,于是被迫代替她去往敵國和親酬蹋。 傳聞我的和親對象是個(gè)殘疾皇子及老,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容