深入淺出理解消息的傳遞和轉發機制

来源:http://www.cnblogs.com/zhanggui/archive/2017/10/25/7731394.html
-Advertisement-
Play Games

前言 在面試過程中你也許會被問到消息轉發機制。這篇文章就是對消息的轉發機制進行一個梳理。主要包括什麼是消息、靜態綁定/動態綁定、消息的傳遞和消息的轉發。接下來開發進入正題。 消息的解釋 在其他語言裡面,我們可以用一個類去調用某個方法,在OC裡面,這個方法就是消息。某個類調用一個方法就是向這個類發送一 ...


前言

在面試過程中你也許會被問到消息轉發機制。這篇文章就是對消息的轉發機制進行一個梳理。主要包括什麼是消息、靜態綁定/動態綁定、消息的傳遞和消息的轉發。接下來開發進入正題。

消息的解釋

在其他語言裡面,我們可以用一個類去調用某個方法,在OC裡面,這個方法就是消息。某個類調用一個方法就是向這個類發送一條消息。舉個例子:

People *zhangSan = [[People alloc] init];
People *lisi = [[People alloc] init];
[zhangSan beFriendWith:lisi];

我們有個People的類,zhangSan這個實例發送了一條beFriendWith:的消息。你也許還看過這種調用方式:

[zhangSan performSelector:@selector(beFriendWith:) withObject:lisi];

其目和上面的一樣,都是向zhangSan發送了一條beFriendWith:的消息,傳人的參數都是lisi。
這裡簡單介紹一下SEL和IMP:

SEL:類成員方法的指針,但和C的函數指針還不一樣,函數指針直接保存了方法的地址,但是SEL只是方法編號。
IMP:函數指針,保存了方法地址。

我們叫@selector(beFriendWith:)為消息的選擇子或者選擇器。(A selector identifying the message to send)

靜態綁定/動態綁定

所謂靜態綁定,就是在編譯期就能決定運行時所調用的函數,例如:

void printHello() {
    printf("Hello,world!\n");
}
void printGoodBye() {
    printf("Goodbye,world!\n");
}

void doTheThing(int type) {
    if (type == 0) {
        printHello();
    }else {
        printGoodBye();
    }
}

所謂動態綁定,就是在運行期才能確定調用函數:

void printHello() {
    printf("Hello,world!\n");
}
void printGoodBye() {
    printf("Goodbye,world!\n");
}
void doTheThing(int type) {
    void (*fnc)(void);
    if (type == 0) {
        fnc = printHello;
    }else {
        fnc = printGoodBye;
    }
    fnc();
}

在OC中,對象發送消息,就會使用動態綁定機制來決定需要調用的方法。其實底層都是C語言實現的函數,當對象收到消息後,究竟調用那個方法完全決定於運行期,甚至你也可以直接在運行時改變方法,這些特性都使OC成為一門動態語言。

消息的傳遞

先看一下一條簡單的消息:

id returnValue = [someObject messageName:parameter];

其中:
someObject叫做接收者(receiver)。
messageName叫做選擇器(selector)
選擇器和參數合起來成為消息(message)
當編譯器看到這條消息,就會轉換成一條標準的C函數:objc_msgSend,此時會變成:

id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

objc_msgSend可以在objc裡面的message.h中看到:
objc_msgSend
根據官方註釋可以看到:

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

它的作用是向一個實例類發送一個帶有簡單返回值的message。是一個參數個數不定的函數。當遇到一個方法調用,編譯器會生成一個objc_msgSend的調用,有:objc_msgSend_stret、objc_msgSendSuper或者是objc_msgSendSuper_stret。發送個父類的message會使用objc_msgSendSuper,其他的消息會使用objc_msgSend。如果方法的返回值是一個結構體(structures),那麼就會使用objc_msgSendSuper_stret或者objc_msgSend_stret。
第一個參數是:指向接收該消息的類的實例的指針
第二個參數是:要處理的消息的selector。
其他的就是要傳入的參數。
這樣消息派發系統就在接收者所屬類中查找器方法列表,如果找到和選擇器名稱相符的方法就跳轉其實現代碼,如果找不到,就再起父類找,等找到合適的方法在跳轉到實現代碼。這裡跳轉到實現代碼這一操作利用了尾遞歸優化
如果該消息無法被該類或者其父類解讀,就會開始進行消息轉發。

