從一下方面來深入研究:
理解面向?qū)ο蟮念惖矫嫦蜻^程的結(jié)構(gòu)體
實踐Category添加屬性與黑魔法method swizzling
runtime就是運行時,runtime很強大,是OC最重要的一部分也是OC最大的特色扮碧,可以不夸張的說runtime成就了OC,盡管runtime是OC的一個模塊而已哩治。
我們都知道高級編程語言想要成為可執(zhí)行文件需要先編譯為匯編語言再匯編為機器語言,機器語言也是計算機能夠識別的唯一語言衬鱼,但是OC并不能直接編譯為匯編語言业筏,而是要先轉(zhuǎn)寫為純C語言再進行編譯和匯編的操作,從OC到C語言的過渡就是由runtime來實現(xiàn)的鸟赫。然而我們使用OC進行面向?qū)ο箝_發(fā)蒜胖,而C語言更多的是面向過程開發(fā)消别,這就需要將面向?qū)ο蟮念愞D(zhuǎn)變?yōu)槊嫦蜻^程的結(jié)構(gòu)體,本文正是通過runtime源碼分析來講解runtime是如何將面向?qū)ο蟮念愞D(zhuǎn)變?yōu)槊嫦蜻^程的結(jié)構(gòu)體台谢。
深入代碼理解instance寻狂、class object、metaclass
面向?qū)ο缶幊讨信缶冢钪匾母拍罹褪穷惿呷旅嫖覀兙蛷拇a入手,看看OC是如何實現(xiàn)類的樊拓。
前文一直在說runtime將面向?qū)ο蟮念愞D(zhuǎn)變?yōu)槊嫦蜻^程的結(jié)構(gòu)體纠亚,那這個結(jié)構(gòu)體到底是什么樣子的?打開#import文件筋夏,可以發(fā)現(xiàn)以下幾行代碼
通過注釋和代碼不難發(fā)現(xiàn)蒂胞,我們創(chuàng)建的一個對象或?qū)嵗鋵嵕褪且粋€struct objc_object結(jié)構(gòu)體,而我們常用的id也就是這個結(jié)構(gòu)體的指針条篷。有如下代碼:
通過上述代碼可以看出骗随,我們創(chuàng)建的NSString類的實例str其實就是一個struct objc_object結(jié)構(gòu)體指針,所以不管是Foundation框架中的類或是自定義的類赴叹,我們創(chuàng)建的類的實例最終獲取的都是一個結(jié)構(gòu)體指針鸿染,這個結(jié)構(gòu)體只有一個成員變量就是Class類型的isa指針,Class是結(jié)構(gòu)體指針乞巧,指向struct objc_class牡昆,那這個結(jié)構(gòu)體又是什么呢?這里先透露一句話str is a NSString摊欠,再加上Class這個指針的名字,我們不難猜測柱宦,Class就是代表NSString這個類些椒。
接下來會詳細講解這個結(jié)構(gòu)體,現(xiàn)在再看另一個例子掸刊,有時我們也會通過下述方法來創(chuàng)建一個實例:
可能你已經(jīng)發(fā)現(xiàn)了免糕,通過實例對象調(diào)用的class方法,我們能夠獲取到一個Class類型的變量忧侧,我們可以通過這個Class來創(chuàng)建相應(yīng)的實例對象石窑。
實際上,OC中的類也是一個對象蚓炬,稱為類對象松逊,上述方法中通過[str class]方法獲取到的就是NSString類的類對象,接著我們就可以通過這個類對象來創(chuàng)建實例對象肯夏,那這個類對象又是什么東西呢经宏?打開#import文件犀暑,我們可以找到結(jié)構(gòu)體struct objc_class的定義,該結(jié)構(gòu)體定義如下:
struct objc_class結(jié)構(gòu)體定義了很多變量烁兰,通過命名不難發(fā)現(xiàn)耐亏,結(jié)構(gòu)體里保存了指向父類的指針、類的名字沪斟、版本广辰、實例大小、實例變量列表主之、方法列表择吊、緩存、遵守的協(xié)議列表等杀餐,一個類包含的信息也不就正是這些嗎干发?沒錯,類對象就是一個結(jié)構(gòu)體struct objc_class史翘,這個結(jié)構(gòu)體存放的數(shù)據(jù)稱為元數(shù)據(jù)(metadata)枉长,該結(jié)構(gòu)體的第一個成員變量也是isa指針,這就說明了Class本身其實也是一個對象琼讽,因此我們稱之為類對象必峰,類對象在編譯期產(chǎn)生用于創(chuàng)建實例對象,是單例钻蹬。
類對象中的元數(shù)據(jù)存儲的都是如何創(chuàng)建一個實例的相關(guān)信息吼蚁,那么類對象和類方法應(yīng)該從哪里創(chuàng)建呢?就是從isa指針指向的結(jié)構(gòu)體創(chuàng)建问欠,類對象的isa指針指向的我們稱之為元類(metaclass)肝匆,元類中保存了創(chuàng)建類對象以及類方法所需的所有信息,因此整個結(jié)構(gòu)應(yīng)該如下圖所示:
實例對象顺献、類對象與元類簡圖
通過上圖我們可以清晰的看出來一個實例對象也就是struct objc_object結(jié)構(gòu)體它的isa指針指向類對象旗国,類對象的isa指針指向了元類,super_class指針指向了父類的類對象注整,而元類的super_class指針指向了父類的元類能曾,那元類的isa指針又指向了什么?為了更清晰的表達直接使用一個大神畫的圖肿轨。
實例對象寿冕、類對象與元類的自閉環(huán)
通過上圖我們可以看出整個體系構(gòu)成了一個自閉環(huán),如果是從NSObject中繼承而來的上圖中的Root class就是NSObject椒袍。至此驼唱,整個實例、類對象驹暑、元類的概念也就講清了曙蒸,接下來我們在代碼中看看這些概念該怎么應(yīng)用捌治。
c1是通過一個實例對象獲取的Class,實例對象可以獲取到其類對象纽窟,類名作為消息的接受者時代表的是類對象肖油,因此類對象獲取Class得到的是其本身,同時也印證了類對象是一個單例的想法臂港。
那么如果我們想獲取isa指針的指向?qū)ο竽兀?/p>
介紹兩個函數(shù)
class_isMetaClass用于判斷Class對象是否為元類森枪,object_getClass用于獲取對象的isa指針指向的對象。
再看如下代碼:
通過代碼可以看出审孽,一個實例對象通過class方法獲取的Class就是它的isa指針指向的類對象县袱,而類對象不是元類,類對象的isa指針指向的對象是元類佑力。
你不知道的msg_send
我們知道在OC中的實例對象調(diào)用一個方法稱作消息傳遞式散,比如有如下代碼:
上述代碼中的第二句str稱為消息的接受者,appendString:稱作選擇子也就是我們常用的selector打颤,selector和參數(shù)共同構(gòu)成了消息暴拄,所以第二句話可以理解為將消息:"增加一個字符串: is a good guy"發(fā)送給消息的接受者str。
OC中里的消息傳遞采用動態(tài)綁定機制來決定具體調(diào)用哪個方法编饺,OC的實例方法在轉(zhuǎn)寫為C語言后實際就是一個函數(shù)乖篷,但是OC并不是在編譯期決定調(diào)用哪個函數(shù),而是在運行期決定透且,因為編譯期根本不能確定最終會調(diào)用哪個函數(shù)撕蔼,這是由于運行期可以修改方法的實現(xiàn),在后文會有講解秽誊。舉個栗子鲸沮,有如下代碼:
上述代碼在編譯期沒有任何問題,因為id類型可以指向任何類型的實例對象锅论,NSString有一個方法appendString:诉探,在編譯期不確定這個num到底具體指代什么類型的實例對象,并且在運行期還可以給NSNumber類型添加新的方法棍厌,因此編譯期發(fā)現(xiàn)有appendString:的函數(shù)聲明就不會報錯,但在運行時找不到在NSNumber類中找不到appendString:方法竖席,就會報錯耘纱。這也就是消息傳遞的強大之處和弊端,編譯期無法檢查到未定義的方法毕荐,運行期可以添加新的方法束析。
講了這么多OC究竟是怎么將實例方法轉(zhuǎn)換為C語言的函數(shù),又是如何調(diào)用這些函數(shù)的呢憎亚?這些都依靠強大的runtime员寇。
在深入代碼之前介紹一個clang編譯器的命令:
通過上述clang命令可以轉(zhuǎn)寫代碼弄慰,然后找到如下定義:
可以發(fā)現(xiàn)轉(zhuǎn)寫后的C語言代碼將實例方法轉(zhuǎn)寫為了一個靜態(tài)函數(shù)。接下來一行一行的分析上述代碼蝶锋,第一行代碼可以簡要表示為如下代碼:
這一行代碼做了三件事情陆爽,第一獲取Person類,第二注冊alloc方法扳缕,第三發(fā)送消息慌闭,將消息alloc發(fā)送給類對象,可以簡單的將注冊方法理解為躯舔,通過方法名獲取到轉(zhuǎn)寫后C語言函數(shù)的函數(shù)指針驴剔。
第二行代碼就可以簡寫為如下代碼:
這一行代碼與上一行類似,注冊了init方法粥庄,然后通過objc_msgSend函數(shù)將消息init發(fā)送給消息的接受者p丧失。
第三行是一個對setter的調(diào)用,同樣的也可以簡寫為如下代碼:
這一行代碼同樣是先注冊方法setName:然后通過objc_msgSend函數(shù)將消息setName:發(fā)送給消息的接收者惜互,只是多了一個參數(shù)的傳遞布讹。
同理,最后一行代碼也可以簡寫為如下:
解釋與上述相同载佳,不再贅述炒事。
到這里,我們應(yīng)該就可以看出OC的runtime通過objc_msgSend函數(shù)將一個面向?qū)ο蟮南鬟f轉(zhuǎn)為了面向過程的函數(shù)調(diào)用蔫慧。
objc_msgSend函數(shù)根據(jù)消息的接受者和selector選擇適當?shù)姆椒▉碚{(diào)用挠乳,那它又是如何選擇的呢?這再來回顧一下幾個主要的結(jié)構(gòu)體:
注意結(jié)構(gòu)體struct objc_class中包含一個成員變量struct objc_method_list **methodLists姑躲,通過名稱我們分析出這個成員變量保存了實例方法列表睡扬,繼續(xù)查找結(jié)構(gòu)體struct objc_method_list的定義如下:
我們發(fā)現(xiàn)struct objc_method_list中還包含了一個未知的結(jié)構(gòu)體struct _objc_method同時也找到它的定義,為了方便查看將兩者寫在一起黍析。
結(jié)構(gòu)體struct objc_method_list里面包含以下幾個成員變量:結(jié)構(gòu)體struct _objc_method的大小卖怜、方法個數(shù)以及最重要的方法列表,方法列表存儲的是方法描述結(jié)構(gòu)體struct _objc_method阐枣,該結(jié)構(gòu)體里保存了選擇子马靠、方法類型以及方法的具體實現(xiàn)“剑可以看出方法的具體實現(xiàn)就是一個函數(shù)指針甩鳄,也就是我們自定義的實例方法,選擇子也就是selector可以理解為是一個字符串類型的名稱额划,用于查找對應(yīng)的函數(shù)實現(xiàn)(由于蘋果沒有開源selector的相關(guān)代碼妙啃,但是可以查到GNU OC中關(guān)于selector的定義,也是一個結(jié)構(gòu)體但是結(jié)構(gòu)體里存儲的就是一個字符串類型的名稱)。
這樣就能解釋objc_msgSend的工作原理的揖赴,為了匹配消息的接收者和選擇子馆匿,需要在消息的接收者所在的類中去搜索這個struct objc_method_list方法列表,如果能找到就可以直接跳轉(zhuǎn)到相關(guān)的具體實現(xiàn)中去調(diào)用燥滑,如果找不到渐北,那就會通過super_class指針沿著繼承樹向上去搜索,如果找到就跳轉(zhuǎn)突倍,如果到了繼承樹的根部(通常為NSObject)還沒有找到腔稀,那就會調(diào)用NSObjec的一個方法doesNotRecognizeSelector:,這個方法就會報unrecognized selector錯誤(其實在調(diào)用這個方法之前還會進行消息轉(zhuǎn)發(fā)羽历,還有三次機會來處理焊虏,消息轉(zhuǎn)發(fā)在后文會有介紹)。
這樣一看秕磷,要發(fā)送消息真的好復雜诵闭,需要經(jīng)過這么多步驟,難道不會影響性能嗎澎嚣?當然了疏尿,這樣一次次搜索和靜態(tài)綁定那樣直接跳轉(zhuǎn)到函數(shù)指針指向的位置去執(zhí)行來比肯定是耗時很多的,因此易桃,類對象也就是結(jié)構(gòu)體struct objc_class中有一個成員變量struct objc_cache褥琐,這個緩存里緩存的正是搜索方法的匹配結(jié)果,這樣在第二次及以后再訪問時就可以采用映射的方式找到相關(guān)實現(xiàn)的具體位置晤郑。
到這里我們就已經(jīng)弄清楚了整個發(fā)送消息的過程敌呈,但是當對象無法接收相關(guān)消息時又會發(fā)生什么?以及前文說的三次機會又是什么造寝?下文將會介紹消息轉(zhuǎn)發(fā)磕洪。
消息轉(zhuǎn)發(fā): unrecognized selector的最后三次機會
前文介紹了進行一次發(fā)送消息會在相關(guān)的類對象中搜索方法列表,如果找不到則會沿著繼承樹向上一直搜索知道繼承樹根部(通常為NSObject)诫龙,如果還是找不到并且消息轉(zhuǎn)發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報unrecognized selector錯析显。那么消息轉(zhuǎn)發(fā)到底是什么呢?接下來將會逐一介紹最后的三次機會签赃。
第一次機會: 所屬類動態(tài)方法解析
首先谷异,如果沿繼承樹沒有搜索到相關(guān)方法則會向接收者所屬的類進行一次請求,看是否能夠動態(tài)的添加一個方法锦聊,注意這是一個類方法歹嘹,因為是向接收者所屬的類進行請求。
第二次機會: 備援接收者
第三次機會: 消息重定向
理解OC的屬性property括丁,主要從runtime出發(fā)講解屬性property相關(guān)的底層實現(xiàn)和相關(guān)方法,
本文將會講解一些runtime操作屬性的相關(guān)方法伶选。
首先回顧一下相關(guān)代碼以及與property底層實現(xiàn)相關(guān)的兩個結(jié)構(gòu)體:
通過上述代碼其實我們可以看出史飞,一個@property屬性在底層就是一個結(jié)構(gòu)體描述尖昏,那么我們?nèi)绾潍@取這個結(jié)構(gòu)體呢?可以通過如下代碼獲取:
首先看一下objc_property_t是什么构资,在objc/runtime.h中可以找到相關(guān)定義:
typedefstructobjc_property*objc_property_t;
它是一個指向結(jié)構(gòu)體struct objc_property的指針抽诉,這里的結(jié)構(gòu)體struct objc_property其實就是前文中.cpp文件中的struct _prop_t結(jié)構(gòu)體,通過class_copyPropertyList方法就可以獲取到相關(guān)類的所有屬性吐绵,具體函數(shù)聲明如下:
注釋可以看出迹淌,第一個參數(shù)是相關(guān)類的類對象(如有疑問可以查閱本系列文章的前兩篇文章),第二個參數(shù)是一個指向unsigned int的指針己单,用于指明property的數(shù)量唉窃,通過該方法就能夠獲取到所有的屬性,接下來可以通過property_getName和property_getAttributes方法獲取該屬性描述的name和attributes值纹笼,輸出的結(jié)果如下:
name很好理解纹份,后面的attributes通過對比不難發(fā)現(xiàn)其規(guī)律,感興趣的讀者也可以多設(shè)置幾個不同類型廷痘、不同修飾符的property看一下輸出蔓涧。
除此之外哈有一下幾個方法用于根據(jù)屬性名獲取一個屬性描述結(jié)構(gòu)體、添加屬性笋额、替換屬性等方法元暴。
舉個簡單的栗子:
通過上述方法就能添加一個屬性,由于本人水平有限實際開發(fā)中沒有用過上述方法兄猩,具體實際例子也舉不出來所以不再過多贅述茉盏。
關(guān)聯(lián)對象 Associated Object
如果我們想為系統(tǒng)的類添加一個方法可以采用類別的方式進行擴展,相對來說比較簡單厦滤,但如果要添加一個屬性或稱為成員變量援岩,通常采用的方法就是繼承,這樣就比較繁瑣了掏导,如果不想去繼承那就可以通過runtime來進行關(guān)聯(lián)對象操作享怀。
使用runtime的關(guān)聯(lián)對象添加屬性與我們自定義類時定義的屬性其實是兩個不同的概念,通過關(guān)聯(lián)對象添加屬性本質(zhì)上是使用類別進行擴展趟咆,通過添加setter和getter方法從而在訪問時可以使用點語法進行方法添瓷,在使用上與自定義類定義的屬性沒有區(qū)別。
具體需要使用的C函數(shù)如下:
通過注釋和函數(shù)名不難發(fā)現(xiàn)上訴三個方法分別是設(shè)置關(guān)聯(lián)對象值纱、獲取關(guān)聯(lián)對象和刪除關(guān)聯(lián)對象鳞贷。
需要說明一下objc_AssociationPolicy,具體的定義如下:
這些關(guān)鍵詞很眼熟虐唠,沒錯搀愧,就是property使用的修飾符,具體含義也與property修飾符相同,
說了這么多咱筛,接下來舉個具體的栗子搓幌,為一個已有類添加一個關(guān)聯(lián)對象。
這個栗子設(shè)置的關(guān)聯(lián)對象其實沒有任何實際意義迅箩,通過代碼可以看出溉愁,使用runtime為一個已有類添加屬性就是通過類別擴展getter和setter方法。
實例方法
在本系列文章的第二篇iOS runtime探究(二): 從runtime開始深入理解OC消息轉(zhuǎn)發(fā)機制饲趋,我們詳細介紹了runtime對方法的底層處理拐揭,以及發(fā)送消息和消息轉(zhuǎn)發(fā)機制,這里就不再贅述了奕塑,如有需要可以查看相關(guān)文章堂污,本文會介紹OC層面對方法的相關(guān)操作,同時會介紹method swizzling的方法爵川。
先來回顧一下實例方法相關(guān)的結(jié)構(gòu)體和底層實現(xiàn)敷鸦,有如下代碼:
weak
weak不論是用作property修飾符還是用來修飾一個變量的聲明其作用是一樣的,就是不增加新對象的引用計數(shù)寝贡,被釋放時也不會減少新對象的引用計數(shù)扒披,同時在新對象被銷毀時,weak修飾的屬性或變量均會被設(shè)置為nil圃泡,這樣可以防止野指針錯誤碟案,本文要講解的也正是這個特性,runtime如何將weak修飾的變量的對象在銷毀時自動置為nil颇蜡。
那么runtime是如何實現(xiàn)在weak修飾的變量的對象在被銷毀時自動置為nil的呢价说?一個普遍的解釋是:runtime對注冊的類會進行布局,對于weak修飾的對象會放入一個hash表中风秤。用weak指向的對象內(nèi)存地址作為key鳖目,當此對象的引用計數(shù)為0的時候會dealloc,假如weak指向的對象內(nèi)存地址是a缤弦,那么就會以a為鍵在這個weak表中搜索领迈,找到所有以a為鍵的weak對象,從而設(shè)置為nil碍沐。
了解了以上知識后就可以深入runtiem代碼來看看具體實現(xiàn)細節(jié)狸捅,有興趣的讀者可以繼續(xù)閱讀。
深入runtime理解weak
這部分內(nèi)容參考《Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理》累提,可以看出具體的實現(xiàn)方式就是使用了一個HashTable尘喝。
NSString*name = [[NSStringalloc] initWithString:@"Jiaming Chen"];
__weakNSString*weakStr = name;
當為weakStr這一weak類型的對象賦值時,編譯器會根據(jù)name的地址為key去查找weak哈希表斋陪,該表項的值為一個數(shù)組朽褪,將weakStr對象的地址加入到數(shù)組中置吓,當name變量超出變量作用域或引用計數(shù)為0時,會執(zhí)行dealloc函數(shù)缔赠,在執(zhí)行該函數(shù)時交洗,編譯器會以name變量的地址去查找weak哈希表的值,并將數(shù)組里所有weak對象全部賦值為nil橡淑。
本文出自
鏈接:http://www.reibang.com/p/4a32fb8648a3