iOS-分類(Category)的本質(zhì) 及其與類擴(kuò)展(Extension) /繼承(Inherit)的區(qū)別

1、分類的概念

分類是為了擴(kuò)展系統(tǒng)類的方法而產(chǎn)生的一種方式藐窄,其作用就是在不修改原有類的基礎(chǔ)上资昧,為一個(gè)類擴(kuò)展方法,最主要的是可以給系統(tǒng)類擴(kuò)展我們自己定義的方法荆忍。

如何創(chuàng)建一個(gè)分類格带?↓↓


image.png

比如我們?yōu)镻erson創(chuàng)建了一個(gè)Student的分類:

image.png

其實(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)↓↓↓

image.png

然后在運(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ù)↓↓


image.png

上面的流程可以解釋為什么調(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è)文件

image.png

我們看到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)如下↓↓

image.png

同樣在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)用了:


image.png

一個(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)用)

主要流程就是這樣↓↓


image.png

這個(gè)順序在源碼中有體現(xiàn):

image.png
image.png

源碼閱讀指引↓↓

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)用

image.png

以上就是有繼承和分類情況下類的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方法桨武,


image.png

我們發(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的方法

image.png

上面流程我們可以在源碼中找到依據(jù):

首先調(diào)用方法是查看有沒有初始化忌愚,沒有的話就調(diào)用初始化操作


image.png

而初始化操作中先初始化父類


image.png

因?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)↓↓

image.png

類對(duì)象的結(jié)構(gòu)↓↓
image.png

因?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↓↓

image.png

image.png

所以我們?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ù)的使用:

image.png

關(guān)聯(lián)對(duì)象的原理我們可以通過源碼來查看 [objc4源碼解讀:objc-references.mm]

image.png

其中有幾個(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)該是這樣

image.png

這個(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)建:

image.png

類擴(kuò)展的格式:

@interface XXX()

//屬性

//方法(如果不實(shí)現(xiàn)亥鸠,編譯時(shí)會(huì)報(bào)警,Method definition for 'XXX' not found)

@end

類擴(kuò)展的創(chuàng)建:

1.直接在類文件中添加interface代碼塊

image.png

關(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è)問題

image

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)用子類的方法肴捉。

文章來自 https://www.cnblogs.com/gaoxiaoniu/p/10703651.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末踊赠,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子每庆,更是在濱河造成了極大的恐慌筐带,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缤灵,死亡現(xiàn)場(chǎng)離奇詭異伦籍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)腮出,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門帖鸦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人胚嘲,你說我怎么就攤上這事作儿。” “怎么了馋劈?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵攻锰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我妓雾,道長(zhǎng)娶吞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任械姻,我火速辦了婚禮妒蛇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘楷拳。我一直安慰自己绣夺,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布欢揖。 她就那樣靜靜地躺著陶耍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浸颓。 梳的紋絲不亂的頭發(fā)上物臂,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音产上,去河邊找鬼棵磷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晋涣,可吹牛的內(nèi)容都是我干的仪媒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼算吩!你這毒婦竟也來了留凭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤偎巢,失蹤者是張志新(化名)和其女友劉穎蔼夜,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體压昼,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡求冷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窍霞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匠题。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖但金,靈堂內(nèi)的尸體忽然破棺而出韭山,到底是詐尸還是另有隱情,我是刑警寧澤冷溃,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布钱磅,位于F島的核電站,受9級(jí)特大地震影響秃诵,放射性物質(zhì)發(fā)生泄漏续搀。R本人自食惡果不足惜塞琼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一菠净、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彪杉,春花似錦毅往、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至渴丸,卻和暖如春侯嘀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谱轨。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工戒幔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人土童。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓诗茎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親献汗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子敢订,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容