iOS - 消息發(fā)送的完整流程

寫在前面

在OC里面,調(diào)用對象的某個方法其實就是給這個對象發(fā)送一個消息,這個過程我們把它分為三大階段蔬胯,分別為:消息發(fā)送階段、動態(tài)解析階段位他、消息轉(zhuǎn)發(fā)階段氛濒,本文將細細剖析這三個階段产场,但是在剖析這三大階段之前我們需要先回顧一下Class的結(jié)構(gòu)。

Class結(jié)構(gòu)

蘋果源碼最新下載地址請點擊:蘋果源碼
objc-runtime-new.h中可以看到objc_class結(jié)構(gòu)如下:

struct objc_object {
    Class isa;
};

struct objc_class : objc_object {
      Class superclass; 
      cache_t cache;  // 方法緩存
      class_data_bits_t bits; // 獲取具體類信息
      class_rw_t *data() const {
         return bits.data();
     }
    ...... 
};

從上面的結(jié)構(gòu)我們可以看到有一個類cache_t舞竿,這個類就是專門拿來做方法緩存相關(guān)的類京景,結(jié)構(gòu)如下:

struct cache_t {
    struct bucket_t *buckets();
    mask_t occupied();
    mask_t mask();
};

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

class_data_bits_t用于獲取具體的類信息,結(jié)構(gòu)如下:

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

// readWrite:可讀可寫
struct class_rw_t {
    uint32_t flags;
    uint32_t witness;
    Class firstSubclass;
    Class nextSiblingClass;
    class_rw_ext_t *ext;
    const class_ro_t *ro;
};

struct class_rw_ext_t {
  class_ro_t *ro;
  method_array_t methods;// 方法列表
  property_array_t properties; // 屬性列表
  protocol_array_t protocols; // 協(xié)議列表
  char *demangledName;
   uint32_t version;
}

// readOnly:只讀
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; 
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;  // 類名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  // 成員變量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

分析到這里骗奖,Class結(jié)構(gòu)我們已了解清楚确徙,接下來就是調(diào)用對象的方法來研究一下消息發(fā)送的完整流程。

消息發(fā)送階段

在OC里面执桌,調(diào)用對象的某個方法就是給這個對象發(fā)送一條消息鄙皇,這里我們新建一個Person類,以[person personRun]為例來看看消息發(fā)送階段的流程仰挣。
【iOS重學】方法緩存cache_t的分析這篇文章中我們主要分析了方法緩存育苟,建議大家先看一下緩存可以幫助我們理解接下來的流程。
我們知道OC中的方法調(diào)用其實就是轉(zhuǎn)成objc_msgSend()函數(shù)的調(diào)用(load方法除外)椎木,如下:

1.png

// 消息發(fā)送階段源碼跟讀順序
1. objc-msg-arm64 匯編文件
    ENTRY _objc_msgSend
    b.le LNilOrTagged
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    .macro CacheLookup
    CacheHit // 命中緩存
    MissLabelDynamic // 其實就是__objc_msgSend_uncached
    STATIC_ENTRY __objc_msgSend_uncached
    MethodTableLookup
    .macro MethodTableLookup
    bl _lookupImpOrForward

2. objc-runtime-new.mm 文件
    lookupImpOrForward
    getMethodNoSuper_nolock(curClass, sel)
    curClass = curClass->getSuperclass()
    cache_getImp(curClass, sel) // 從父類緩存里面查找
    log_and_fill_cache // 緩存方法到消息接收者這個類

消息發(fā)送的流程圖如下:


2.png

我們來驗證一下是否真的緩存了調(diào)用的方法:

未調(diào)用personRun時违柏,我們查一下在Person類的cache里面是否能找到personRun方法緩存:

Person *person = [[Person alloc] init];
mj_objc_class *personClass = (__bridge  mj_objc_class *)[Person class];
NSLog(@"%@ %p",NSStringFromSelector(@selector(personRun)), personClass->cache.imp(@selector(personRun)));

打印結(jié)果如下:

2022-04-10 13:11:30.367394+0800 RuntimeDemo[88049:12459843] personRun 0x0

結(jié)果分析:在cache并沒有找到personRun的IMP。
調(diào)用personRun之后香椎,我們查一下Person類的cache里面是否能找到personRun方法緩存:

Person *person = [[Person alloc] init];
[person personRun];
mj_objc_class *personClass = (__bridge  mj_objc_class *)[Person class];
NSLog(@"%@ %p",NSStringFromSelector(@selector(personRun)), personClass->cache.imp(@selector(personRun)));

打印結(jié)果如下:

2022-04-10 13:13:30.294687+0800 RuntimeDemo[88074:12461806] personRun 0x78cc0

結(jié)果分析:調(diào)用personRun之后漱竖,會把personRun緩存到方法緩存里面

動態(tài)方法解析階段

當?shù)谝浑A段【消息發(fā)送階段】沒有找到方法實現(xiàn)就會進入第二階段【動態(tài)方法解析階段】。

// 動態(tài)方法解析階段源碼跟讀順序
1. objc-runtime-new.mm 文件
  resolveMethod_locked
  resolveInstanceMethod 或 resolveClassMethod
  lookupImpOrNilTryCache
  _lookupImpTryCache
  lookupImpOrForward

動態(tài)方法解析的流程圖如下:


3.png

動態(tài)方法解析流程

