1、分類的概念
分類是為了擴(kuò)展系統(tǒng)類的方法而產(chǎn)生的一種方式藐窄,其作用就是在不修改原有類的基礎(chǔ)上资昧,為一個(gè)類擴(kuò)展方法,最主要的是可以給系統(tǒng)類擴(kuò)展我們自己定義的方法荆忍。
如何創(chuàng)建一個(gè)分類格带?↓↓
比如我們?yōu)镻erson創(chuàng)建了一個(gè)Student的分類:
其實(shí)分類的作用還是挺大的,比如我們有一個(gè)類的功能很復(fù)雜 如果只在這個(gè)類中實(shí)現(xiàn)的話不夠清晰刹枉,這個(gè)時(shí)候我們可以給這個(gè)類按照功能多建幾個(gè)分類叽唱,可以條理清晰的完成相應(yīng)功能,比如person類微宝,老師/學(xué)生/工人等等都有自己的特性棺亭,可以通過分類來合理管理。
2芥吟、分類的底層實(shí)現(xiàn)
我們可以通過一個(gè)例子來引出分類的本質(zhì)侦铜,比如現(xiàn)在有一個(gè)person類专甩,而person類現(xiàn)在又有兩個(gè)分類钟鸵,分別是PersonClass+Kid和PersonClass+sutdent,如果這三個(gè)類中都有一個(gè)test的對(duì)象方法(方法中打印對(duì)應(yīng)的文件名),那么當(dāng)我創(chuàng)建person對(duì)象后調(diào)用test方法,究竟會(huì)是個(gè)什么結(jié)果呢漫玄?出現(xiàn)這個(gè)結(jié)果的原因又是什么荆萤?
我們通過打印發(fā)現(xiàn)叨恨,調(diào)用set的方法后控制臺(tái)上打印的是PersonClass-kid澡罚,也就是實(shí)際上是調(diào)用PersonClass-kid分類的test方法
這個(gè)時(shí)候我們可能就有疑惑了顶瞒,我們?cè)谥爸v到對(duì)象的本質(zhì)的時(shí)候說當(dāng)調(diào)用方法時(shí)血筑,其底層都是通過消息機(jī)制來實(shí)現(xiàn)的 也就是objc_msgSend(objc,selector(msg))
消息機(jī)制會(huì)通過isa找對(duì)應(yīng)的對(duì)象找到對(duì)應(yīng)的方法 比如實(shí)例對(duì)象調(diào)用對(duì)象方法 就會(huì)根據(jù)實(shí)例對(duì)象的isa去類對(duì)象中遍歷對(duì)象方法列表 找到合適的方法就return 沒有的話就根據(jù)supperclass去父類中查找 一級(jí)級(jí)查找
按理說應(yīng)該是person調(diào)用test方法害幅,person實(shí)例對(duì)象根據(jù)其isa指針跑到person的類對(duì)象中找到對(duì)象方法列表消恍,也就是person類的test方法進(jìn)行調(diào)用。
但實(shí)際并非如此以现,我們都知道一個(gè)類只有一個(gè)類對(duì)象 只有一個(gè)元類對(duì)象狠怨,所以出現(xiàn)這個(gè)結(jié)果的原因只可能是分類的方法被添加到了類對(duì)象的方法列表中,并處在主類自身方法的前面邑遏。
那么分類的方法是什么時(shí)候被添加到類對(duì)象的方法中去的呢佣赖?是編譯的時(shí)候還是運(yùn)行的時(shí)候呢?
答案是在運(yùn)行時(shí)通過runtime動(dòng)態(tài)添加到類對(duì)象中去的
首先在編譯階段,系統(tǒng)是先將各個(gè)分類轉(zhuǎn)換為category_t結(jié)構(gòu)體记盒,這個(gè)結(jié)構(gòu)體里面存儲(chǔ)著分類中的各種信息(類方法/對(duì)象方法/屬性/協(xié)議等等)
我們可以在源碼中找到這個(gè)結(jié)構(gòu)↓↓↓
然后在運(yùn)行時(shí)通過runtime加載各個(gè)category_t結(jié)構(gòu)體(PersonClass+Kid和PersonClass+sutdent),通過while循環(huán)【①】遍歷所有的分類,把每一個(gè)Category的方法添加到一個(gè)新的方法大數(shù)組中,屬性添加到一個(gè)新的方法大數(shù)組中,協(xié)議數(shù)據(jù)添加到一個(gè)新的方法大數(shù)組中憎蛤;
最后,將合并后的分類數(shù)據(jù)(方法、屬性纪吮、協(xié)議)俩檬,插入到類原來數(shù)據(jù)(也就是主類的數(shù)據(jù))的前面,我們?cè)僬{(diào)用方法時(shí),通過消息機(jī)制遍歷方法列表,優(yōu)先找到了分類方法
這個(gè)流程我們可以在閱讀源碼中找到依據(jù)↓↓
上面的流程可以解釋為什么調(diào)用同名方法時(shí)有限調(diào)用了分類中的實(shí)現(xiàn)方法,但是我們這里有兩個(gè)分類方法碾盟,那為什么是調(diào)用的PersonClass-kid的方法呢豆胸?分類間的優(yōu)先級(jí)又是什么?
分類的優(yōu)先級(jí)其實(shí)我們?cè)谏厦娴牧鞒讨杏刑岬较锾郏簿褪洽俚奈恢猛砗褪峭ㄟ^while循環(huán)遍歷所有的分類,添加到數(shù)組中嚼沿,也就是優(yōu)先調(diào)用哪個(gè)分類取決于哪個(gè)分類被添加到數(shù)組的前面估盘,
因?yàn)槭莣hile循環(huán),所以越先編譯的反倒是放到了數(shù)組后面,后面參與編譯的Category數(shù)據(jù),會(huì)在數(shù)組的前面
這個(gè)編譯順序我們可以在這個(gè)位置查看↓↓哪個(gè)文件排名靠前就先編譯哪個(gè)文件
我們看到PersonClass-kid在最后骡尽,也就是最晚編譯的遣妥,根據(jù)while的取值規(guī)則,反倒被添加到了數(shù)組的最前面攀细,消息機(jī)制在方法列表中找到了對(duì)應(yīng)方法后就直接teturn了箫踩,所以調(diào)用了了PersonClass-kid的方法,當(dāng)我們手動(dòng)調(diào)整編譯順序后谭贪,比如把PersonClass-student.m調(diào)到了最后境钟,發(fā)現(xiàn)最終打印的結(jié)果是:PersonClass-sutdent
如果當(dāng)出現(xiàn)繼承關(guān)系呢?方法又會(huì)怎么調(diào)用呢俭识?
我們繼續(xù)創(chuàng)建一個(gè)teacher類慨削,繼承自person類,同事teacher類有兩個(gè)分類,分別是teacher+chinese和teacher+english缚态,結(jié)構(gòu)如下↓↓
同樣在teacher類及其分類中實(shí)現(xiàn)test方法磁椒,打印自己的文件名,
然后創(chuàng)建一個(gè)teacher類玫芦,調(diào)用teacher實(shí)例對(duì)象的對(duì)象方法浆熔,打印結(jié)果是 teacher-chinese
這個(gè)流程和剛才說到的一樣,teacher實(shí)例對(duì)象調(diào)用方法桥帆,首先根據(jù)isa去teacher的類對(duì)象中查找方法蘸拔,而分類中的方法在運(yùn)行時(shí)也被添加到了方法列表,且在主類自己的方法之前环葵,所以會(huì)調(diào)用分類的方法调窍,而究竟先調(diào)用哪個(gè)分類的方法取決于編譯順序,又因?yàn)閠eacher-chinese是teacher分類中最晚被編譯的张遭,所以結(jié)果是 teacher-chinese
假如teacher及其分類沒有實(shí)現(xiàn)test方法呢邓萨?
打印結(jié)果是PersonClass-sutdent
這是因?yàn)閠eacher實(shí)例變量根絕isa去類對(duì)象方法列表中沒有找到對(duì)應(yīng)的方法(即分類和主類都沒實(shí)現(xiàn)此方法)那么類對(duì)象將根據(jù)自己的superclass指針去父類(person)中去尋找對(duì)應(yīng)的方法,而上面也分析到了菊卷,person的分類方法加載到方法列表且處在主類方法前面缔恳,所以調(diào)用的是最晚編譯的分類的方法,即PersonClass-sutdent
所以當(dāng)調(diào)用某個(gè)方法時(shí)洁闰,流程應(yīng)該是這樣的
1.先去該類的分類中查看有無此方法歉甚,有的話調(diào)用分類的方法(多個(gè)分類都有此方法的話就調(diào)用最晚編譯的分類的方法);
2.沒有分類的話或者分類中沒有此方法的話扑眉,就查看主類中有無實(shí)現(xiàn)此方法纸泄,有的話調(diào)用;
3.主類在也沒有實(shí)現(xiàn)對(duì)應(yīng)方法的話就根據(jù)superclass指針去父類中查找腰素,一級(jí)級(jí)查找聘裁,找到調(diào)用
4.找到最頂部的基類也沒找到對(duì)應(yīng)方法的話,報(bào)方法找不到的錯(cuò)誤弓千,項(xiàng)目crash
3衡便、分類的load方法和initialize方法
在面試過程中涉及到分類時(shí)經(jīng)常會(huì)問道,category有l(wèi)oad方法嗎洋访?loda方法什么時(shí)候加載镣陕?load方法與initialize方法有什么區(qū)別?再出現(xiàn)繼承與分類情況時(shí)姻政,各個(gè)load方法或者initialize方法是按什么順序調(diào)用的呆抑?
我們?cè)诓榭刺O果官方關(guān)于load方法的介紹文檔中,可以看出:
當(dāng)類被引用進(jìn)項(xiàng)目的時(shí)候就會(huì)執(zhí)行l(wèi)oad函數(shù)(在main函數(shù)開始執(zhí)行之前),與這個(gè)類是否被用到無關(guān),每個(gè)類的load函數(shù)只會(huì)自動(dòng)調(diào)用一次.也就是load函數(shù)是系統(tǒng)自動(dòng)加載的扶歪,load方法會(huì)在runtime加載類理肺、分類時(shí)調(diào)用。
比如我們?cè)陧?xiàng)目中創(chuàng)建了幾個(gè)類及分類善镰,發(fā)現(xiàn)沒有做任何處理運(yùn)行項(xiàng)目妹萨,發(fā)現(xiàn)load方法被自動(dòng)調(diào)用了:
一個(gè)項(xiàng)目中有很多類,那么這些類的調(diào)用順序是什么炫欺?
先調(diào)用類的+load
1.按照編譯先后順序調(diào)用(先編譯乎完,先調(diào)用)
2.用子類的+load之前會(huì)先調(diào)用父類的+load
再調(diào)用分類的+load
1.按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
主要流程就是這樣↓↓
這個(gè)順序在源碼中有體現(xiàn):
源碼閱讀指引↓↓
objc4源碼解讀過程:objc-os.mm
_objc_init
load_images
prepare_load_methods
schedule_class_load
add_class_to_loadable_list
add_category_to_loadable_list
call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)
比如品洛,現(xiàn)在有一個(gè)person類树姨,person類有兩個(gè)子類student和teacher,
編譯順序是student/person/teacher 那么load調(diào)用順序應(yīng)該是這樣的:
1.系統(tǒng)按照編譯順序桥状,先找到student類帽揪,然后查看student有沒有父類且這個(gè)父類沒有執(zhí)行過
loda方法,發(fā)現(xiàn)有(pserson類)辅斟,然后再查看person類有沒有沒調(diào)用過load方法的父類转晰,
發(fā)現(xiàn)有一個(gè)NSObject,在遍歷NSObject有沒有沒調(diào)用過load方法的父類士飒,發(fā)現(xiàn)其是基類查邢,
沒有父類了所以,就先調(diào)用NSObject的load方法酵幕,然后接下來調(diào)用person的load方法扰藕,然后
再調(diào)用student的load方法
2.接下來找到person類,發(fā)現(xiàn)其不存在沒有調(diào)用過load方法的父類且其自己的load方法也被調(diào)用
過了芳撒,所以直接跳過了邓深,沒有調(diào)用任何的load方法
3.最后來到了teacher類,查找其父類時(shí)笔刹,發(fā)現(xiàn)父類及更高級(jí)別的父類都實(shí)現(xiàn)了load方法庐完,而自
己的load方法還沒有調(diào)用過,所以調(diào)用了teacher的load方法
所以調(diào)用順序是:NSObject的load方法->Person的load方法->sudent的loda方法->techer的load方法
因?yàn)槲覀儫o法修改NSObject的load方法實(shí)現(xiàn)徘熔,所以無法查看到它的方法打印
當(dāng)所有類的都調(diào)用完load方法后门躯,接下來開始調(diào)用分類的load方法↓↓
分類的load方法調(diào)用順序和分類的主類沒有任何關(guān)系,分類的調(diào)用順序很簡(jiǎn)單:
就是完全按照編譯順序調(diào)用load方法酷师,比如A有兩個(gè)分類a1讶凉,a2,B有兩個(gè)分類b1山孔,b2懂讯,
分類的編譯順序是b1,a2台颠,b2褐望,a1勒庄,那么分類的load方法調(diào)用順序就是:
b1的load方法->a2的load方法->b2的load方法->a1的load方法
這個(gè)時(shí)候我們又會(huì)產(chǎn)生一個(gè)新的困惑?我們之前在調(diào)用方法時(shí)瘫里,比如我們調(diào)用一個(gè)對(duì)象的test方法实蔽,是根據(jù)isa指針去方法列表中查找,找到后就return不在向上或者向下繼續(xù)查找執(zhí)行了谨读,但是為什么load方法卻不這樣呢局装?為什么load方法在執(zhí)行完父類的load方法后還繼續(xù)向下執(zhí)行子類的load方法?
這是因?yàn)閘oad方法并不是通過消息機(jī)制實(shí)現(xiàn)的劳殖,也就是不是通過objc_msgSend(obj,@selector(方法))來實(shí)現(xiàn)的铐尚,消息機(jī)制是找到對(duì)應(yīng)的方法就return,而load方法是直接通過方法地址直接調(diào)用
以上就是有繼承和分類情況下類的load方法調(diào)用順序問題哆姻。
接下來來看initialize方法:
initialize方法是在一個(gè)類或其子類第一次接收到消息之前進(jìn)行調(diào)用的宣增,用來初始化,一個(gè)類只會(huì)被初始化一次
initialize在類或者其子類的第一個(gè)方法被調(diào)用前調(diào)用矛缨。即使類文件被引用進(jìn)項(xiàng)目,但是沒有使用,initialize不會(huì)被調(diào)用
load方法是無論類有沒有被用到统舀,只要添加被引入到項(xiàng)目就會(huì)被調(diào)用,而initialize則是在這個(gè)類或者其子類第一次調(diào)用方法的時(shí)候就會(huì)進(jìn)行調(diào)用劳景。
某個(gè)類調(diào)用方法時(shí)誉简,是通過消息機(jī)制,也就是runtime的objc_msgSend方法實(shí)現(xiàn)的,所以initialize方法其實(shí)是在objc_msgSend進(jìn)行判斷調(diào)用的
也就是當(dāng)我們調(diào)用[teacher alloc]盟广,實(shí)際上是轉(zhuǎn)化為了objc_msgSend([teacher class],@selector(alloc))方法闷串,而objc_msgSend([teacher class],@selector(alloc))的內(nèi)部結(jié)構(gòu)有對(duì)teacher的initialize進(jìn)行了判斷,內(nèi)部結(jié)構(gòu)如下
objc_msgSend([teacher class],@selector(alloc)){
if([teacher class]沒有初始化){
//對(duì)teacher進(jìn)行初始化 當(dāng)然初始化并沒有這么簡(jiǎn)單還涉及到了父類的初始化
objc_msgSend([teacher class],@selector(initialize))筋量;
}
objc_msgSend([teacher class],@selector(alloc))
}
同樣在上面的項(xiàng)目中烹吵,我們重寫每個(gè)類及分類的initialize方法,調(diào)用teacher的alloc方法桨武,
我們發(fā)現(xiàn)是先調(diào)用父類Person分類的initialize方法 然后在調(diào)用自己分類的initialize方法肋拔,
上面提到了,objc_msgSend方法會(huì)判斷類是否進(jìn)行了初始化呀酸,沒有的話就進(jìn)行初始化凉蜂,
而對(duì)類的初始化過程,是優(yōu)先對(duì)類的父類進(jìn)行初始化的性誉,也就是如下的結(jié)構(gòu)
objc_msgSend([teacher class],@selector(initialize)){
if(teacher有父類 && teacher的父類沒有初始化){
//遞歸 有限初始化最頂級(jí)的父類
objc_msgSend([teacher父類 class],@selector(initialize))窿吩;
}
//標(biāo)記
類已初始化 = yes;
}
又因?yàn)?strong>initialize不同于load通過地址調(diào)用方法 错览,而是通過消息機(jī)制來進(jìn)行調(diào)用的纫雁,所以會(huì)遍歷類對(duì)象的方法列表,找到對(duì)應(yīng)的方法就return了倾哺,而分類的方法位于主類方法前轧邪,后編譯的分類排序更靠前刽脖,所以先調(diào)用了父類person分類Kid的方法,然后調(diào)用了teacher分類english的方法
上面流程我們可以在源碼中找到依據(jù):
首先調(diào)用方法是查看有沒有初始化忌愚,沒有的話就調(diào)用初始化操作
而初始化操作中先初始化父類
因?yàn)閕nitialize是通過消息機(jī)制來實(shí)現(xiàn)的曲管,所以當(dāng)子類沒事實(shí)現(xiàn)initialize方法是,會(huì)根據(jù)supertclass指針去調(diào)用父類中的同名方法(對(duì)象本質(zhì)中有講到)
也就是當(dāng)我們注釋掉teacher類及其分類中initialize方法的實(shí)現(xiàn)再調(diào)用[teacher alloc]方法時(shí)發(fā)現(xiàn) 調(diào)用了兩次person分類的initialize方法
2019-04-15 13:44:26.323779+0800 test[68408:9131102] PersonClass-Kid
2019-04-15 13:44:26.324083+0800 test[68408:9131102] PersonClass-Kid
第一次打印是因?yàn)槌跏蓟痶eacher時(shí)會(huì)先初始化父類person菜循,第二次打印是因?yàn)槌跏蓟痶eacher時(shí)沒有找到它的initialize方法翘地,所以去父類中查找了
雖然調(diào)用了兩次person的initialize方法申尤,但person只初始化了一次癌幕,第二次是初始化teacher
所以,initialize是當(dāng)類第一次用到時(shí)就對(duì)調(diào)用昧穿,先調(diào)用父類的+initialize勺远,再調(diào)用子類的initialize。
load方法和initialize方法都可以用來做什么操作时鸵?
首先 load方法和initialize方法有幾個(gè)相同點(diǎn):
1>在不考慮開發(fā)者主動(dòng)調(diào)用的情況下胶逢,系統(tǒng)最多會(huì)調(diào)用一次
2> 如果父類和子類都被調(diào)用,父類的調(diào)用一定在子類之前
+load
由于調(diào)用load方法時(shí)的環(huán)境很不安全饰潜,我們應(yīng)該盡量減少load方法的邏輯初坠,load很常見的一個(gè)使用場(chǎng)景,交換兩個(gè)方法的實(shí)現(xiàn):
//摘自MJRefresh
+ (void)load
{
[self exchangeInstanceMethod1:@selector(reloadData) method2:@selector(mj_reloadData)];
[self exchangeInstanceMethod1:@selector(reloadRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_reloadRowsAtIndexPaths:withRowAnimation:)];
[self exchangeInstanceMethod1:@selector(deleteRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_deleteRowsAtIndexPaths:withRowAnimation:)];
[self exchangeInstanceMethod1:@selector(insertRowsAtIndexPaths:withRowAnimation:) method2:@selector(mj_insertRowsAtIndexPaths:withRowAnimation:)];
[self exchangeInstanceMethod1:@selector(reloadSections:withRowAnimation:) method2:@selector(mj_reloadSections:withRowAnimation:)];
[self exchangeInstanceMethod1:@selector(deleteSections:withRowAnimation:) method2:@selector(mj_deleteSections:withRowAnimation:)];
[self exchangeInstanceMethod1:@selector(insertSections:withRowAnimation:) method2:@selector(mj_insertSections:withRowAnimation:)];
}
+ (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2
{
method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2));
}
+initialize
initialize方法一般只應(yīng)該用來設(shè)置內(nèi)部數(shù)據(jù),比如彭雾,某個(gè)全局狀態(tài)無法在編譯期初始化碟刺,可以放在initialize里面。比如NSMutableArray這種類型的實(shí)例化依賴于runtime的消息發(fā)送薯酝,所以顯然無法在編譯器初始化:
// int類型可以在編譯期賦值
static int someNumber = 0;
static NSMutableArray *someArray;
+ (void)initialize {
if (self == [Person class]) {
// 不方便編譯期復(fù)制的對(duì)象在這里賦值
someArray = [[NSMutableArray alloc] init];
}
}
還有幾個(gè)注意點(diǎn):
1》load調(diào)用時(shí)機(jī)比較早,運(yùn)行環(huán)境不安全半沽,所以在load方法中盡量不要涉及到其他的類。因?yàn)椴煌念惣虞d順序不同吴菠,當(dāng)load調(diào)用時(shí),其他類可能還沒加載完成者填,可能會(huì)導(dǎo)致使用到還沒加載的類從而出現(xiàn)問題;
2》load方法是線程安全的做葵,它使用了鎖占哟,我們應(yīng)該避免線程阻塞在load方法(因?yàn)檎麄€(gè)應(yīng)用程序在執(zhí)行l(wèi)oad方法時(shí)會(huì)阻塞,即酿矢,程序會(huì)阻塞直到所有類的load方法執(zhí)行完畢重挑,才會(huì)繼續(xù));initialize內(nèi)部也使用了鎖棠涮,所以是線程安全的(即只有執(zhí)行initialize的那個(gè)線程可以操作類或類實(shí)例谬哀。其他線程都要先阻塞,等待initialize執(zhí)行完)严肪。但同時(shí)要避免阻塞線程史煎,不要再使用鎖谦屑。
3》iOS會(huì)在應(yīng)用程序啟動(dòng)的時(shí)候調(diào)用load方法,在main函數(shù)之前調(diào)用
4》在首次使用某個(gè)類之前篇梭,系統(tǒng)會(huì)向其發(fā)送initialize消息氢橙,通常應(yīng)該在里面判斷當(dāng)前要初始化的類,防止子類未覆寫initialize的情況下調(diào)用兩次
4恬偷、關(guān)聯(lián)對(duì)象
分類中是可以使用屬性的悍手,但不能創(chuàng)建成員變量的,而主類中是可以使用屬性與成員變量的
原因我們可以通過比較類與分類的底層結(jié)構(gòu)可以看出袍患,
分類的結(jié)構(gòu)↓↓
類對(duì)象的結(jié)構(gòu)↓↓
因?yàn)榉诸惖膶?shí)際結(jié)構(gòu)中并沒有存放成員變量的數(shù)組坦康,所以其是無法創(chuàng)建和使用成員變量的
而當(dāng)我們?cè)趧?chuàng)建屬性時(shí),其實(shí)這個(gè)屬性實(shí)際上是執(zhí)行了一下操作:
@property(nonatomic,assign)double height;
/**
//1.聲明成員變量
{
double _weight;
}
2.實(shí)現(xiàn)set方法和get方法
- (void)setHeight:(double)height{
_height = height;
}
- (double)height{
return _height;
}
*/
因?yàn)榉诸愔袥]有成員變量诡延,所以分類中的屬性也就沒有自動(dòng)去實(shí)現(xiàn)set方法和get方法滞欠,這也就導(dǎo)致了我們?cè)谑褂梅诸悓傩詴r(shí)出現(xiàn)crash↓↓
所以我們?nèi)绻胱尫诸愔械膶傩曰虺蓡T變量能跟主類中一樣使用的話,需要通過運(yùn)行時(shí)建立關(guān)聯(lián)引用
使用方法:重寫分類屬性的set/get方法↓↓
//首先需要導(dǎo)入runtime的頭文件 #import <objc/runtime.h>
- (void)setHeight:(double)height{
/**
id _Nonnull object:這個(gè)參數(shù)是指屬性與哪個(gè)對(duì)象產(chǎn)生關(guān)聯(lián)?一般寫self即可
const void * _Nonnull key:這個(gè)是關(guān)聯(lián)屬性名 我一般都是直接寫屬性名 即@"height"
id _Nullable value:關(guān)聯(lián)屬性的屬性值 也就是height
objc_AssociationPolicy policy:這個(gè)參數(shù)一般是值屬性的修飾符 比如我們經(jīng)常用copy來字符串 assign修飾基本數(shù)據(jù)類型 還有就是原子鎖,我們常用的就是不加鎖nonatomic
*/
//這就相當(dāng)于把height這個(gè)屬性與self進(jìn)行綁定,可以看成是相當(dāng)于把@{@"height":@(height)}這個(gè)鍵值對(duì)存放在全局中的某個(gè)位置,可以讀取與設(shè)置
//height是基本類型 需要包裝成NSNumber
objc_setAssociatedObject(self, @"height", @(height), OBJC_ASSOCIATION_ASSIGN);
}
- (double)height{
//這個(gè)是指根據(jù)key去取出對(duì)應(yīng)的屬性值 這個(gè)需要注意的點(diǎn)事key一定要和set方法中的key一致
return [objc_getAssociatedObject(self, @"height") doubleValue];
}
關(guān)聯(lián)對(duì)象提供了以下API
//添加關(guān)聯(lián)對(duì)象
void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
//獲得關(guān)聯(lián)對(duì)象
id objc_getAssociatedObject(id object, const void * key)
//移除所有的關(guān)聯(lián)對(duì)象
void objc_removeAssociatedObjects(id object)
關(guān)于objc_setAssociatedObject中objc_AssociationPolicy參數(shù)的使用:
關(guān)聯(lián)對(duì)象的原理我們可以通過源碼來查看 [objc4源碼解讀:objc-references.mm]
其中有幾個(gè)點(diǎn)需要注意:
1.關(guān)聯(lián)對(duì)象并不是存儲(chǔ)在被關(guān)聯(lián)對(duì)象本身內(nèi)存中 主類的屬性是存儲(chǔ)到類對(duì)象自己的內(nèi)存中的肆良,但是通過關(guān)聯(lián)方式并不會(huì)把屬性添加到類對(duì)象內(nèi)存中 而是將關(guān)聯(lián)對(duì)象存儲(chǔ)在全局的統(tǒng)一的一個(gè)AssociationsManager中
2.設(shè)置關(guān)聯(lián)對(duì)象為nil筛璧,就相當(dāng)于是移除關(guān)聯(lián)對(duì)象
3.當(dāng)關(guān)聯(lián)對(duì)象被銷毀時(shí),AssociationsManager中存在所有與關(guān)聯(lián)對(duì)象綁定的信息都會(huì)被釋放
按照個(gè)人理解的方式應(yīng)該是這樣
這個(gè)Map我們就可以理解為一個(gè)字典惹恃,里面存放著一個(gè)個(gè)鍵值對(duì)
所以通過上面的分析夭谤,我們可以回答一個(gè)經(jīng)常被問道 的關(guān)于category的面試題
//Category能否添加成員變量?如果可以巫糙,如何給Category添加成員變量朗儒?
答:不能直接給Category添加成員變量,但是可以間接實(shí)現(xiàn)Category有成員變量的效果曲秉。
我們可以使用runtime的API采蚀,objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)和objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)這兩個(gè)來實(shí)現(xiàn)。[重寫分類屬性點(diǎn)的set方法和get方法]
5承二、分類(Category)與類擴(kuò)展(Extension)/繼承(Inherit)的區(qū)別
很多面試題經(jīng)常會(huì)比較分類和類擴(kuò)展的區(qū)別榆鼠,首先我們要看一下什么是分類,什么是類擴(kuò)展↓↓↓
分類的格式:
@interface待擴(kuò)展的類(分類的名稱)
@end
@implementation待擴(kuò)展的名稱(分類的名稱)
@end
分類的創(chuàng)建:
類擴(kuò)展的格式:
@interface XXX()
//屬性
//方法(如果不實(shí)現(xiàn)亥鸠,編譯時(shí)會(huì)報(bào)警,Method definition for 'XXX' not found)
@end
類擴(kuò)展的創(chuàng)建:
1.直接在類文件中添加interface代碼塊
關(guān)于類擴(kuò)展和分類的區(qū)別:
1妆够、上面提到的分類不能添加成員變量【雖然可以添加屬性 但是一旦調(diào)用就會(huì)報(bào)方法找不到的錯(cuò)誤】 (可以通過runtime給分類間接添加成員變量),而類擴(kuò)展可以添加成員變量;
2负蚊、分類中的屬性不會(huì)自動(dòng)實(shí)現(xiàn)set方法和get方法神妹,而類擴(kuò)展中的屬性再轉(zhuǎn)為底層時(shí)是可以自動(dòng)實(shí)現(xiàn)set、get方法
3家妆、類擴(kuò)展中添加的新方法鸵荠,不實(shí)現(xiàn)會(huì)報(bào)警告。categorygory中定義了方法不實(shí)現(xiàn)則沒有這個(gè)問題
4伤极、類擴(kuò)展可以定義在.m文件中蛹找,這種擴(kuò)展方式中定義的變量都是私有的姨伤,也可以定義在.h文件中,這樣定義的代碼就是共有的庸疾,類擴(kuò)展在.m文件中聲明私有方法是非常好的方式乍楚。
5、類擴(kuò)展不能像分類那樣擁有獨(dú)立的實(shí)現(xiàn)部分(@implementation部分)届慈,也就是說徒溪,類擴(kuò)展所聲明的方法必須依托對(duì)應(yīng)類的實(shí)現(xiàn)部分來實(shí)現(xiàn)。
分類(Categories) 和 繼承(Inherit) 可能有時(shí)候可以實(shí)現(xiàn)相同的功能金顿,但其實(shí)兩個(gè)存在較大的差異臊泌,簡(jiǎn)單介紹一下兩者的異同。
比如剛才上面的情況串绩,我們調(diào)用一個(gè)方法是缺虐,系統(tǒng)的查找順序是先查找分類芜壁,分類沒有查找主類礁凡,主類沒有查找父類(分類沒有查找主類是因?yàn)榉诸? 主類沒有查找父類是因?yàn)槔^承)
有人可能會(huì)有疑問,既然是先查找分類再查找主類慧妄,這不是和繼承中的先查找子類方法顷牌,沒有的話再去父類查找是一樣的么,能否用集成來代替分類呢塞淹?
其實(shí)是不行的窟蓝,雖然先查找分類再查找主類這個(gè)流程很像繼承(看著像是分類是繼承自主類的子類),但是兩者有很大區(qū)別饱普,主要表現(xiàn)在兩點(diǎn):
1运挫、邏輯方面:兩者代表的層級(jí)關(guān)系不一樣,繼承代表父子關(guān)系套耕,分類代表同級(jí)關(guān)系谁帕。
比如dog與animal是繼承關(guān)系,dog與cat是同級(jí)關(guān)系(dog是animal的子類冯袍,dog和cat是同級(jí) 都是animal的子類)
如果我們用繼承來代替分類匈挖,也就是cat繼承自dog,那么無論是可讀性還是邏輯表達(dá)上都是難以理解的
2康愤、方法調(diào)用上:分類這種方式中儡循,主類可以調(diào)用分類的方法,分類也可以調(diào)用主類的方法征冷,可以相互調(diào)用择膝,
而繼承則不行,子類可以調(diào)用父類的方法检激,但是父類卻不能調(diào)用子類的方法肴捉。