1、分類的概念
分類是為了擴展系統類的方法而產生的一種方式,其作用就是在不修改原有類的基礎上,為一個類擴展方法,最主要的是可以給系統類擴展我們自己定義的方法。
如何創建一個分類?↓↓
(1)Cmd+N,iOS-->Objective-C File,Next; (2)File Type選擇category,class選擇需要的類,分類名,Next。
比如我們為Person創建了一個Student的分類:
其實分類的作用還是挺大的,比如我們有一個類的功能很復雜 如果只在這個類中實現的話不夠清晰,這個時候我們可以給這個類按照功能多建幾個分類,可以條理清晰的完成相應功能,比如person類,老師/學生/工人等等都有自己的特性,可以通過分類來合理管理。
2、分類的底層實現
我們可以通過一個例子來引出分類的本質,比如現在有一個person類,而person類現在又有兩個分類,分別是PersonClass+Kid和PersonClass+sutdent,如果這三個類中都有一個test的對象方法(方法中打印對應的文件名),那么當我創建person對象后調用test方法,究竟會是個什么結果呢?出現這個結果的原因又是什么?
我們通過打印發現,調用set的方法后控制台上打印的是PersonClass-kid,也就是實際上是調用PersonClass-kid分類的test方法
這個時候我們可能就有疑惑了,我們在之前講到對象的本質的時候說當調用方法時,其底層都是通過消息機制來實現的 也就是objc_msgSend(objc,selector(msg))
消息機制會通過isa找對應的對象找到對應的方法 比如實例對象調用對象方法 就會根據實例對象的isa去類對象中遍歷對象方法列表 找到合適的方法就return 沒有的話就根據supperclass去父類中查找 一級級查找
按理說應該是person調用test方法,person實例對象根據其isa指針跑到person的類對象中找到對象方法列表,也就是person類的test方法進行調用。
但實際並非如此,我們都知道一個類只有一個類對象 只有一個元類對象,所以出現這個結果的原因只可能是分類的方法被添加到了類對象的方法列表中,並處在主類自身方法的前面。
那么分類的方法是什么時候被添加到類對象的方法中去的呢?是編譯的時候還是運行的時候呢?
答案是在運行時通過runtime動態添加到類對象中去的
首先在編譯階段,系統是先將各個分類轉換為category_t結構體,這個結構體里面存儲着分類中的各種信息(類方法/對象方法/屬性/協議等等)
我們可以在源碼中找到這個結構↓↓↓
然后在運行時通過runtime加載各個category_t結構體(PersonClass+Kid和PersonClass+sutdent),通過while循環【①】遍歷所有的分類,把每一個Category的方法添加到一個新的方法大數組中,屬性添加到一個新的方法大數組中,協議數據添加到一個新的方法大數組中;
最后,將合並后的分類數據(方法、屬性、協議),插入到類原來數據(也就是主類的數據)的前面,我們再調用方法時,通過消息機制遍歷方法列表,優先找到了分類方法
這個流程我們可以在閱讀源碼中找到依據↓↓
上面的流程可以解釋為什么調用同名方法時有限調用了分類中的實現方法,但是我們這里有兩個分類方法,那為什么是調用的PersonClass-kid的方法呢?分類間的優先級又是什么?
分類的優先級其實我們在上面的流程中有提到,也就是①的位置,就是通過while循環遍歷所有的分類,添加到數組中,也就是優先調用哪個分類取決於哪個分類被添加到數組的前面,
因為是while循環,所以越先編譯的反倒是放到了數組后面,后面參與編譯的Category數據,會在數組的前面
這個編譯順序我們可以在這個位置查看↓↓哪個文件排名靠前就先編譯哪個文件
我們看到PersonClass-kid在最后,也就是最晚編譯的,根據while的取值規則,反倒被添加到了數組的最前面,消息機制在方法列表中找到了對應方法后就直接teturn了,所以調用了了PersonClass-kid的方法,當我們手動調整編譯順序后,比如把PersonClass-student.m調到了最后,發現最終打印的結果是:PersonClass-sutdent
如果當出現繼承關系呢?方法又會怎么調用呢?
我們繼續創建一個teacher類,繼承自person類,同事teacher類有兩個分類,分別是teacher+chinese和teacher+english,結構如下↓↓
同樣在teacher類及其分類中實現test方法,打印自己的文件名,
然后創建一個teacher類,調用teacher實例對象的對象方法,打印結果是 teacher-chinese
這個流程和剛才說到的一樣,teacher實例對象調用方法,首先根據isa去teacher的類對象中查找方法,而分類中的方法在運行時也被添加到了方法列表,且在主類自己的方法之前,所以會調用分類的方法,而究竟先調用哪個分類的方法取決於編譯順序,又因為teacher-chinese是teacher分類中最晚被編譯的,所以結果是 teacher-chinese
假如teacher及其分類沒有實現test方法呢?
打印結果是PersonClass-sutdent
這是因為teacher實例變量根絕isa去類對象方法列表中沒有找到對應的方法(即分類和主類都沒實現此方法)那么類對象將根據自己的superclass指針去父類(person)中去尋找對應的方法,而上面也分析到了,person的分類方法加載到方法列表且處在主類方法前面,所以調用的是最晚編譯的分類的方法,即PersonClass-sutdent
所以當調用某個方法時,流程應該是這樣的
1.先去該類的分類中查看有無此方法,有的話調用分類的方法(多個分類都有此方法的話就調用最晚編譯的分類的方法);
2.沒有分類的話或者分類中沒有此方法的話,就查看主類中有無實現此方法,有的話調用;
3.主類在也沒有實現對應方法的話就根據superclass指針去父類中查找,一級級查找,找到調用
4.找到最頂部的基類也沒找到對應方法的話,報方法找不到的錯誤,項目crash
3、分類的load方法和initialize方法
在面試過程中涉及到分類時經常會問道,category有load方法嗎?loda方法什么時候加載?load方法與initialize方法有什么區別?再出現繼承與分類情況時,各個load方法或者initialize方法是按什么順序調用的?
我們在查看蘋果官方關於load方法的介紹文檔中,可以看出:
當類被引用進項目的時候就會執行load函數(在main函數開始執行之前),與這個類是否被用到無關,每個類的load函數只會自動調用一次.也就是load函數是系統自動加載的,load方法會在runtime加載類、分類時調用。
比如我們在項目中創建了幾個類及分類,發現沒有做任何處理運行項目,發現load方法被自動調用了:
一個項目中有很多類,那么這些類的調用順序是什么?
先調用類的+load 1.按照編譯先后順序調用(先編譯,先調用) 2.用子類的+load之前會先調用父類的+load 再調用分類的+load 1.按照編譯先后順序調用(先編譯,先調用)
主要流程就是這樣↓↓
這個順序在源碼中有體現:
源碼閱讀指引↓↓
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)
比如,現在有一個person類,person類有兩個子類student和teacher, 編譯順序是student/person/teacher 那么load調用順序應該是這樣的: 1.系統按照編譯順序,先找到student類,然后查看student有沒有父類且這個父類沒有執行過 loda方法,發現有(pserson類),然后再查看person類有沒有沒調用過load方法的父類, 發現有一個NSObject,在遍歷NSObject有沒有沒調用過load方法的父類,發現其是基類, 沒有父類了所以,就先調用NSObject的load方法,然后接下來調用person的load方法,然后 再調用student的load方法 2.接下來找到person類,發現其不存在沒有調用過load方法的父類且其自己的load方法也被調用 過了,所以直接跳過了,沒有調用任何的load方法 3.最后來到了teacher類,查找其父類時,發現父類及更高級別的父類都實現了load方法,而自 己的load方法還沒有調用過,所以調用了teacher的load方法 所以調用順序是:NSObject的load方法->Person的load方法->sudent的loda方法->techer的load方法 因為我們無法修改NSObject的load方法實現,所以無法查看到它的方法打印
當所有類的都調用完load方法后,接下來開始調用分類的load方法↓↓
分類的load方法調用順序和分類的主類沒有任何關系,分類的調用順序很簡單:
就是完全按照編譯順序調用load方法,比如A有兩個分類a1,a2,B有兩個分類b1,b2,
分類的編譯順序是b1,a2,b2,a1,那么分類的load方法調用順序就是:
b1的load方法->a2的load方法->b2的load方法->a1的load方法
這個時候我們又會產生一個新的困惑?我們之前在調用方法時,比如我們調用一個對象的test方法,是根據isa指針去方法列表中查找,找到后就return不在向上或者向下繼續查找執行了,但是為什么load方法卻不這樣呢?為什么load方法在執行完父類的load方法后還繼續向下執行子類的load方法?
這是因為load方法並不是通過消息機制實現的,也就是不是通過objc_msgSend(obj,@selector(方法))來實現的,消息機制是找到對應的方法就return,而load方法是直接通過方法地址直接調用
以上就是有繼承和分類情況下類的load方法調用順序問題。
接下來來看initialize方法:
initialize方法是在一個類或其子類第一次接收到消息之前進行調用的,用來初始化,一個類只會被初始化一次
initialize在類或者其子類的第一個方法被調用前調用。即使類文件被引用進項目,但是沒有使用,initialize不會被調用
load方法是無論類有沒有被用到,只要添加被引入到項目就會被調用,而initialize則是在這個類或者其子類第一次調用方法的時候就會進行調用。
某個類調用方法時,是通過消息機制,也就是runtime的objc_msgSend方法實現的,所以initialize方法其實是在objc_msgSend進行判斷調用的
也就是當我們調用[teacher alloc],實際上是轉化為了objc_msgSend([teacher class],@selector(alloc))方法,而objc_msgSend([teacher class],@selector(alloc))的內部結構有對teacher的initialize進行了判斷,內部結構如下
objc_msgSend([teacher class],@selector(alloc)){ if([teacher class]沒有初始化){ //對teacher進行初始化 當然初始化並沒有這么簡單還涉及到了父類的初始化 objc_msgSend([teacher class],@selector(initialize)); } objc_msgSend([teacher class],@selector(alloc)) }
同樣在上面的項目中,我們重寫每個類及分類的initialize方法,調用teacher的alloc方法,
我們發現是先調用父類Person分類的initialize方法 然后在調用自己分類的initialize方法,
上面提到了,objc_msgSend方法會判斷類是否進行了初始化,沒有的話就進行初始化,
而對類的初始化過程,是優先對類的父類進行初始化的,也就是如下的結構
objc_msgSend([teacher class],@selector(initialize)){ if(teacher有父類 && teacher的父類沒有初始化){ //遞歸 有限初始化最頂級的父類 objc_msgSend([teacher父類 class],@selector(initialize)); } //標記 類已初始化 = yes; }
又因為initialize不同於load通過地址調用方法 ,而是通過消息機制來進行調用的,所以會遍歷類對象的方法列表,找到對應的方法就return了,而分類的方法位於主類方法前,后編譯的分類排序更靠前,所以先調用了父類person分類Kid的方法,然后調用了teacher分類english的方法
上面流程我們可以在源碼中找到依據:
首先調用方法是查看有沒有初始化,沒有的話就調用初始化操作
而初始化操作中先初始化父類
因為initialize是通過消息機制來實現的,所以當子類沒事實現initialize方法是,會根據supertclass指針去調用父類中的同名方法(對象本質中有講到)
也就是當我們注釋掉teacher類及其分類中initialize方法的實現再調用[teacher alloc]方法時發現 調用了兩次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
第一次打印是因為初始化teacher時會先初始化父類person,第二次打印是因為初始化teacher時沒有找到它的initialize方法,所以去父類中查找了
雖然調用了兩次person的initialize方法,但person只初始化了一次,第二次是初始化teacher
所以,initialize是當類第一次用到時就對調用,先調用父類的+initialize,再調用子類的initialize。
load方法和initialize方法都可以用來做什么操作?
首先 load方法和initialize方法有幾個相同點:
1>在不考慮開發者主動調用的情況下,系統最多會調用一次
2> 如果父類和子類都被調用,父類的調用一定在子類之前
+load
由於調用load方法時的環境很不安全,我們應該盡量減少load方法的邏輯,load很常見的一個使用場景,交換兩個方法的實現:
//摘自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方法一般只應該用來設置內部數據,比如,某個全局狀態無法在編譯期初始化,可以放在initialize里面。比如NSMutableArray這種類型的實例化依賴於runtime的消息發送,所以顯然無法在編譯器初始化:
// int類型可以在編譯期賦值 static int someNumber = 0; static NSMutableArray *someArray; + (void)initialize { if (self == [Person class]) { // 不方便編譯期復制的對象在這里賦值 someArray = [[NSMutableArray alloc] init]; } }
還有幾個注意點:
1》load調用時機比較早,運行環境不安全,所以在load方法中盡量不要涉及到其他的類。因為不同的類加載順序不同,當load調用時,其他類可能還沒加載完成,可能會導致使用到還沒加載的類從而出現問題;
2》load方法是線程安全的,它使用了鎖,我們應該避免線程阻塞在load方法(因為整個應用程序在執行load方法時會阻塞,即,程序會阻塞直到所有類的load方法執行完畢,才會繼續);initialize內部也使用了鎖,所以是線程安全的(即只有執行initialize的那個線程可以操作類或類實例。其他線程都要先阻塞,等待initialize執行完)。但同時要避免阻塞線程,不要再使用鎖。
3》iOS會在應用程序啟動的時候調用load
方法,在main
函數之前調用
4》在首次使用某個類之前,系統會向其發送initialize消息,通常應該在里面判斷當前要初始化的類,防止子類未覆寫initialize
的情況下調用兩次
4、關聯對象
分類中是可以使用屬性的,但不能創建成員變量的,而主類中是可以使用屬性與成員變量的
原因我們可以通過比較類與分類的底層結構可以看出,
分類的結構↓↓
類對象的結構↓↓
因為分類的實際結構中並沒有存放成員變量的數組,所以其是無法創建和使用成員變量的
而當我們在創建屬性時,其實這個屬性實際上是執行了一下操作:
@property(nonatomic,assign)double height; /** //1.聲明成員變量 { double _weight; } 2.實現set方法和get方法 - (void)setHeight:(double)height{ _height = height; } - (double)height{ return _height; } */
因為分類中沒有成員變量,所以分類中的屬性也就沒有自動去實現set方法和get方法,這也就導致了我們在使用分類屬性時出現crash↓↓
所以我們如果想讓分類中的屬性或成員變量能跟主類中一樣使用的話,需要通過運行時建立關聯引用
使用方法:重寫分類屬性的set/get方法↓↓
//首先需要導入runtime的頭文件 #import <objc/runtime.h>
- (void)setHeight:(double)height{ /** id _Nonnull object:這個參數是指屬性與哪個對象產生關聯?一般寫self即可 const void * _Nonnull key:這個是關聯屬性名 我一般都是直接寫屬性名 即@"height" id _Nullable value:關聯屬性的屬性值 也就是height objc_AssociationPolicy policy:這個參數一般是值屬性的修飾符 比如我們經常用copy來字符串 assign修飾基本數據類型 還有就是原子鎖,我們常用的就是不加鎖nonatomic */ //這就相當於把height這個屬性與self進行綁定,可以看成是相當於把@{@"height":@(height)}這個鍵值對存放在全局中的某個位置,可以讀取與設置 //height是基本類型 需要包裝成NSNumber objc_setAssociatedObject(self, @"height", @(height), OBJC_ASSOCIATION_ASSIGN); } - (double)height{ //這個是指根據key去取出對應的屬性值 這個需要注意的點事key一定要和set方法中的key一致 return [objc_getAssociatedObject(self, @"height") doubleValue]; }
關聯對象提供了以下API //添加關聯對象 void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy) //獲得關聯對象 id objc_getAssociatedObject(id object, const void * key) //移除所有的關聯對象 void objc_removeAssociatedObjects(id object)
關於objc_setAssociatedObject中objc_AssociationPolicy參數的使用:
關聯對象的原理我們可以通過源碼來查看 [objc4源碼解讀:objc-references.mm]
其中有幾個點需要注意:
1.關聯對象並不是存儲在被關聯對象本身內存中 主類的屬性是存儲到類對象自己的內存中的,但是通過關聯方式並不會把屬性添加到類對象內存中 而是將關聯對象存儲在全局的統一的一個AssociationsManager中 2.設置關聯對象為nil,就相當於是移除關聯對象
3.當關聯對象被銷毀時,AssociationsManager中存在所有與關聯對象綁定的信息都會被釋放
按照個人理解的方式應該是這樣
這個Map我們就可以理解為一個字典,里面存放着一個個鍵值對
所以通過上面的分析,我們可以回答一個經常被問道 的關於category的面試題
//Category能否添加成員變量?如果可以,如何給Category添加成員變量? 答:不能直接給Category添加成員變量,但是可以間接實現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)這兩個來實現。[重寫分類屬性點的set方法和get方法]
5、分類(Category)與類擴展(Extension)/繼承(Inherit)的區別
很多面試題經常會比較分類和類擴展的區別,首先我們要看一下什么是分類,什么是類擴展↓↓↓
分類的格式:
@interface待擴展的類(分類的名稱) @end @implementation待擴展的名稱(分類的名稱) @end
分類的創建:
類擴展的格式:
@interface XXX() //屬性 //方法(如果不實現,編譯時會報警,Method definition for 'XXX' not found) @end
類擴展的創建:
1.直接在類文件中添加interface代碼塊
2.
關於類擴展和分類的區別:
1、上面提到的分類不能添加成員變量【雖然可以添加屬性 但是一旦調用就會報方法找不到的錯誤】 (可以通過runtime給分類間接添加成員變量)
而類擴展可以添加成員變量;
2、分類中的屬性不會自動實現set方法和get方法,而類擴展中的屬性再轉為底層時是可以自動實現set、get方法
3、類擴展中添加的新方法,不實現會報警告。categorygory中定義了方法不實現則沒有這個問題
4、類擴展可以定義在.m文件中,這種擴展方式中定義的變量都是私有的,也可以定義在.h文件中,這樣定義的代碼就是共有的,類擴展在.m文件中聲明私有方法是非常好的方式。
5、類擴展不能像分類那樣擁有獨立的實現部分(@implementation部分),也就是說,類擴展所聲明的方法必須依托對應類的實現部分來實現。
分類(Categories) 和 繼承(Inherit) 可能有時候可以實現相同的功能,但其實兩個存在較大的差異,簡單介紹一下兩者的異同。
比如剛才上面的情況,我們調用一個方法是,系統的查找順序是先查找分類,分類沒有查找主類,主類沒有查找父類(分類沒有查找主類是因為分類 主類沒有查找父類是因為繼承)
有人可能會有疑問,既然是先查找分類再查找主類,這不是和繼承中的先查找子類方法,沒有的話再去父類查找是一樣的么,能否用集成來代替分類呢?
其實是不行的,雖然先查找分類再查找主類這個流程很像繼承(看着像是分類是繼承自主類的子類),但是兩者有很大區別,主要表現在兩點:
1、邏輯方面:兩者代表的層級關系不一樣,繼承代表父子關系,分類代表同級關系。
比如dog與animal是繼承關系,dog與cat是同級關系(dog是animal的子類,dog和cat是同級 都是animal的子類)
如果我們用繼承來代替分類,也就是cat繼承自dog,那么無論是可讀性還是邏輯表達上都是難以理解的
2、方法調用上:分類這種方式中,主類可以調用分類的方法,分類也可以調用主類的方法,可以相互調用,
而繼承則不行,子類可以調用父類的方法,但是父類卻不能調用子類的方法。
關於對category本質的解讀,下面這篇文章介紹的更為詳細,包括對源碼的查找與解讀,對理解category有很棒的幫助(分為三篇文章介紹的)