Objective-C 與 Runtime:為什么是這樣?
筆者非常高興能為Objective-C寫寫自己的理解和總結(jié),不僅僅因?yàn)槭枪P者是Objective-C多年的重度開發(fā)者,更是因?yàn)檫@是一門有獨(dú)特想法的,有創(chuàng)造性的,有優(yōu)美語(yǔ)法的,有歷史地位的編程語(yǔ)言。如果說(shuō)對(duì)本文有什么預(yù)期的話,筆者希望能把一些類似“為什么是這樣”的問(wèn)題說(shuō)清楚。
Objective-C發(fā)明于上世紀(jì)80年代,Objective-C的作者——Brad Cox和Tom Love,在接觸到SmallTalk語(yǔ)言之后,一方面受到SmallTalk的啟發(fā),另一方面也是看好C語(yǔ)言有著巨大影響力和廣闊前景,因此選擇在C語(yǔ)言的基礎(chǔ)上引入SmallTalk語(yǔ)言面向?qū)ο蠛拖⑴砂l(fā)的概念。最初的版本以C語(yǔ)言的擴(kuò)展的形式實(shí)現(xiàn)的,在C編譯器中編寫支持Objective-C的預(yù)處理模塊,預(yù)處理會(huì)先將Objective-C語(yǔ)法代碼轉(zhuǎn)化為C代碼,再繼續(xù)C代碼的編譯過(guò)程。1988年,以企業(yè)為目標(biāo)客戶的NeXT公司購(gòu)買Objective-C的使用授權(quán),接著擴(kuò)展著名開源編譯器GCC,使其支持Objective-C,并且開發(fā)了AppKit和FoundationKit等基礎(chǔ)庫(kù),Objective-C成為了NeXTSTEP系統(tǒng)(工作站)上“標(biāo)準(zhǔn)”的應(yīng)用程序開發(fā)語(yǔ)言。1996年,Apple公司收購(gòu)了NeXT公司,NeXTSTEP/OPENSTEP系統(tǒng)成為Apple新一代操作系統(tǒng)OS X的研發(fā)基礎(chǔ)。 2005年,Apple引入了Chris Lattner以及他的LLVM技術(shù)團(tuán)隊(duì),Objective-C新特性和編譯優(yōu)化***次得到高水平編譯器***優(yōu)先級(jí)的支持,先從后端的代碼優(yōu)化和生成開始,逐步擴(kuò)展到前端的語(yǔ)法解析(Clang)。如今(2015),Objective-C已經(jīng)擁有GCC之外更為適合更為優(yōu)異的編譯器套裝選擇——LLVM編譯器,LLVM包括完整的前后端模塊,***版本6.1(2015)。
Objective-C是面向?qū)ο蟮模@是Objective-C最基本的的概念。關(guān)于面向?qū)ο螅岩欢ǖ乃惴ǎê瘮?shù))和數(shù)據(jù)(變量)因某種內(nèi)在的聯(lián)系綁定在一起,形成最基本的程序結(jié)構(gòu)單元,這些結(jié)構(gòu)單元即是經(jīng)常談及的對(duì)象,加上抽象二字,我們稱呼它為抽象對(duì)象,術(shù)語(yǔ)簡(jiǎn)稱類;通過(guò)對(duì)變量的賦值(筆者認(rèn)為不僅是變量,邏輯運(yùn)算如閉包也是可以用于賦值)則會(huì)構(gòu)成實(shí)體對(duì)象,術(shù)語(yǔ)簡(jiǎn)稱對(duì)象(Objective-C一般也稱作實(shí)例)。對(duì)象和對(duì)象之間不是完全獨(dú)立的,通過(guò)巧妙的方式,它們之間能建立緊密的聯(lián)系,比如繼承、派生,對(duì)事物的抽象以及對(duì)代碼的復(fù)用有著微妙而重大的價(jià)值。Brad Cox和Tom Lov出版的***本正式Objective-C著作,書名即為《Object-Oriented Programming, An Evolutionary Approach》。那么,為什么要對(duì)象,為什么要面向?qū)ο??這是個(gè)好問(wèn)題,觀察人類普遍的思維,我們理解這個(gè)世界使用最多的概念就是物體,我們擅長(zhǎng)把感知到的一切抽象為一個(gè)個(gè)的物體,通過(guò)了解物體的構(gòu)成,以及物體之間的作用關(guān)系,實(shí)現(xiàn)對(duì)這個(gè)世界的認(rèn)知和作用的目的。這一直是非常奏效的!面向?qū)ο缶褪前讶祟惖乃季S的天賦和積累的思想財(cái)富應(yīng)用于編程,這樣,程序?qū)τ谠鰪?qiáng)生產(chǎn)能力/提高生活品質(zhì)的效率和能力方面會(huì)大大提高。
/* 上圖為FoundationKit中支持的集合對(duì)象——(不可變)數(shù)組,繼承于根類NSObject,支持實(shí)現(xiàn)NSCopying在內(nèi)的一系列協(xié)議(接口),count代表著有一個(gè)只讀變量,- (id)objectAtIndex:(NSUInteger)index等表示數(shù)組支持的可供使用的方法(函數(shù)) */
消息派發(fā)是Objective-C函數(shù)(Objective-C實(shí)際稱方法)調(diào)用的模式,前文亦有提及,概念繼承于Smalltalk。Objective-C的對(duì)象相互調(diào)用函數(shù),被看做是向目標(biāo)對(duì)象傳遞消息,消息的發(fā)送者稱作sender,消息的接收者稱作receiver,消息中間傳遞的字符串稱作selector(選擇子)。
/* 上圖的代碼表示至少有兩個(gè)明顯receiver,self.view為其中一個(gè)消息接收者,傳遞的消息(字符串/選擇子)為 “setBackgroundColor:“,UIColor表示一個(gè)類,類也是可以作為消息的接收者,字符串/選擇子為 “yellowColor” */
消息的處理就是需要先確定實(shí)際執(zhí)行的方法然后跳轉(zhuǎn)過(guò)去并執(zhí)行,我們理解為這是對(duì)該消息的回應(yīng),編譯期間,單從一句”派發(fā)消息”的語(yǔ)法是無(wú)法確定實(shí)際執(zhí)行的結(jié)果。只有在程序運(yùn)行期間,實(shí)際執(zhí)行的結(jié)果才能得到確定。這種在運(yùn)行期間才確定實(shí)際執(zhí)行的方法,Objective-C稱為動(dòng)態(tài)綁定。消息派發(fā)這種工作機(jī)制明顯區(qū)別另一著名面向?qū)ο缶幊陶Z(yǔ)言——C++。C++調(diào)用對(duì)象的函數(shù),函數(shù)與對(duì)象之間的關(guān)系,在編譯期間就必須嚴(yán)格確定,如果car里面沒(méi)有定義函數(shù)名為fly的函數(shù),編譯器不會(huì)通過(guò),而是會(huì)報(bào)錯(cuò)。Objective-C如果向car發(fā)送字符串為”fly”的selector,即使car沒(méi)有實(shí)現(xiàn)fly方法,編譯器依然能夠通過(guò),但是運(yùn)行期間則會(huì)因?yàn)楂@取不到實(shí)際執(zhí)行的方法而拋出異常。這也就是說(shuō),消息派發(fā)的設(shè)計(jì)使得編譯期間Objective-C非常包容對(duì)象所屬的類。如上述,相同對(duì)象有相同的定義,稱為類,類本身還可以看作對(duì)象——“類”對(duì)象,可以對(duì)“類”對(duì)象進(jìn)行“類”的定義,比如比較運(yùn)算,哈希,描述,類名等,總之一切皆為對(duì)象。C++里面我們可以基于稱之為模板的方式實(shí)現(xiàn)對(duì)“類”的自定義,Objective-C通過(guò)統(tǒng)一基類比如NSObject(不僅僅只有NSObject,還可以是各類根協(xié)議)方式對(duì)所有類新增定義。你可以向任何包括空指針nil在內(nèi)的對(duì)象發(fā)你想發(fā)的消息。消息派發(fā)的機(jī)制使得在不重新編譯的情況下,在運(yùn)行期間,干預(yù)或者說(shuō)hook原來(lái)的target(方法、變量等)變得更易于實(shí)現(xiàn),更有實(shí)際應(yīng)用價(jià)值。這個(gè)是需要依賴于消息派發(fā)和動(dòng)態(tài)綁定的實(shí)現(xiàn)機(jī)制——Runtime,但是Runtime并不僅僅為消息派發(fā)和動(dòng)態(tài)綁定而work,它也是Objective-C面向?qū)ο蟆?nèi)存模型等特性的實(shí)現(xiàn)者。
在正式介紹Runtime之前,我們先繼續(xù)介紹Objective-C的另外一個(gè)重要概念,筆者要說(shuō)是Objective-C內(nèi)存管理模型,程序運(yùn)行時(shí),創(chuàng)建一個(gè)對(duì)象總是要占用內(nèi)存的,而內(nèi)存總大小總歸有限,所以當(dāng)一個(gè)對(duì)象不再被需要時(shí),應(yīng)當(dāng)及時(shí)回收它所占用的內(nèi)存資源用于新的對(duì)象,Objective-C的內(nèi)存管理原理,簡(jiǎn)單說(shuō)就是“引用計(jì)數(shù)”機(jī)制。如果有模塊需要引用一個(gè)對(duì)象,引用時(shí)會(huì)讓對(duì)象統(tǒng)計(jì)用的引用計(jì)數(shù)值加1,并記錄在對(duì)象的結(jié)構(gòu)信息當(dāng)中,當(dāng)模塊不再需要該對(duì)象的時(shí)候則減1,而當(dāng)該對(duì)象的引用計(jì)數(shù)值為0時(shí),就可以認(rèn)為該對(duì)象不再被需要,及時(shí)銷毀釋放內(nèi)存(回收資源)。Objective-C對(duì)象的內(nèi)存空間僅分配在“堆空間”(heap space)中,肯定是不會(huì)分配在“棧”(stack)上。我們知道,“棧”的占用和回收是有嚴(yán)格的數(shù)據(jù)操作規(guī)則,簡(jiǎn)稱“先入后出”。函數(shù)執(zhí)行時(shí),傳入的變量(當(dāng)然包括對(duì)象變量)會(huì)按照確定的序列規(guī)則自動(dòng)壓入“棧”(占有內(nèi)存資源),函數(shù)執(zhí)行結(jié)束時(shí),這些變量又會(huì)按照相反的序列規(guī)則自動(dòng)彈出(釋放內(nèi)存資源)。因此,我們可以看出,“棧”其實(shí)是無(wú)法實(shí)施“引用計(jì)數(shù)”機(jī)制的,Objective-C否定使用“棧”存儲(chǔ)對(duì)象的設(shè)想。在語(yǔ)法上,Objective-C也無(wú)法像C++那樣直接聲明并創(chuàng)建一個(gè)對(duì)象變量,更無(wú)法直接操作該對(duì)象,Objective-C都是需要以類似C語(yǔ)言申請(qǐng)堆內(nèi)存塊的語(yǔ)法(alloc)那樣創(chuàng)建一個(gè)對(duì)象變量,并且必須通過(guò)對(duì)象指針作為訪問(wèn)句柄,這跟C語(yǔ)言申請(qǐng)堆內(nèi)存塊非常類似。Objective-C這一“任性”的設(shè)計(jì),也使得對(duì)象嵌套(一個(gè)對(duì)象作為另一個(gè)對(duì)象的成員變量)時(shí),對(duì)象基于引用計(jì)數(shù)機(jī)制,其成員變量也必須遞歸地遵循引用計(jì)數(shù)機(jī)制。因?yàn)槌蓡T變量實(shí)際都是一枚枚對(duì)象指針,很可能是與其它對(duì)象共享同一個(gè)對(duì)象(指針都指向同一塊內(nèi)存),引用計(jì)數(shù)機(jī)制正是適合用于支持這種“共享”內(nèi)存的管理。需要特別說(shuō)明,如果可以像C++那樣創(chuàng)建一個(gè)對(duì)象變量做成員變量,那么該成員變量會(huì)被存儲(chǔ)在該對(duì)象所在的一塊連續(xù)內(nèi)存塊,該對(duì)象銷毀時(shí)能夠自動(dòng)把成員變量的占有的內(nèi)存塊全部釋放收回,這與引用計(jì)數(shù)的機(jī)制并不太符合,所以,在Objective-C中對(duì)象變量不被支持也進(jìn)一步得到理解。
/* 上圖的接口(方法)是Objective-C中內(nèi)存管理相關(guān)的接口 (方法)*/
Runtime(component)譯名一般稱為運(yùn)行期組件,一個(gè)純C語(yǔ)言寫成的基礎(chǔ)庫(kù)(lib),Objective-C編寫出來(lái)的程序必須得到Runtime的運(yùn)行才能正常work,在Java、PHP或者Flash之類的編程語(yǔ)言當(dāng)中,大家對(duì)于Runtime并不會(huì)太陌生,Objective-C的Runtime其實(shí)也是一回事。正是Runtime實(shí)現(xiàn)了Objective-C許多的特性,Objective-C面向?qū)ο?、消息派發(fā)、動(dòng)態(tài)綁定和內(nèi)存管理都與Runtime的息息相關(guān)。那么,在Objective-C當(dāng)中,對(duì)象、類、函數(shù)(方法)都是如何被構(gòu)造并發(fā)揮作用的?前文提及,面向?qū)ο笾械念悾豢醋鞒橄罅说膶?duì)象,Runtime也是秉持這一理念。Runtime是純C寫成,用struct結(jié)構(gòu)體來(lái)描述對(duì)象(實(shí)體對(duì)象)和類(抽象對(duì)象)。
對(duì)象的struct比較簡(jiǎn)單,用*id作為結(jié)構(gòu)體objc_object的指針別名,***struct成員isa是Class類型的指針變量,正是該變量確定了對(duì)象所屬的類。Class類型也是struct,是結(jié)構(gòu)體objc_class的指針別名,用于描述類構(gòu)成的struct,***成員isa也是Class類型的指針變量(由兩個(gè)結(jié)構(gòu)體的***成員均為Class類型的指針變量的設(shè)計(jì)使得我們能進(jìn)一步體會(huì)到Runtime中,類的確有著和對(duì)象相同的看待),類的isa會(huì)指向稱之為metaclass(元類)的struct,metaclass抽象了類的特性,metaclass的***成員自然也是isa的Class類型的指針變量,不同的是元類的isa最終指向的是它自身,由此我們可以觀察到,類struct是一種遞歸嵌套的設(shè)計(jì),它正體現(xiàn)了面向?qū)ο?**抽象的理念,最終實(shí)現(xiàn)上指向自己則是實(shí)際工程處理的需要。一般我們還認(rèn)為objc_class這個(gè)struct存放類的metadata(元數(shù)據(jù)),例如類的實(shí)例方法、類的實(shí)例變量以及類的超類指針等。
Runtime還允許我們通過(guò)標(biāo)準(zhǔn)的接口(C函數(shù))對(duì)所有Objective-C類的變量、方法、屬性以及協(xié)議等等作查詢和動(dòng)態(tài)擴(kuò)展,從而達(dá)到我們豐富項(xiàng)目中語(yǔ)言和類庫(kù)特性的目標(biāo)。
/* 上圖的通過(guò)標(biāo)準(zhǔn)的Runtime API(C函數(shù))打印UIKit中UIView的所有變量、屬性以及方法*/
Runtime的另外一個(gè)重要的特性實(shí)現(xiàn)即為消息派發(fā),objc_msgSend是消息派發(fā)最核心最基礎(chǔ)的入口函數(shù),除此之外還有objc_msgsend_stret,objc_msgSend_fpret,objc_msgSendSuper等函數(shù),然而它們的重要性和作用遠(yuǎn)不及objc_msgSend。objc_msgSend函數(shù)會(huì)依據(jù)receiver和selector的來(lái)調(diào)用適當(dāng)?shù)姆椒?。為了完成此操作,該函?shù)需要在recevier所屬的類中搜尋其“方法列表”,如果能找到與selector字符串名稱相符的方法,就跳轉(zhuǎn)至該方法。若是找不到,那就沿著繼承體系繼續(xù)向上查找,等找到合適的方法之后再跳轉(zhuǎn)。如果最終還是找不到相符的方法,那就執(zhí)行“消息轉(zhuǎn)發(fā)”操作。由此,我們可以看到,調(diào)用一個(gè)方法似乎需要相當(dāng)?shù)牟襟E。每一個(gè)步驟都是開銷,是否會(huì)導(dǎo)致Objective-C有性能問(wèn)題?所幸obj_msgSend會(huì)將匹配到得結(jié)果緩存在“快速映射表”(fast map),每個(gè)類都有這樣一塊緩存,若是后面還需要向該類發(fā)送和相同的selector消息,執(zhí)行起來(lái)將會(huì)快許多。當(dāng)然,這種“快速執(zhí)行路徑”(fast path)還是不如“靜態(tài)綁定函數(shù)調(diào)用”(statically bound function call)那樣快,不過(guò)通過(guò)匯編等優(yōu)化技術(shù),映射表的查詢開銷已非常小,可以說(shuō),即使相比較C++的靜態(tài)綁定,Objective-C的消息派發(fā)機(jī)制已經(jīng)不是性能瓶頸所在。如果說(shuō)以上的消息派發(fā)機(jī)制就是Objective-C動(dòng)態(tài)綁定的全部?jī)?nèi)容,其實(shí)并不完全。當(dāng)對(duì)象查詢不到相關(guān)的方法,消息無(wú)法正確回應(yīng)時(shí),還會(huì)啟動(dòng)“消息轉(zhuǎn)發(fā)”機(jī)制。是的,在支持“動(dòng)態(tài)增加和替換”的方法列表之外,我們還能夠提供其它的正常響應(yīng)方式。消息轉(zhuǎn)發(fā)還分為幾個(gè)階段,***,先詢問(wèn)receiver或者說(shuō)是它所屬的類,看其是否能動(dòng)態(tài)添加方法,以處理當(dāng)前這個(gè)“未知選擇子”(unkonwn selector),這叫做“動(dòng)態(tài)方法解析”(dynamic method resolution),Runtime會(huì)通過(guò)回調(diào)一個(gè)類方法來(lái)尋求動(dòng)態(tài)添加方法的支持。如果Runtime完成動(dòng)態(tài)添加方法的詢問(wèn)之后,receiver仍然無(wú)法正常響應(yīng),則Runtime會(huì)繼續(xù)向receiver詢問(wèn)是否有其它對(duì)象即其它receiver能處理這條消息,若返回能夠處理的對(duì)象,Runtime會(huì)把消息轉(zhuǎn)給返回的對(duì)象,消息轉(zhuǎn)發(fā)流程也就結(jié)束。若無(wú)對(duì)象返回,Runtime會(huì)把消息有關(guān)的全部細(xì)節(jié)都封裝到NSInvocation對(duì)象中,再給receiver***一次機(jī)會(huì),令其設(shè)法解決當(dāng)前還未處理的這條消息。消息轉(zhuǎn)發(fā)的流程可以歸納到以下圖表:
由圖表可以看出,receiver在每一步中均有機(jī)會(huì)處理消息,步驟越往后,處理消息累計(jì)開銷就越大。所以,***能在***步就處理完,這樣的話,Runtime還可以把方法進(jìn)行緩存,在一步到位的同時(shí)進(jìn)一步降低***查詢這樣的開銷。需要注意的是在***一個(gè)階段,需要由兩個(gè)接口一起完成,先要通過(guò)- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector接口返回格式化的方法對(duì)象,下一個(gè)接口- (void)forwardInvocation:(NSInvocation *)anInvocation中傳入?yún)?shù)NSInvocation對(duì)象對(duì)此方法對(duì)象是有依賴,***步的NSMethodSignature對(duì)象返回nil,則消息轉(zhuǎn)發(fā)流程即告結(jié)束。
***利用消息轉(zhuǎn)發(fā)機(jī)制,我們實(shí)現(xiàn)一個(gè)讓NSString類支持NSArray實(shí)例方法的例子,這對(duì)于降低程序的Crash率很有幫助:
我們先實(shí)現(xiàn)一個(gè)方法替換的接口swizzle method,幫助我們?cè)诓恍枰^承的情況下,實(shí)現(xiàn)對(duì)父類方法的代碼注入
通過(guò)swizzle方式(class_addMethod、class_replaceMethod、method_exchangeImplementations),在NSString類的resolveInstanceMethod:中,動(dòng)態(tài)方法解析的方式注入3個(gè)NSArray的實(shí)例方法:
測(cè)試用例:
測(cè)試結(jié)果:
結(jié)尾,筆者用了很大的篇幅和代碼片段嘗試去解釋Objective-C最基本的一些概念,包括面向?qū)ο?、消息派發(fā)、內(nèi)存管理等等,并且也討論了這些概念在Rumtime上的實(shí)現(xiàn),這當(dāng)中還不包括屬性、分類、類族、協(xié)議等Objective-C中同樣重要的feature,也沒(méi)有深入闡述其中的一些編碼細(xì)節(jié)(有關(guān)編碼,通過(guò)搜索引擎,總能獲取許多令人滿意的答案)。筆者更多地是希望在有限的篇幅中幫助讀者快速理解Objective-C,理解它為什么是這樣而不是那樣,并且對(duì)于想進(jìn)一步學(xué)習(xí)和使用Objective-C的開發(fā)者和工程師能有所幫助。
參考鏈接(部分):