【iOS】消息傳遞和消息轉發

文章目錄

    • 前言
    • 一、消息傳遞:objc_msgSend 的“查字典+遞歸找家長”流程
      • 1. 第一步:查“最近調用記錄”(方法緩存)—— 最快即快速查找!
      • 2. 第二步:翻“自己的字典”(類方法列表查找)—— 較慢!
      • 3. 第三步:遞歸“找家長”(父類方法列表)—— 最慢!
      • 總結:消息傳遞的“三級跳”
    • 二、消息轉發:快遞送不到時的“三級補救方案”
      • 階段 1:動態方法解析(自己加方法)——“我馬上補一個!”
      • 一、消息傳遞的本質:`objc_msgSend`的執行流程
        • 1. 快速查找:方法緩存(Method Cache)
        • 2. 類方法列表查找
        • 3. 緩存更新與結果返回
      • 二、消息轉發:當消息無法被處理時
        • 階段 1:動態方法解析(Dynamic Method Resolution)
      • 階段 2:快速轉發(轉交給其他對象)——“我找朋友幫忙!”
      • 階段 3:完整轉發(自定義處理流程)——“我自己寫個轉單系統!”
      • 關鍵注意點:
    • 三、底層原理:用匯編看 `objc_msgSend` 的“高效魔法”
    • 四、總結

前言

??Objective-C的消息傳遞與消息轉發是其動態特性的核心,基于運行時(Runtime)系統實現。本文將從底層機制出發,詳細解析消息傳遞的完整流程及消息轉發的三個關鍵階段,并結合源碼(如 objc/runtime)和匯編層面進行深入探討。

一、消息傳遞:objc_msgSend 的“查字典+遞歸找家長”流程

當調用 OC 對象的方法時(如 [obj doSomething]),編譯器會將其轉換為 C 函數調用:

objc_msgSend(obj, @selector(doSomething));

objc_msgSend是 OC 消息傳遞的核心函數,其本質是在接收者的類及其父類的方法列表中查找目標方法(SEL)的實現(IMP),并執行該實現。我們下面以OC 中調用方法(如 [dog 叫])為例子,本質是讓系統幫我們找到方法的實現代碼(IMP)并執行。這個過程由 objc_msgSend 函數完成,它的執行邏輯像“查字典+遞歸找家長”,分三步:

1. 第一步:查“最近調用記錄”(方法緩存)—— 最快即快速查找!