理解消息轉發機制(message forwarding)
動態方法解析

不要把消息轉發機制想象得很難,其實看過下麵的你就會發現,沒有那麼難。
我們有的時候會遇到這樣的crash:
crash
我們都知道crash的原因是People沒有gotoschool這個方法,但是你調用了該方法,所以會產生NSInvalidArgumentException,reason:

-[People gotoschool]: unrecognized selector sent to instance 0x1d4201780'

接下來讓我們看看從發送消息到此crash的過程。前面消息的傳遞沒有成功找到實現,所以會走到消息轉發裡面,我先在People類裡面實現了這樣一個方法:

void gotoSchool(id self,SEL _cmd,id value) {
    printf("go to school");
}
//對象在收到無法解讀的消息後,首先將調用所屬類的該方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString isEqualToString:@"gotoschool"]) {
        class_addMethod(self, sel, (IMP)gotoSchool, "@@:");
    }
    return [super resolveInstanceMethod:sel];
}

然後再次運行程式,你會發現沒有crash了,而且順利列印出來"go to school"。
這個是什麼個情況呢?先看看這個方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
+ (BOOL)resolveClassMethod:(SEL)sel OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

這個方法是objc裡面NSObject.h裡面的方法。從字面理解就是處理實例方法(處理類方法)。下麵是對其的介紹:
resolveInstanceMethod/forwardingTargetForSelector:
它的作用就是給一個實例方法(給定的選擇器)動態提供一個實現。註釋也提供了一個demo告訴我們如何動態添加實現。
也就是說當消息傳遞無法處理的時候,首先會看一下所屬類,是否能動態添加方法,以處理當前未知的選擇子。這個過程叫做“動態方法解析”(dynamic method resolution)。
這裡我在動態方法解析這裡動態添加了實現,然後程式就不會崩潰啦。
如果是類方法,就調用resolveClassMethod:方法進行操作,和上面的resolveInstanceMethod一樣的處理方式。
這裡還用到了calss_addMethod,後面會單獨寫篇博客對其介紹。感興趣的可以先自行查看API。

備援接收者

當動態方法解析沒有實現或者無法處理的時候,就會執行

- (id)forwardingTargetForSelector:(SEL)aSelector OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

這個方法也是objc裡面NSObject.h裡面的方法。我對People進行瞭如下處理:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorString = NSStringFromSelector(aSelector);
    if ([selectorString isEqualToString:@"gotoschool"]) {
        return self.student;
    }
    return nil;
    
}

我在People裡面添加了一個Student類實例,然後實現了forwardingTargetForSelector:方法。然後運行,奇跡地發現程式也沒有崩潰。該方法的作用是(上圖也有介紹):
返回一個對未識別消息處理的對象。如果實現了該方法,並且該方法沒有返回nil,那麼這個返回的對象就會作為新的接收對象,這個未知的消息將會被新對象處理。通過此方案,我們可以用組合來模擬多重繼承的某些特性,比如我返回多個類的組合,那麼就像繼承多個類一樣進行處理。在對外調用者來說,好像就是該對象親自處理的這些消息。

消息轉發

當動態方法解析和備援接收者都沒有進行處理的話,就會執行:

- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE("");

這個方法也是objc裡面NSObject.h裡面的方法,我對People進行如下處理:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"%@ can't handle by People",NSStringFromSelector([anInvocation selector]));
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"@@:"];
    return sign;
}

再次運行程式,發現程式沒有崩潰,只不過列印出來了“gotoschool can't handle by People”。
forwardInvocation:方法是將消息轉發給其他對象。
forwardInvocation:
從註釋看:對一個你的對象不識別的消息進行相應,你必須重寫methodSignatureForSelector:方法,該方法返回一個NSMethodSIgnature對象,該對象包含了給定選擇器所標識方法的描述。主要包含返回值的信息和參數信息。
實現forwardInvocation:方法時,若發現調用的message不是由本類處理,則續調用超類的同名方法。這樣所有父類均有機會處理此消息,直到NSObject。如果最後調用了NSObject的方法,那麼該方法就會調用“doesNotRecognizerSelector:”,拋出異常,標明選擇器最終未能得到處理。也就是上面的crash:NSInvalidArgumentException。
至此,真個消息轉發全流程結束。
上一個王圖:
消息轉發全流程

