前言
寫這篇文章也是因為我一個朋友覺得學runtime沒啥用處,平時開發(fā)也用不到外盯,學了也就是為了應付面試而已饱苟,我不這么覺得狼渊,所以今天討論一下runtime這個黑科技能搞點啥東西出來囤锉,如有錯誤希望大家積極指正官地,我們一起進步驱入。
搞IOS的同學肯定都知道有這個東西,但又僅僅知道有這個東西的存在莺褒,oc的運行都得靠他遵岩,黑科技可遠觀而不可褻玩焉尘执。說白了runtime就是oc運行的一些機制宴凉,oc是一門動態(tài)語言弥锄,它的動態(tài)特性由runtime體現(xiàn)出來。今天主要研究四個問題饭庞。本文的demo稍后上傳但绕。
1.給分類添加屬性
2.消息轉(zhuǎn)發(fā)機制
3.動態(tài)交換方法的實現(xiàn)
4.手動實現(xiàn)多繼承(oc本身是不支持多繼承的)
在研究runtime之前先了解一下兩個經(jīng)常出現(xiàn)在runtime里的知識點
IMP
這是一個函數(shù)指針,顧名思義它保存的是一個函數(shù)的內(nèi)存地址六孵,通過IMP系統(tǒng)就可以找到這個函數(shù)然后去執(zhí)行它劫窒,也可以說說它指向一個方法的實現(xiàn)主巍。
SEL
這個類型我們經(jīng)常遇到比如[self performSelector:@selector(click) withObject:nil];這個方法里的:@selector(click)就是SEL類型孕索。SEL是方法編號搞旭,會根據(jù)方法的名字生成一個用來區(qū)分這個方法的唯一的一個ID肄渗,只要方法名稱相同翎嫡,那么它們的ID就是相同的。是對方法的一個包裝翁垂,它本身并不指向方法的實現(xiàn)沿猜,系統(tǒng)在內(nèi)部通過SEL找到對應的IMP啼肩,然后去執(zhí)行這個方法祈坠。
Method
在objc.h中, Method 的定義如下:
/// An opaque type that represents a method in a class definition.typedefstructobjc_method*Method;structobjc_method{SEL method_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;char*method_types? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;? ? IMP method_imp? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;}
Method = SEL + IMP + method_types,相當于在SEL和IMP之間建立了一個映射阁猜。
接下來進入正題蹋艺,我們都知道Category(分類)里面是不能添加屬性和成員變量的捎谨,為什么涛救?比如你在Category里申明了一個屬性@property(nonatomic,copy)NSString * userName;這個時候你看.m文件會有一個警告检吆,告訴我們要實現(xiàn)它的set方法咧栗,這就是原因所在致板,在分類里面添加屬性,系統(tǒng)只會申明set和get方法素征,并不會實現(xiàn)他們萝挤,并且也不會生成一個帶下劃線的成員變量御毅,也就是說在內(nèi)部我們沒辦法操作這個屬性。既然已經(jīng)知道問題了怜珍,那就可以開始解決的問題了端蛆,我們只要能在它的內(nèi)部自己實現(xiàn)set和get方法,然后可以操作這個屬性問題不就解決了酥泛,這個時候我們只要用到objc_setAssociatedObject和objc_getAssociatedObject這兩個函數(shù)就可以了今豆。首先我們創(chuàng)建一個UIView的分類Test嫌拣。然后在.m里實現(xiàn)set和get方法呆躲,代碼如下异逐。
這里面用到的知識點就是動態(tài)的關聯(lián)屬性
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
? ? ? ? ? ? ? ? ? ? ? ?? id _Nullable value, objc_AssociationPolicy policy)
可以看到這個函數(shù)需要傳入四個函數(shù)分別是被綁定的對象,鍵插掂,綁定的值和策略灰瞻,這個策略有幾個選項
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,?
OBJC_ASSOCIATION_RETAIN = 01401,? ? ?
OBJC_ASSOCIATION_COPY = 01403 類似于assign,retain,copy。
而這個鍵一般用靜態(tài)字符常量辅甥,用來保存和獲取所關聯(lián)的值酝润。這個函數(shù)相當于我們的set方法。同理objc_getAssociatedObject這個函數(shù)相當于我們的get方法肆氓。通過動態(tài)的關聯(lián)屬性袍祖,我們就可以變相的操作這個userName屬性了底瓣。
接下來我們來看看消息轉(zhuǎn)發(fā)是怎么玩的
首先我們定義一個類叫MessageForward谢揪,然后申明一個方法-(void)sendMessage;
//創(chuàng)建一個對象
MessageForward * message = [[MessageForward alloc]init];
[message sendMessage];//調(diào)用方法
接下來會發(fā)生什么呢?
編譯器將代碼[message sendMessage];轉(zhuǎn)化為objc_msgSend(object, @selector (makeText));捐凭,在objc_msgSend函數(shù)中拨扶。首先通過message的isa指針找到message對應的class。在Class中先去cache中 通過SEL(方法編號)查找對應的method茁肠,若 cache中未找到患民。再去methodList中查找,若methodlist中未找到垦梆,則去superClass中查找匹颤。若能找到,則將method加 入到cache中托猩,以方便下次查找印蓖,并通過method中的函數(shù)指針跳轉(zhuǎn)到對應的函數(shù)中去執(zhí)行。這是正常流程京腥,如果經(jīng)常上面一系列騷操作都沒有找到這個方法的實現(xiàn)赦肃,那么。公浪。他宛。。欠气。厅各。。crash预柒,game over队塘。然后報一個無法識別方法的錯誤琐鲁。如果不想crash,那么我們需要去實現(xiàn)整個消息轉(zhuǎn)發(fā)流程人灼。
第一步動 態(tài)方法解析
查找接收者所屬的類围段,看其是否能動態(tài)添加方法,以處理這個“未知的方法”投放。我們需要實現(xiàn)如下方法
這個方法是NSObject里面奈泪,留給我們來動態(tài)的解析方法
根據(jù)sel得到對應的方法的名字,然后進行判斷灸芳,如果methodNam為 sendMessage涝桅,那么調(diào)用
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
? ? ? ? ? ? ? ? const char * _Nullable types)函數(shù),添加方法烙样,需要四個參數(shù)冯遂,第一個是誰添加到哪個類,第二個要添加方法的編號谒获,第三個參數(shù)是一個函數(shù)指針蛤肌,方法的實現(xiàn),第四個參數(shù)用來描述這個方法的類型批狱,比如返回值類型裸准,各個參數(shù)的類型
’v‘ :void類型,第一個字符代表返回值類型
’@‘ : 一個id類型的對象赔硫,第一個參數(shù)類型
’:‘ : 對應SEL炒俱,第二個參數(shù)類型
下來及就是實現(xiàn)這個方法
這里面有兩個隱藏參數(shù) self和_cmd需要我們寫上,如果有別的自定義參數(shù)可以跟著后面寫爪膊。這個時候再運行就不會蹦了权悟,而是輸出我最帥三個字。如果沒有動態(tài)添加方法那么將會進入下一步找備用推盛。
找備用接受者(也就是備胎峦阁,專門用來接鍋的,心疼一分鐘)
首先我們先準備一個接鍋俠小槐,創(chuàng)建一個類Reserve
申明sendMessage方法拇派,并實現(xiàn)它(申不申明都行)
根據(jù)aSelector得到對應的方法的名字,然后進行判斷凿跳,如果methodNam為 sendMessage件豌,那么調(diào)用 [[Reserve alloc]init];然后將這個對象返回,也就是讓它去響應這個消息控嗜,這個時候我們運行茧彤,程序會走到Reserve這個類當中的sendMessage方法,這樣找備胎的就完成了疆栏。備胎這個詞真的很讓人心痛曾掂。惫谤。。珠洗。溜歪。。许蓖。蝴猪。。膊爪。
如果上面兩步騷操作都失效了自阱,那就只能用最后的大招了“消息轉(zhuǎn)發(fā)”,平時玩游戲的小伙伴都知道大招和普通的技能相比除了更牛x以外還有一個令人頭疼的后遺癥米酬,CD時間太長沛豌,在這里也就對應的程序的消耗更大,越往后面走消耗越大赃额〖优桑看代碼。
首先調(diào)用methodSignatureForSelector這個方法爬早,對方法進行簽名哼丈,獲取到方法的類型信息启妹,比如返回值類型筛严,各個參數(shù)類型等。然后調(diào)用-(void)forwardInvocation:(NSInvocation *)anInvocation饶米,將消息進行轉(zhuǎn)發(fā)給Reserve的對象r桨啃,讓這個r去響應這個消息,這個時候再運行檬输,依然完美的的解決了問題照瘾。
但是。丧慈。析命。。逃默。鹃愤。。完域。软吐。。這個連大招都失靈了會怎么樣吟税,一樣crash凹耙,這就很讓人尷尬姿现,我一頓操作猛如虎,一看戰(zhàn)績0-5肖抱,為了避免這種尷尬的情況我們還有最后一手底牌备典,請看代碼
-(void)doesNotRecognizeSelector:(SEL)aSelector{
? ? NSLog(@"消息無法處理");
};
這個方法就是最后的最后都失效了調(diào)用的意述,至少保證程序不會crash啊熊经,也不枉擼了那么多代碼。消息轉(zhuǎn)發(fā)機制到這里就算結(jié)束了欲险。
接下來我們就要動態(tài)交換方法的實現(xiàn)
首先我們創(chuàng)建一個UIView的分類Exchange然后如下代碼
所謂交換方法實際上交換兩個方法的實現(xiàn)镐依,比如有a,b兩個方法天试,將他們交換槐壳,那么調(diào)用a方法會去執(zhí)行b方法的實現(xiàn),反之調(diào)用b方法會去執(zhí)行a方法的實現(xiàn)喜每。這個很簡單务唐,我的demo里有,可以下載玩一下带兜。
接下類就是重頭戲了枫笛,多繼承(oc本身是不支持多繼承的)
記得在很久以前我去面試,面試官問我oc支持多繼承嗎刚照,當時雖然我是一個剛畢業(yè)的菜鳥刑巧,但這點常識還是有的,立刻回答了沒有(畢竟我也是背過不少面試題的无畔,我猜測接下來他會問啊楚,那oc協(xié)議可以多繼承嗎,哈哈浑彰,都是套路恭理,辛虧我早有準備),正當我胸有成竹的時候郭变,他說現(xiàn)在我就想實現(xiàn)多繼承你說怎么辦颜价。窩草,這么傲嬌诉濒,你想實現(xiàn)就實現(xiàn)周伦,蘋果不要面子的啊,對于這個問題當時我覺得不可能啊循诉,從來沒聽說過横辆,所以就回答了不知道,果然這場面試就這樣gg了”吩椋回去之后百度了一下困肩,窩草,居然真的有這種騷操作脆侮,今天就和大家分享一下锌畸。
我們先聊聊大致的思路
一個類既繼承A類,又繼承B類靖避,也就說這兩個類方法它都可以用潭枣,有什么辦法可以同時調(diào)用兩個類的方法呢,答案其實上面已經(jīng)說了幻捏,協(xié)議盆犁,oc是支持協(xié)議的多繼承的,我們把方法的申明寫在協(xié)議里篡九,然后讓這個類同時繼承A類和B類的協(xié)議谐岁,這樣就可以調(diào)用這兩個類的方法了,調(diào)用的時候編譯器不會報錯榛臼,但運行時還是會crash伊佃,因為在這個類內(nèi)部找不到方法的實現(xiàn),方法的實現(xiàn)是在A類和B類里沛善,那么怎么辦航揉,其實答案也在上面了,我們可以通過消息轉(zhuǎn)發(fā)機智金刁,將消息轉(zhuǎn)發(fā)到A類和B類里帅涂,這樣就解決得了找不到方法實現(xiàn)的問題,首先創(chuàng)建兩個類Man和Women胀葱,定義各自的協(xié)議代碼如下漠秋。
然后創(chuàng)建一個Children類,同時集成上面兩個協(xié)議抵屿。不過這個Children類的基類不是NSObject而是NSProxy,這是一個虛類捅位,沒有構(gòu)造方法轧葛,需要自己手動實現(xiàn)init方法。相當于輕量級的NSObject艇搀,主要實現(xiàn)-(nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel和-(void)forwardInvocation:(NSInvocation *)invocation兩個方法進行消息轉(zhuǎn)發(fā)尿扯,上面已經(jīng)說了這兩個方法。首先我們先寫個init方法焰雕。
第一步創(chuàng)建一個字典然后調(diào)用-(void)registerMethodsWithTarget:(id)target方法
這個方法是干嘛的衷笋,首先獲取到調(diào)用者所在類的所有實例方法,然后進行遍歷矩屁,然后以調(diào)用者為值辟宗,方法名作為鍵爵赵,添加到_methodsMap這個字典里,后邊會用的泊脐,先不說空幻。接下來進行消息轉(zhuǎn)發(fā)
首先調(diào)用-(nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel這個方法,返回方法簽名容客。
然后調(diào)用-(void)forwardInvocation:(NSInvocation *)invocation進行消息轉(zhuǎn)發(fā)秕铛,首先獲取到sel,然后根據(jù)sel獲取到methodName缩挑,然后真?zhèn)€methodName到_methodsMap這個字典去取值但两,得到目標target,就是我們再init方法里創(chuàng)建一個Man對象和一個Women對象供置,然后進行判斷如果target響應這個方法镜遣,將消息轉(zhuǎn)發(fā)給這個target,結(jié)束士袄。
這些就是runtime的幾個簡單應用悲关,還有好多好多騷操作,不過需要我們我們慢慢地去發(fā)現(xiàn)娄柳,runtime對于新手來說還是不太理解和操作的寓辱,用不好就是天大的bug,建議大家用的時候一定要小心一點赤拒,因為它是運行時候的機制秫筏,所以在編譯階段編譯器是發(fā)現(xiàn)不了錯誤的,出bug了也很難定位挎挖,雙刃劍吧这敬,用好的吊炸天,用不好蕉朵,估計就要上天臺了崔涂。