OC 運行時會為每個類維護一個方法緩存(methodCache_t,用于加速方法查找。緩存的結構是一個哈希表,鍵為 SEL(方法選擇子),值為 IMP(方法實現的指針)。

objc_msgSend首先檢查接收者類的緩存:

  • 若緩存中存在目標 SEL,直接跳轉到對應的 IMP執行(零成本緩存命中)。
  • 若緩存未命中,進入類方法列表查找

例如,每個類(如 Dog 類)都有一個 方法緩存(Method Cache),類似手機的“最近通話記錄”:

  • 作用:存“最近調用過的方法名(SEL)”和對應的“實現代碼地址(IMP)”,下次調用直接查緩存,無需重復計算。
  • 為什么快:哈希表結構,查找時間復雜度接近 O(1)(常數級)。

例子
你上周讓 dog 叫過 3 次,系統就把“叫”這個方法名(SEL)和對應的“汪汪汪”實現(IMP)記在 Dog 類的緩存里。這周再調用 [dog 叫]objc_msgSend 直接查緩存,秒級找到 IMP 并執行。

2. 第二步:翻“自己的字典”(類方法列表查找)—— 較慢!

若緩存未命中,objc_msgSend會從接收者的當前類開始,逐級向上遍歷繼承鏈(直到 NSObject或根類),在每個類的方法列表(method_list_t)中查找目標 SEL

每個類的方法列表存儲了該類自身定義的方法(不包括父類)。若當前類未找到,繼續查找其父類的方法列表,直到根類(如 NSObject)的父類為 nil,此時查找失敗。

例子
如果緩存里沒找到(比如第一次調用 [dog 叫]),objc_msgSend 會去當前類的“字典”(方法列表)里找。每個類的方法列表存著自己定義的所有方法(類似字典的“正文”)。 Dog 類的字典里有 等方法的定義(SEL 是“叫”,IMP 是“汪汪汪”的代碼)。objc_msgSend 遍歷這個字典,找到“叫”對應的 IMP,執行。

3. 第三步:遞歸“找家長”(父類方法列表)—— 最慢!

如果當前類的字典里也沒有(比如 Dog 類沒寫 方法),objc_msgSend 會去父類的字典里繼續找(類似“問爸爸有沒有這個詞的解釋”)。一直找到根類(如 NSObject)的父類(nil),若最終找到目標 SELIMP,則將該 SELIMP的映射寫入當前類的方法緩存(后續調用直接命中緩存),并跳轉到 IMP執行方法邏輯;還沒找到,就觸發消息轉發。

總結:消息傳遞的“三級跳”

調用 [dog 叫] → objc_msgSend 開始:

1.查 Dog 類的緩存 → 找到?直接執行(最快)。

2.沒找到 → 查 Dog 類的方法列表 → 找到?執行(較快)。

3.沒找到 → 遞歸查父類(Animal → NSObject)的方法列表 → 找到?執行(較慢)。

4.全沒找到 → 觸發消息轉發(兜底邏輯)。

二、消息轉發:快遞送不到時的“三級補救方案”

objc_msgSend遍歷完緩存、當前類、父類繼承鏈仍未找到目標 SELIMP,OC 運行時會觸發**消息轉發(Message Forwarding)**機制,嘗試通過一系列回調讓開發者有機會“補救”未處理的消息。消息轉發分為三個階段,按順序執行且不可逆(前一階段成功則后續階段不再觸發)。

階段 1:動態方法解析(自己加方法)——“我馬上補一個!”

Objective-C(OC)的消息傳遞與消息轉發是其動態特性的核心,基于運行時(Runtime)系統實現。本文將從底層機制出發,詳細解析消息傳遞的完整流程及消息轉發的三個關鍵階段,并結合源碼(如 objc/runtime)和匯編層面進行深入探討。

一、消息傳遞的本質:objc_msgSend的執行流程

當調用 OC 對象的方法時(如 [obj doSomething]),編譯器會將其轉換為 C 函數調用:

objc_msgSend(obj, @selector(doSomething));

objc_msgSend是 OC 消息傳遞的核心函數,其本質是在接收者的類及其父類的方法列表中查找目標方法(SEL)的實現(IMP),并執行該實現。整個過程可分為以下步驟:

1. 快速查找:方法緩存(Method Cache)

OC 運行時會為每個類維護一個方法緩存(methodCache_t,用于加速方法查找。緩存的結構是一個哈希表,鍵為 SEL(方法選擇子),值為 IMP(方法實現的指針)。

objc_msgSend首先檢查接收者類的緩存:

  • 若緩存中存在目標 SEL,直接跳轉到對應的 IMP執行(零成本緩存命中)。
  • 若緩存未命中,進入類方法列表查找。
2. 類方法列表查找

若緩存未命中,objc_msgSend會從接收者的當前類開始,逐級向上遍歷繼承鏈(直到 NSObject或根類),在每個類的方法列表(method_list_t)中查找目標 SEL

每個類的方法列表存儲了該類自身定義的方法(不包括父類)。若當前類未找到,繼續查找其父類的方法列表,直到根類(如 NSObject)的父類為 nil,此時查找失敗。

3. 緩存更新與結果返回

若最終找到目標 SELIMP,則將該 SELIMP的映射寫入當前類的方法緩存(后續調用直接命中緩存),并跳轉到 IMP執行方法邏輯。

二、消息轉發:當消息無法被處理時

objc_msgSend遍歷完繼承鏈仍未找到目標 SELIMP,OC 運行時會觸發**消息轉發(Message Forwarding)**機制,嘗試通過一系列回調讓開發者有機會“補救”未處理的消息。消息轉發分為三個階段,按順序執行且不可逆(前一階段成功則后續階段不再觸發)。

階段 1:動態方法解析(Dynamic Method Resolution)

運行時首先調用類的類方法 +resolveInstanceMethod:(針對實例方法)或 +resolveClassMethod:(針對類方法),允許開發者動態添加方法實現。系統先問當前類:“你能自己寫一個這個方法嗎?”(調用 +resolveInstanceMethod:)。這時候我們可以用 class_addMethod 動態添加方法實現,相當于“臨時補字典條目”。

例子
我們發現 Dog 類忘記實現 方法,于是在 +resolveInstanceMethod: 里補上:

+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(叫)) { // 動態添加方法:SEL 是“叫”,IMP 是“汪汪汪”的代碼class_addMethod(self, sel, (IMP)叫的實現, "v@:"); return YES; // 返回YES,表示消息已經被處理,即告訴系統:“我自己解決了!”,objc_msgSend會重新嘗試發送消息(此時緩存已更新)。}return [super resolveInstanceMethod:sel];
}// 方法實現(IMP)
void 叫的實現(id self, SEL _cmd) {NSLog(@"汪汪汪!");
}

如果成功,系統會把新方法加入緩存,下次調用直接命中。

階段 2:快速轉發(轉交給其他對象)——“我找朋友幫忙!”

如果動態解析失敗(+resolveInstanceMethod:返回 NO),運行時會調用實例方法 -forwardingTargetForSelector:,允許開發者指定一個備用接收者(Forwarding Target),將消息轉發給該對象處理。動態解析失敗比如你不想自己加方法,系統問:“你能找個朋友(其他對象)幫我處理嗎?”(調用 -forwardingTargetForSelector:)。你返回一個能處理該消息的對象,相當于“把快遞轉交給鄰居”。

例子
Dog 類發現自己不會“叫”,但它的朋友 Cat 類會,于是返回 Cat 的實例:

- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(叫)) {return [Cat new]; // 找 Cat 幫忙}return [super forwardingTargetForSelector:aSelector];
}