根據(jù)+ (BOOL)resolveInstanceMethod:(SEL)sel (實例方法調(diào)用這個)+ (BOOL)resolveClassMethod:(SEL)sel(類方法調(diào)用這個)來做動態(tài)方法解析畜伐,然后重新走一遍消息發(fā)送的流程(從消息接受者的方法緩存里面開始繼續(xù)往下執(zhí)行)

動態(tài)方法解析代碼

- (void)otherRun {
  NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(personRun)) {
        Method otherMethod = class_getInstanceMethod(self, @selector(otherRun));
        IMP imp = class_getMethodImplementation(self, @selector(otherRun));
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        return  YES;
    }
    return [super resolveInstanceMethod:sel];
}

消息轉(zhuǎn)發(fā)階段

如果前面的兩個階段都沒有實現(xiàn)馍惹,就會繼續(xù)進入【消息轉(zhuǎn)發(fā)】的流程。

// 消息轉(zhuǎn)發(fā)階段的源碼跟讀順序
1. objc-msg-arm64 匯編文件
  forward_imp = _objc_msgForward_impcache
  STATIC_ENTRY __objc_msgForward_impcache
  b __objc_msgForward
  ENTRY __objc_msgForward
  ENTRY __objc_msgForward_stret
  __objc_forward_stret_handler
2. objc-runtime-new.mm 文件
  void *_objc_forward_stret_handler = (void *)objc_defaultForwardStretHandler;
3. CoreFoundation 框架
  __forwarding__ // 不開源

消息轉(zhuǎn)發(fā)的流程圖如下:


4.png

消息轉(zhuǎn)發(fā)流程

消息轉(zhuǎn)發(fā)流程也分為了兩步:
第一步:forwardingTargetForSelector:方法是指把響應(yīng)這個方法的對象轉(zhuǎn)發(fā)給其他的對象玛界,那么消息接受者就發(fā)生了變化万矾,會重新調(diào)用一遍objc_MsgSend(消息接受者,SEL)流程
第二步:forwardingTargetForSelector: 方法返回為nil慎框,繼續(xù)檢查methodSignatureForSelector:是否返回了一個方法簽名良狈,然后去執(zhí)行forwardInvocation:方法

消息轉(zhuǎn)發(fā)流程相關(guān)代碼實現(xiàn)

  1. 實例方法流程
// 第一步
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(personRun)) {
        return [[Student alloc] init]; // 這里返回的是你想把這個消息轉(zhuǎn)發(fā)給哪個對象
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 第二步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(personRun)) {
        // ??:這里的方法簽名的types不能隨便寫 因為這里的方法簽名決定了下一步的NSInvocation的返回值、參數(shù)類型等
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
     [anInvocation invokeWithTarget:[Student new]];
  // 在這個方法里可以做任何我們想做的事情
}

類方法流程

// 第一步
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(personTest)) {
        return [Student class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 第二步
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(personTest)) {
        return [Student methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[Student class]];
}

+ (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
}

關(guān)于NSInvocation

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
// 方法簽名
@property (readonly, retain) NSMethodSignature *methodSignature;

// retain所有參數(shù) 防止參數(shù)被dealloc
- (void)retainArguments;
// 參數(shù)是否都被retained
@property (readonly) BOOL argumentsRetained;
// 消息接收者
@property (nullable, assign) id target;
// 方法名
@property SEL selector;

// 獲取返回值
- (void)getReturnValue:(void *)retLoc;
// 設(shè)置返回值
- (void)setReturnValue:(void *)retLoc;
// 獲取idx的參數(shù)
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
// 設(shè)置idx的參數(shù)
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
// 調(diào)用
- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

大家有興趣的話可以去試試NSInvacation的使用笨枯。

最后

如果按照上面的三大流程都走完之后依然沒有找到相應(yīng)的方法實現(xiàn)薪丁,那這個調(diào)用最后就會調(diào)用doesNotRecognizeSelecto:拋出異常,如果錯誤請多多指教馅精,最后歡迎去我的個人技術(shù)博客逛逛严嗜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市洲敢,隨后出現(xiàn)的幾起案子漫玄,更是在濱河造成了極大的恐慌,老刑警劉巖压彭,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件睦优,死亡現(xiàn)場離奇詭異渗常,居然都是意外死亡,警方通過查閱死者的電腦和手機刨秆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門凳谦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忆畅,“玉大人衡未,你說我怎么就攤上這事〖铱” “怎么了缓醋?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绊诲。 經(jīng)常有香客問我送粱,道長,這世上最難降的妖魔是什么掂之? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任抗俄,我火速辦了婚禮,結(jié)果婚禮上世舰,老公的妹妹穿的比我還像新娘动雹。我一直安慰自己,他們只是感情好跟压,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布胰蝠。 她就那樣靜靜地躺著,像睡著了一般震蒋。 火紅的嫁衣襯著肌膚如雪茸塞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天查剖,我揣著相機與錄音钾虐,去河邊找鬼。 笑死笋庄,一個胖子當著我的面吹牛禾唁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播无切,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼荡短,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哆键?” 一聲冷哼從身側(cè)響起掘托,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎籍嘹,沒想到半個月后闪盔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弯院,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年泪掀,在試婚紗的時候發(fā)現(xiàn)自己被綠了听绳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡异赫,死狀恐怖椅挣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塔拳,我是刑警寧澤鼠证,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站靠抑,受9級特大地震影響量九,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颂碧,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一荠列、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧载城,春花似錦肌似、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至垦搬,卻和暖如春呼寸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背猴贰。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工对雪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人米绕。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓瑟捣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親栅干。 傳聞我的和親對象是個殘疾皇子迈套,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

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