總結

接收者在每一步都有機會對未知消息進行處理,一句話:越早處理越好。如果能在第一步做完,就不進行其他操作,因為動態方法解析會將此方法緩存。如果動態方法解析不了,就放到第二步備援接收者,因為第三步還要創建完整的NSInvocation。
在完整來一遍:
Q:說一下你理解的消息轉發機制?
A:
先會調用objc_msgSend方法,首先在Class中的緩存查找IMP,沒有緩存則初始化緩存。如果沒有找到,則向父類的Class查找。如果一直查找到根類仍舊沒有實現,則執行消息轉發。
1、調用resolveInstanceMethod:方法。允許用戶在此時為該Class動態添加實現。如果有實現了,則調用並返回YES,重新開始objc_msgSend流程。這次對象會響應這個選擇器,一般是因為它已經調用過了class_addMethod。如果仍沒有實現,繼續下麵的動作。
2、調用forwardingTargetForSelector:方法,嘗試找到一個能響應該消息的對象。如果獲取到,則直接把消息轉發給它,返回非nil對象。否則返回nil,繼續下麵的動作。註意這裡不要返回self,否則會形成死迴圈。
3、調用methodSignatureForSelector:方法,嘗試獲得一個方法簽名。如果獲取不到,則直接調用doesNotRecognizeSelector拋出異常。如果能獲取,則返回非nil;傳給一個NSInvocation並傳給forwardInvocation:。
4、調用forwardInvocation:方法,將第三步獲取到的方法簽名包裝成Invocation傳入,如何處理就在這裡面了,並返回非nil。
5、調用doesNotRecognizeSelector:,預設的實現是拋出異常。如果第三步沒能獲得一個方法簽名,執行該步驟 。

另附相關雜亂代碼(裡面有動態方法解析demo)。
轉載請註明來源:http://www.cnblogs.com/zhanggui/p/7731394.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前端存儲 轉載請註明出處: "前端存儲" 前端存儲是每個前端開發工程師必備的技能。廢話不多說了,直接進入主題。 以下總結以下我接觸過的一些有關前端存儲方面的知識。主要是Cookie與WebStorage。當然,對於這兩種存儲方式的介紹,會與前端安全的問題一起討論 Cookie + Cookie定義 ...
  • ...
  • 通過<input />標簽,給它指定type類型為file,可提供文件上傳; accept:可選擇上傳類型,如:只要傳圖片,且不限製圖片格式,為image/*; multiple:規定是否可以選擇多個文件; 規定只可上傳圖片,且可以選擇多個文件 當然,直接一個input type=file 只能選擇 ...
  • 關於在vue cli搭建的項目中怎麼配置sass,網上搜到的基本是這種答案: 但是我認為,直接將樣式寫在每個單文件的<style 里,是十分不明智的做法。且不說node sass安裝過程的各種坑,內嵌的<style 也讓組件顯得十分混亂。想象一下你在修改某個methods時必須拖動滾輪穿 ...
  • 在這之前是給路由加一個meta屬性: 註意:但是事實是登錄的時候大多數時候並不進行跳轉,所以這裡需要在login跳轉的路徑中再加一段: ...
  • 有時候特別需要,個別網頁要去掉橫向滾動條和豎向滾動條,那該怎麼去掉呢,很簡單,看代碼: 讓豎條沒有: <body style=`overflow:-Scroll;overflow-y:hidden` > </body> 讓橫條沒有: <body style=`overflow:-Scroll;ove ...
  • 前言 vue這個框架現在挺流行的,作為一個專註前端100年的代碼愛好者,學習下目前流行的框架是必須的!在網上搜索vue的項目是比較少的,在官網進行了入門學習後,沒有一個項目練習鞏固下,學了就等於沒學,所以我就決定自己寫一個項目咯。在這裡我也順便分享下我學習vue的資源。我在GitHub上發現了一個v ...
  • 一、簡述 最近跟小伙伴一起討論了一下,決定一起仿一個BiliBili的app(包括android端和iOS端),我們並沒有打算把這個項目完全做完,畢竟我們的重點是掌握一些新框架的使用,併在實戰過程中發現並彌補自身的不足。 本系列將記錄我(android端)在開發過程中的一些我覺得有必要記錄的功能實現 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...