若返回非 nil對象,消息會被發送給該對象(相當于“代理”模式);若返回 nil,進入下一階段。上述代碼中系統會把 [dog 叫] 轉發給 Cat 對象,如果 Cat 會“叫”,消息就被正確處理。

階段 3:完整轉發(自定義處理流程)——“我自己寫個轉單系統!”

若前兩階段均失敗(一般是快速轉發未提供備用接收者),運行時會觸發完整的消息轉發流程,核心是構造一個 NSInvocation對象封裝消息信息,并調用 -forwardInvocation:方法讓開發者自定義處理。就例如你找不到能幫忙的對象,系統啟動“完整轉發”:把消息(誰發的、方法名、參數)打包成 NSInvocation 對象,調用 -forwardInvocation: 讓你自定義處理。你需要自己決定如何處理這個消息(比如轉給其他對象、修改參數、記錄日志)。

例子
你重寫 -forwardInvocation:,把消息轉給 Cat,并記錄日志:

- (void)forwardInvocation:(NSInvocation *)invocation {// 1. 獲取原消息的信息(方法名、參數)SEL sel = invocation.selector;id target = [Cat new]; // 臨時目標// 2. 修改消息目標為 Cat[invocation setTarget:target]; // 改成轉給 Cat[invocation invoke]; // 重新發送消息// 3. (可選)獲取返回值并處理id result;[invocation getReturnValue:&result];NSLog(@"轉發成功,結果是:%@", result);
}// 必須實現:獲取目標方法的簽名(NSMethodSignature),用于描述方法的參數、返回值類型等信息。若未實現此方法,會直接拋出 unrecognized selector異常。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if (aSelector == @selector(叫)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 無參數,返回 void}return [super methodSignatureForSelector:aSelector];
}

-methodSignatureForSelector:未實現或返回 nil,運行時會直接拋出 NSInvalidArgumentExceptionunrecognized selector sent to instance)。

關鍵注意點:

  • 如果 -methodSignatureForSelector: 沒實現或返回 nil,系統會直接拋出 unrecognized selector sent to instance 崩潰(常見錯誤)。

三、底層原理:用匯編看 objc_msgSend 的“高效魔法”

objc_msgSend 是用 ARM64 匯編 寫的,核心邏輯用幾行偽代碼概括:

objc_msgSend:// 1. 檢查接收者是否為 nil(OC 允許向 nil 發消息)cbz x0, LReturnNil  // 如果 receiver 是 nil,直接返回 0// 2. 查緩存:從 receiver 的 isa 指針找到類,然后在緩存里找 SELldr x1, [x0]        // x1 = receiver->isa(類的地址)CacheLookup         // 匯編指令:在類的緩存里查 SEL 對應的 IMP// 3. 緩存命中:直接跳轉到 IMP 執行br x2               // x2 是緩存的 IMP 地址,跳轉執行LReturnNil:mov x0, #0          // 返回 0(對應 nil 消息的處理)ret

為什么快:緩存查找是匯編級別的優化,幾乎無額外開銷;方法列表查找是遞歸遍歷,但僅在緩存未命中時觸發。

四、總結

OC 的消息傳遞與轉發機制,本質是 “運行時動態性” 的體現:

  • 高效性:通過緩存和方法列表的層級查找,平衡了“首次調用”和“重復調用”的性能。
  • 靈活性:消息轉發的三階段設計,允許開發者在運行時動態修復未處理的方法(如 KVO、動態代理)。

一句話總結objc_msgSend 像一個“智能快遞員”,先查最近記錄(緩存),再翻自己家抽屜(方法列表),最后遞歸問家長(父類);找不到時,系統給你三次“補救機會”(動態解析→快速轉發→完整轉發),確保消息“不輕易丟失”。**

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/89632.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/89632.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/89632.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MySQL查詢優化與事務實戰指南

本節用到的員工信息管理表結構放到資源中,需要的同學自取。本節內容以此表為示例: 面試題:innodb與myisam的區別。 外鍵,事務 特性InnoDBMyISAM事務支持支持不支持外鍵支持不支持鎖粒度行級鎖表級鎖索引結構聚簇索引非聚簇索引崩…

Windows 10/11 磁盤清理操作指南:徹底解決系統盤空間不足問題

🧑 博主簡介:CSDN博客專家、CSDN平臺優質創作者,高級開發工程師,數學專業,10年以上C/C, C#,Java等多種編程語言開發經驗,擁有高級工程師證書;擅長C/C、C#等開發語言,熟悉Java常用開發…

b-up:Enzo_Mi:深度學習基礎知識

1.最近鄰差值(Neareast Neighbor Interpolation) 插值算法 | 最近鄰插值法_嗶哩嗶哩_bilibili 上圖中最后一行,第一個圖像,因為目標像素(放大后,位于第1行第0列的像素)距離它最近的…

微信小程序商品結算功能

整體結算流程概述微信小程序的商品結算涉及前端交互、API調用和數據管理。典型流程包括:用戶交互:用戶選擇商品、填寫地址和時間。數據獲取:從小程序緩存或后端服務器獲取訂單信息。邏輯處理:驗證參數、應用紅包折扣。提交訂單&am…

2025年7月份最新一區算法——向光生長算法

