Objective-C Runtime 執行時之三:方法與訊息
前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。
基礎資料型別
SEL
SEL又叫選擇器,是表示一個方法的selector的指標,其定義如下:
1 | typedefstructobjc_selector * |
objc_selector結構體的詳細定義沒有在<objc/runtime.h>標頭檔案中找到。方法的selector用於表示執行時方法的名字。Objective-C在編譯時,會依據每一個方法的名字、引數序列,生成一個唯一的整型標識(Int型別的地址),這個標識就是SEL。如下程式碼所示:
12 | SEL sel1=@selector(method1);NSLog(@"sel : %p",sel1); |
上面的輸出為:
1 | 2014-10-3018:40:07.518RuntimeTest[52734:466626]sel:0x100002d72 |
兩個類之間,不管它們是父類與子類的關係,還是之間沒有這種關係,只要方法名相同,那麼方法的SEL就是一樣的。每一個方法都對應著一個SEL。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使引數型別不同也不行。相同的方法只能對應一個SEL。這也就導致Objective-C在處理相同方法名且引數個數相同但型別不同的方法方面的能力很差。如在某個類中定義以下兩個方法:
12 | -(void)setWidth:(int)width;-(void)setWidth:(double)width; |
這樣的定義被認為是一種編譯錯誤,所以我們不能像C++, C#那樣。而是需要像下面這樣來宣告:
12 | -(void)setWidthIntValue:(int)width;-(void)setWidthDoubleValue:(double)width; |
當然,不同的類可以擁有相同的selector,這個沒有問題。不同類的例項物件執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。
工程中的所有的SEL組成一個Set集合,Set的特點就是唯一,因此SEL是唯一的。因此,如果我們想到這個方法集合中查詢某個方法時,只需要去找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字串,而對於字串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比!!但是,有一個問題,就是數量增多會增大hash衝突而導致的效能下降(或是沒有衝突,因為也可能用的是perfect hash)。但是不管使用什麼樣的方法加速,如果能夠將總量減少(多個方法可能對應同一個SEL),那將是最犀利的方法。那麼,我們就不難理解,為什麼SEL僅僅是函式名了。
本質上,SEL只是一個指向方法的指標(準確的說,只是一個根據方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。這個查詢過程我們將在下面討論。
我們可以在執行時新增新的selector,也可以在執行時獲取已存在的selector,我們可以通過下面三種方法來獲取SEL:
- sel_registerName函式
- Objective-C編譯器提供的@selector()
- NSSelectorFromString()方法
IMP
IMP實際上是一個函式指標,指向方法實現的首地址。其定義如下:
1 | id(*IMP)(id,SEL,...) |
這個函式使用當前CPU架構實現的標準的C呼叫約定。第一個引數是指向self的指標(如果是例項方法,則是類例項的記憶體地址;如果是類方法,則是指向元類的指標),第二個引數是方法選擇器(selector),接下來是方法的實際引數列表。
前面介紹過的SEL就是為了查詢方法的最終實現IMP的。由於每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的IMP,查詢過程將在下面討論。取得IMP後,我們就獲得了執行這個方法程式碼的入口點,此時,我們就可以像呼叫普通的C語言函式一樣來使用這個函式指標了。
通過取得IMP,我們可以跳過Runtime的訊息傳遞機制,直接執行IMP指向的函式實現,這樣省去了Runtime訊息傳遞過程中所做的一系列查詢操作,會比直接向物件傳送訊息高效一些。
Method
介紹完SEL和IMP,我們就可以來講講Method了。Method用於表示類定義中的方法,則定義如下:
1234567 | typedefstructobjc_method *Method;structobjc_method{SEL method_name OBJC2_UNAVAILABLE;// 方法名char*method_types OBJC2_UNAVAILABLE;IMP method_imp OBJC2_UNAVAILABLE;// 方法實現} |
我們可以看到該結構體中包含一個SEL和IMP,實際上相當於在SEL和IMP之間作了一個對映。有了SEL,我們便可以找到對應的IMP,從而呼叫方法的實現程式碼。具體操作流程我們將在下面討論。
objc_method_description
objc_method_description定義了一個Objective-C方法,其定義如下:
1 | structobjc_method_description{SEL name;char*types;}; |
方法相關操作函式
Runtime提供了一系列的方法來處理與方法相關的操作。包括方法本身及SEL。本節我們介紹一下這些函式。
方法
方法操作相關函式包括下以:
1234567891011121314151617181920212223242526272829303132333435363738 | // 呼叫指定方法的實現id method_invoke(id receiver,Methodm,...);// 呼叫返回一個數據結構的方法的實現voidmethod_invoke_stret(id receiver,Methodm,...);// 獲取方法名SEL method_getName(Methodm);// 返回方法的實現IMP method_getImplementation(Methodm);// 獲取描述方法引數和返回值型別的字串constchar*method_getTypeEncoding(Methodm);// 獲取方法的返回值型別的字串char*method_copyReturnType(Methodm);// 獲取方法的指定位置引數的型別字串char*method_copyArgumentType(Methodm,unsignedintindex);// 通過引用返回方法的返回值型別字串voidmethod_getReturnType(Methodm,char*dst,size_t dst_len);// 返回方法的引數的個數unsignedintmethod_getNumberOfArguments(Methodm);// 通過引用返回方法指定位置引數的型別字串voidmethod_getArgumentType(Methodm,unsignedintindex,char*dst,size_t dst_len);// 返回指定方法的方法描述結構體structobjc_method_description *method_getDescription(Methodm);// 設定方法的實現IMP method_setImplementation(Methodm,IMP imp);// 交換兩個方法的實現voidmethod_exchangeImplementations(Method m1,Method m2); |
- method_invoke函式,返回的是實際實現的返回值。引數receiver不能為空。這個方法的效率會比method_getImplementation和method_getName更快。
- method_getName函式,返回的是一個SEL。如果想獲取方法名的C字串,可以使用sel_getName(method_getName(method))。
- method_getReturnType函式,型別字串會被拷貝到dst中。
- method_setImplementation函式,注意該函式返回值是方法之前的實現。
方法選擇器
選擇器相關的操作函式包括:
1234567891011 | // 返回給定選擇器指定的方法的名稱constchar*sel_getName(SEL sel);// 在Objective-C Runtime系統中註冊一個方法,將方法名對映到一個選擇器,並返回這個選擇器SEL sel_registerName(constchar*str);// 在Objective-C Runtime系統中註冊一個方法SEL sel_getUid(constchar*str);// 比較兩個選擇器BOOLsel_isEqual(SEL lhs,SEL rhs); |
- sel_registerName函式:在我們將一個方法新增到類定義時,我們必須在Objective-C Runtime系統中註冊一個方法名以獲取方法的選擇器。
方法呼叫流程
在Objective-C中,訊息直到執行時才繫結到方法實現上。編譯器會將訊息表示式[receiver message]轉化為一個訊息函式的呼叫,即objc_msgSend。這個函式將訊息接收者和方法名作為其基礎引數,如以下所示:
1 | objc_msgSend(receiver,selector) |
如果訊息中還有其它引數,則該方法的形式如下所示:
1 | objc_msgSend(receiver,selector,arg1,arg2,...) |
這個函式完成了動態繫結的所有事情:
- 首先它找到selector對應的方法實現。因為同一個方法可能在不同的類中有不同的實現,所以我們需要依賴於接收者的類來找到的確切的實現。
- 它呼叫方法實現,並將接收者物件及方法的所有引數傳給它。
- 最後,它將實現返回的值作為它自己的返回值。
訊息的關鍵在於我們前面章節討論過的結構體objc_class,這個結構體有兩個欄位是我們在分發訊息的關注的:
- 指向父類的指標
- 一個類的方法分發表,即methodLists。
當我們建立一個新物件時,先為其分配記憶體,並初始化其成員變數。其中isa指標也會被初始化,讓物件可以訪問類及類的繼承體系。
下圖演示了這樣一個訊息的基本框架:
當訊息傳送給一個物件時,objc_msgSend通過物件的isa指標獲取到類的結構體,然後在方法分發表裡面查詢方法的selector。如果沒有找到selector,則通過objc_msgSend結構體中的指向父類的指標找到其父類,並在父類的分發表裡面查詢方法的selector。依此,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector,函式會就獲取到了實現的入口點,並傳入相應的引數來執行方法的具體實現。如果最後沒有定位到selector,則會走訊息轉發流程,這個我們在後面討論。
為了加速訊息的處理,執行時系統快取使用過的selector及對應的方法的地址。這點我們在前面討論過,不再重複。
隱藏引數
objc_msgSend有兩個隱藏引數:
- 訊息接收物件
- 方法的selector
這兩個引數為方法的實現提供了呼叫者的資訊。之所以說是隱藏的,是因為它們在定義方法的原始碼中沒有宣告。它們是在編譯期被插入實現程式碼的。
雖然這些引數沒有顯示宣告,但在程式碼中仍然可以引用它們。我們可以使用self來引用接收者物件,使用_cmd來引用選擇器。如下程式碼所示:
123456789 | -strange{id target=getTheReceiver();SEL method=getTheMethod();if(target==self||method==_cmd)returnnil;return[target performSelector:method];} |
當然,這兩個引數我們用得比較多的是self,_cmd在實際中用得比較少。
獲取方法地址
Runtime中方法的動態繫結讓我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。不過靈活性的提升也帶來了效能上的一些損耗。畢竟我們需要去查詢方法的實現,而不像函式呼叫來得那麼直接。當然,方法的快取一定程度上解決了這一問題。
我們上面提到過,如果想要避開這種動態繫結方式,我們可以獲取方法實現的地址,然後像呼叫函式一樣來直接呼叫它。特別是當我們需要在一個迴圈內頻繁地呼叫一個特定的方法時,通過這種方式可以提高程式的效能。
NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指標,然後通過這個指標來呼叫實現程式碼。我們需要將methodForSelector:返回的指標轉換為合適的函式型別,函式引數和返回值都需要匹配上。
我們通過以下程式碼來看看methodForSelector:的使用:
1234567 | void(*setter)(id,SEL,BOOL);inti;setter=(void(*)(id,SEL,BOOL))[targetmethodForSelector:@selector(setFilled:)];for(i=0;i<1000;i++)setter(targetList[i],@selector(setFilled:),YES); |
這裡需要注意的就是函式指標的前兩個引數必須是id和SEL。
當然這種方式只適合於在類似於for迴圈這種情況下頻繁呼叫同一方法,以提高效能的情況。另外,methodForSelector:是由Cocoa執行時提供的;它不是Objective-C語言的特性。
訊息轉發
當一個物件能接收一個訊息時,就會走正常的方法呼叫流程。但如果一個物件無法接收指定訊息時,又會發生什麼事呢?預設情況下,如果是以[object message]的方式呼叫方法,如果object無法響應message訊息時,編譯器會報錯。但如果是以perform…的形式來呼叫,則需要等到執行時才能確定object是否能接收message訊息。如果不能,則程式崩潰。
通常,當我們不能確定一個物件是否能接收某個訊息時,會先呼叫respondsToSelector:來判斷一下。如下程式碼所示:
123 | if([selfrespondsToSelector:@selector(method)]){[selfperformSelector:@selector(method)];} |
不過,我們這邊想討論下不使用respondsToSelector:判斷的情況。這才是我們這一節的重點。
當一個物件無法接收某一訊息時,就會啟動所謂”訊息轉發(message forwarding)“機制,通過這一機制,我們可以告訴物件如何處理未知的訊息。預設情況下,物件接收到未知的訊息,會導致程式崩潰,通過控制檯,我們可以看到以下異常資訊:
12 | -[SUTRuntimeMethod method]:unrecognized selector sent toinstance0x100111940***Terminating app due touncaught exception'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940' |
這段異常資訊實際上是由NSObject的”doesNotRecognizeSelector”方法丟擲的。不過,我們可以採取一些措施,讓我們的程式執行特定的邏輯,而避免程式的崩潰。
訊息轉發機制基本上分為三個步驟:
- 動態方法解析
- 備用接收者
- 完整轉發
下面我們詳細討論一下這三個步驟。
動態方法解析
物件在接收到未知的訊息時,首先會呼叫所屬類的類方法+resolveInstanceMethod:(例項方法)或者+resolveClassMethod:(類方法)。在這個方法中,我們有機會為該未知訊息新增一個”處理方法”“。不過使用該方法的前提是我們已經實現了該”處理方法”,只需要在執行時通過class_addMethod函式動態新增到類裡面就可以了。如下程式碼所示:
相關推薦Objective-C Runtime 執行時之三:方法與訊息前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL iOS學習筆記56(Runtime)-Objective-C Runtime 執行時之三:方法與訊息前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 SEL [ObjectC]Runtime執行時之三:方法與訊息這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。 基礎資料型別 SEL SEL又叫選擇器,是表示一個方法的selector的指標,其定義如下:typedef struct objc_selector *SEL;o Objective-C Runtime 執行時之五:協議與分類Objective-C中的分類允許我們通過給一個類新增方法來擴充它(但是通過category不能新增新的例項變數),並且我們不需要訪問類中的程式碼就可以做到。 Objective-C中的協議是普遍存在的介面定義方式,即在一個類中通過@protocol定義介面,在另外 Objective-C Runtime 執行時之六:拾遺前面幾篇基本介紹了runtime中的大部分功能,包括對類與物件、成員變數與屬性、方法與訊息、分類與協議的處理。runtime大部分的功能都是圍繞這幾點來實現的。 本章的內容並不算重點,主要針對前文中對Objective-C Runtime Reference內容遺漏 Objective-C Runtime 執行時之二:成員變數與屬性在前面一篇文章中,我們介紹了Runtime中與類和物件相關的內容,從這章開始,我們將討論類實現細節相關的內容,主要包括類中成員變數,屬性,方法,協議與分類的實現。 本章的主要內容將聚集在Runtime對成員變數與屬性的處理。在討論之前,我們先介紹一個重要的概念:型別 Objective-C Runtime 執行時之四:Method Swizzling理解Method Swizzling是學習runtime機制的一個很好的機會。在此不多做整理,僅翻譯由Mattt Thompson發表於nshipster的Method Swizzling一文。 Method Swizzling是改變一個selector的實際實現的 iOS Runtime 執行時之三:訊息處理機制前面我們討論了Runtime中對類和物件的處理,及對成員變數與屬性的處理。這一章,我們就要開始討論Runtime中最有意思的一部分:訊息處理機制。我們將詳細討論訊息的傳送及訊息的轉發。不過在討論訊息之前,我們先來了解一下與方法相關的一些內容。 基礎資料型別 Objective-C Runtime 執行時之一:類與物件Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。 這種特性意味著Objective-C不僅需要一 Objective-C Runtime 執行時:成員變數(ivars)及屬性獲取類的成員變數和屬性: 在objc_class中,所有的成員變數、屬性的資訊是放在連結串列ivars中的。ivars是一個數組,陣列中每個元素是指向Ivar(變數資訊)的指標。runtime提供了豐富的函式來操作這一欄位。大體上可以分為以下幾類: 1.成員變數操作函式 objective-c runtime安全措施之二:反注入《O'Reilly.Hacking.and.Securing.iOS.Applications>>讀書筆記 反注入:在類函式被呼叫前做完整性檢測(預防應用自定義函式或apple標準庫函式被修改或替換) 原理:呼叫dladdr()函式檢查類方法的基本資訊是否合法 C++11併發學習之三:執行緒同步1.<mutex> 標頭檔案介紹 (1)Mutex系列類(四種) std::mutex,最基本的 Mutex 類。 std::recursive_mutex,遞迴 Mutex 類。 std::time_mutex,定時 Mutex 類。 std::recursive_ti [執行時]Objective-C的執行時程式設計(Runtime Programming)以前還真沒了解過Objective-C的執行時程式設計(Runtime Programming),今天特意在網上搜了下,原來這麼深奧啊 表示現在理解不了,先轉走了再說,之前轉載的文章都是大神們總結的綜合,轉載地址忘記註明了 ,抱歉。 -- [1] 版本和平臺 - Objective-C runtime原始碼學習之IMP定址(不包括訊息轉發部分)寫在前面 前段時間寫了一篇部落格runtime如何通過selector找到對應的IMP地址?(分別考慮類方法和例項方法),這是在看《招聘一個靠譜的iOS》時回答第22題時總結的一篇部落格,不過這篇部落格中並沒有牽涉到底層的程式碼,而且也留下了幾個沒有解決的 C/C++ socket程式設計教程之三:Windows下的socket程式本節講解 Windows 下 Socket ,學習 Linux Socket 的讀者可以跳過。 伺服器端程式碼 server.cpp: #include <stdio.h> #include <winsock2.h> #pragma c Linux學習之三:檔案與文件系統的壓縮與打包常用 etc 存在 filename 目錄 時有 blog 備份工具 restore 將檔案進行壓縮處理是為了使文件更加方便在網絡上傳輸以及降低硬盤使用量。進行壓縮的原理就是檔案在存儲時有很多的空間是無用的,而壓縮就是將這些空間給釋放出來。 Linux下幾種常見的壓縮方式後 Quarkus框架入門之三:Quarkus與Spring Boot啟動效能對比開篇 為什麼要從安逸的Spring全家桶切換到Quarkus框架呢?Quarkus有什麼優勢?除了能夠編譯成Native Appl Objective-C runtime之執行時的基本特點(三)學了那麼久的Objective-C,給我的感覺就是它什麼都是動態的,你將會聽到一個新的名詞: 一、動態方法解析 1、+(BOOL) resolveInstanceMethod:(SEL) sel 這是NSObject根類提供的類方法,呼叫時機為當被呼叫的方法實現部分沒有找到,而訊息轉發機制啟動之前的這個 iOS開發之關於Runtime執行時:類與物件Objective-C語言是一門動態語言,它將很多靜態語言在編譯和連結時期做的事放到了執行時來處理。這種動態語言的優勢在於:我們寫程式碼時更具靈活性,如我們可以把訊息轉發給我們想要的物件,或者隨意交換一個方法的實現等。 這種特性意味著Objective-C不僅需要一個編譯器,還需要一個執行時系統 C中異步IO淺析之三:深入理解異步IO的基本數據結構c 異步io libaio 一個函數庫或一段代碼的數據結構之間的關系,既展示了數據的行蹤,同時又隱含了函數的調用順序和使用方法。libaio內部的多個數據結構尤其如此,哪怕我們找不到文檔或者幫助手冊,只要深刻領悟頭文件中定義的數據結構及其內在聯系,再加一點代碼的驗證,就可以達到對libaio的A |