注:該算法已按照智能優化算法APP標準格式進行整改,可直接集成到APP中,方便大家與自己的算法進行對比。(近期智能優化算法APP將會迎來超級大更新!請時刻保持關注哦!)向光生長算法(Pho…

腳手架新建Vue2/Vue3項目時,項目文件內容的區別

一. package.json vue版本號不同vue2中會多一個依賴:vue-template-compiler,作用是預編譯Vue2模板為渲染函數,減少運行時開銷。vue-template-compiler與vue版本要保持一致,否則會報錯。eslintConfig中的extends不同 eslintConfig…

微信小程序入門實例_____從零開始 開發一個每天記賬的微信小程序

在前面的微信小程序實例中我們開發了體重記錄等實用小程序,今天來嘗試一個和生活消費緊密相關的 ——“每日記賬小程序”。它能幫你隨時記錄收支情況,讓每一筆花費都清晰可查。下面就跟著步驟,一步步構建這個小程序。?體驗一個開發者的快樂。…

2026python實戰——如何利用海外代理ip爬取海外數據

家人們!隨著跨境電商的發展,是不是越來越多的小伙伴們也開始搞海外的數據分析了?不過雖然我們已經整天爬蟲、數據采集打交道了,但一到海外數據,還是有不少人掉進坑里。你們是不是也遇到過以下情況:花了一堆…

Spring Boot啟動原理:從main方法到內嵌Tomcat的全過程

Spring Boot的啟動過程是一個精心設計的自動化流程,下面我將詳細闡述從main方法開始到內嵌Tomcat啟動的全過程。 1. 入口:main方法 一切始于一個簡單的main方法: SpringBootApplication public class MyApplication {public static void m…

小白學Python,網絡爬蟲篇(1)——requests庫

目錄 一、網絡爬蟲的介紹 1.網絡爬蟲庫 2.robots.txt 規則 二、requests 庫和網頁源代碼 1.requests 庫的安裝 2.網頁源代碼 三、獲取網頁資源 1.get () 函數 (1)get() 搜索信息 (2)get() 添加信息 2.返回 Response 對象…

平板可以用來辦公嗎?從文檔處理到創意創作的全面測評

在快節奏的現代職場,一個核心疑問始終縈繞在追求效率的職場人心中:平板電腦,這個輕薄便攜的設備,真的能替代筆記本電腦,成為值得信賴的辦公伙伴嗎? 答案并非簡單的“是”或“否”,而是一個充滿潛…

docker gitlab 備份 恢復 版本升級(16.1.1到18.2.0)

docker 啟動 # 在線 docker pull gitlab/gitlab-ce:latest # 離線 docker save -o gitlab-ce-latest.tar gitlab/gitlab-ce:latest docker load -i gitlab-ce-latest.tardocker run --detach \--publish 8021:80 --publish 8023:22 \ --name gitlab_test \--restart always \-…

web3 區塊鏈技術與用

#53 敲點算法題 瑞吉外賣day4 調整心態 睡眠 及精神 web3 以下是應北京大學肖臻老師《區塊鏈技術與用》公開課的完整教學大綱,綜合課程內容、技術模塊及前沿擴展,分為核心章節與專題拓展兩部分,引用自公開課資料及學員筆記。 &#x1f4…

Redis1:高并發與微服務中的鍵值存儲利器

redis中存儲的數據格式為鍵值對(Key,Value)在高并發的項目和微服務的項目會頻繁的用到redisNoSQL型數據庫1.初始Redis1.1認識NoSQLSQL:structure query language關系型數據庫結構化:有固定格式要求(表關系,…

/字符串/

字符串 個人模板 5. 最長回文子串 93. 復原 IP 地址 43. 字符串相乘 227. 基本計算器 II

我的開發日志:隨機數小程序

文章目錄前言UI設計代碼前言 為什么我要設計這個程序呢?因為我要用,懶得在網上下載了,于是干脆寫了一個。 UI設計 UI是我凹出來的,你們要使用,直接新建一個UI.ui文件,然后把下面的東西輸進去就可以了。 …

《Oracle SQL:使用 RTRIM 和 TO_CHAR 函數格式化數字并移除多余小數點》

select RTRIM(to_char(1222.11123344,fm9999990.9999),.) from dual 這條 SQL 語句主要用于對數字進行格式化處理,并移除格式化結果右側多余的小數點。下面將詳細拆解該語句的執行過程和各部分作用。語句詳細拆解1. to_char(1222.11123344,fm9999990.9999)函數功能&…

「Java案例」方法重裝求不同類型數的立方

利用方法重裝實現不同類型數值的立方計算 立方計算方法的重載實現 編寫一個程序,要求編寫重載方法xxx cube(xxx value)實現對不同類型數值計算立方。 # 源文件保存為“CubeCalculator.java” public class CubeCalculator {public static void main(String[] args) {// 測試…

API 接口開發與接入實踐:自動化采集淘寶商品數據

在電商數據分析、價格監控等場景中,自動化采集淘寶商品數據具有重要價值。本文將詳細介紹如何通過 API 接口開發實現淘寶商品數據的自動化采集,包含完整的技術方案和代碼實現。 一、淘寶 API 接入基礎 1. 接入流程概述 注冊淘寶賬號獲取 ApiKey 和 Ap…

python-pptx 的layout 布局

一、布局基礎概念 在 PowerPoint 中,布局(Layout) 決定了幻燈片的占位符(如標題、內容、圖片等)的排列方式。python-pptx 提供了對布局的編程控制。二、默認布局類型及索引 通過 prs.slide_layouts[index] 訪問&#x…