Category的概述以及實現(xiàn)原理
Category和Class Extension 類擴展的區(qū)別
Category中l(wèi)oad方法是什么時候調(diào)用的?
使用關聯(lián)對象為分類添加成員變量
load 與 initialize 的區(qū)別
作用
分類的主要作用就是在不改變原有類的前提下,動態(tài)地給這個類添加一些方法。具體在項目中大概有這么幾個好處:
1、分類在架構(gòu)設計上面可以達到解耦的效果。 開發(fā)過程中比較繁重啰嗦的業(yè)務代碼對項目的可讀性造成了壓力,為追求架構(gòu)清晰,降低維護成本低,可以通過分類進行梳理。典型的就是將項目中 AppDelegate 拆分。 AppDelegate 作為程序的入口,一般都會實現(xiàn)各種第三方 SDK 的初始化、寫各種版本的容錯代碼、實現(xiàn)通知、支付邏輯等等功能,所以 AppDelegate 這個類很容易臃腫,這個時候可以通過實現(xiàn) AppDelegate 分類來將不同的業(yè)務代碼分離。
2、通過實現(xiàn)分類的 load 方法來實現(xiàn) Method Swizzling
3、通過分類來為已知的類擴展方法和屬性,Category 不會為我們的屬性添加實例變量和存取方法,我們可以通過關聯(lián)對象這個技術來實現(xiàn)對象綁定
注意事項:
1:如果category中有和原有類同名的方法,會優(yōu)先調(diào)用分類中的方法,就是說會忽略原有類的方法。
2:如果多個分類中都有和原有類中同名的方法,那么調(diào)用該方法的時候執(zhí)行誰由編譯器決定,編譯器會執(zhí)行最后一個參與編譯的分類中的方法。
3:過渡使用分類 也會導致APP項目 支離破碎感+性能降低
4:我們在分類中添加了屬性之后,系統(tǒng)只是為我們生成了get方法和set方法的聲明,并沒有為我們生成成員變量和方法的實現(xiàn)
5: 名字相同的分類會引起編譯報錯;

Category 的實現(xiàn)原理
我們知道 Objective-C 通過 Runtime 運行時來實現(xiàn)動態(tài)語言這個特性,所有的類和對象,在 Runtime 中都是用結(jié)構(gòu)體來表示的,Category 在 Runtime 中是用結(jié)構(gòu)體 category_t 來表示的,下面是結(jié)構(gòu)體 category_t 具體表示:
typedef struct category_t {
const char *name;//類的名字 主類名字
classref_t cls;//類
struct method_list_t *instanceMethods;//實例方法的列表
struct method_list_t *classMethods;//類方法的列表
struct protocol_list_t *protocols;//所有協(xié)議的列表
struct property_list_t *instanceProperties;//添加的所有屬性
} category_t;
通過結(jié)構(gòu)體 category_t 可以知道,在 Category 中我們可以增加實例方法、類方法、協(xié)議、屬性。我們這里簡述下 Category 的實現(xiàn)原理:
1、在編譯時期,會將分類中實現(xiàn)的方法生成一個結(jié)構(gòu)體 method_list_t 、將聲明的屬性生成一個結(jié)構(gòu)體 property_list_t ,然后通過這些結(jié)構(gòu)體生成一個結(jié)構(gòu)體 category_t 。
2、然后將結(jié)構(gòu)體 category_t 保存下來
3、在運行時期,Runtime 會拿到編譯時期我們保存下來的結(jié)構(gòu)體 category_t。然后將結(jié)構(gòu)體 category_t 中的實例方法列表、協(xié)議列表、屬性列表添加到主類中。將結(jié)構(gòu)體 category_t 中的類方法列表、協(xié)議列表添加到主類的 metaClass 中。
- 在程序運行的時候,通過Runtime加載某個類的所有Category數(shù)據(jù),同時將Category中的方法、屬性、協(xié)議數(shù)據(jù)合并到一個大數(shù)組中,后來參與編譯的Category數(shù)據(jù),會在數(shù)組的前面。
- 將合并后的分類數(shù)據(jù)包括方法、屬性、協(xié)議等信息,插入到類原來數(shù)據(jù)的前面,所以這也就造成了如果分類和類中有相同的方法,調(diào)用的時候會優(yōu)先調(diào)用分類的方法,而且如果多個分類中有相同的名稱方法則會優(yōu)先調(diào)用最后參與編譯的
編譯順序
例如上圖,如果這兩個分類中有相同名稱的方法則最終會調(diào)用紅框中的
因為源碼太多 就不在一一貼圖解釋了,這里列舉了源碼的閱讀順序,感興趣的朋友可以自己嘗試著去仔細分析一下
objc-os.mm
_objc_init
map_images
map_images_nolock
objc-runtime-new.mm
_read_images
remethodizeClass
attachCategories
attachLists
realloc、memmove、 memcpy
Category 為什么不能添加實例變量
通過結(jié)構(gòu)體 category_t ,我們就可以知道,在 Category 中我們可以增加實例方法、類方法、協(xié)議、屬性。這里沒有 objc_ivar_list 結(jié)構(gòu)體,代表我們不可以在分類中添加實例變量。
因為在運行期,對象的內(nèi)存布局已經(jīng)確定,如果添加實例變量就會破壞類的內(nèi)部布局,這個就是 Category 中不能添加實例變量的根本原因。
使用關聯(lián)對象為分類添加成員變量
上面我們說到 在分類中添加了屬性之后,系統(tǒng)只是為我們生成了get方法和set方法的聲明,并沒有為我們生成成員變量和方法的實現(xiàn),下面我們來驗證這一說法
我們?yōu)?code>GSPerson對象添加了一個分類, 并且在分類中設置了一個height的屬性,下面我們來使用一下該分類對象:
#import "GSPerson.h"
@interface GSPerson (Utility)
@property (nonatomic, assign) int height;
@end

我們?yōu)?code>height賦值20 ,運行起來之后竟然奔潰了,并且提示我們
reason: '-[GSPerson setHeight:]: unrecognized selector sent to instance 0x1006259a0'
是不是印證了我們的說法?
那么我們該如何為Category添加一個成員變量呢?答案是關聯(lián)對象:
其實主要涉及到關聯(lián)對象的兩個方法:objc_getAssociatedObject和 objc_setAssociatedObject
/*
參數(shù)一:id object : 獲取哪個對象里面的關聯(lián)的屬性。
參數(shù)二:void * == id key : 什么屬性,與objc_setAssociatedObject中的key相對應,即通過key值取出value。
*/
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
/*
參數(shù)一:id object : 給哪個對象添加屬性,這里要給自己添加屬性,用self
參數(shù)二:void * == id key : 屬性名,根據(jù)key獲取關聯(lián)對象的屬性的值,在objc_getAssociatedObject中通過此key獲得屬性的值并返回。
參數(shù)三:id value : 關聯(lián)的值,也就是set方法傳入的值給屬性去保存。
參數(shù)四:objc_AssociationPolicy policy : 策略,屬性以什么形式保存。
*/
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
其中objc_AssociationPolicy policy策略是個枚舉值
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一個弱引用相關聯(lián)的對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相關對象的強引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相關的對象被復制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相關對象的強引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相關的對象被復制,原子性
};
具體的對應關系如下所示:

了解了這兩個方法后,為關聯(lián)對象添加屬性就非常的簡單了,比如為 GSPerson對象添加height屬性具體如下:
#import "GSPerson+Utility.h"
#import <objc/runtime.h>
@implementation GSPerson (Utility)
-(void)setHeight:(int)height{
//key值只要是一個指針即可,我們可以傳入@selector(name)
objc_setAssociatedObject(self, @selector(height), @(height), OBJC_ASSOCIATION_ASSIGN);
//或者
//objc_setAssociatedObject(self, @"height", @(height), OBJC_ASSOCIATION_ASSIGN);
}
-(int)height{
//_cmd == @selector(height)
return [objc_getAssociatedObject(self, _cmd) intValue];
//或者
//return [objc_getAssociatedObject(self, @"height") intValue];
}
@end
那么問題來了,我們用關聯(lián)對象為Category添加的成員變量具體是存儲在了哪呢?
下面我們通過一幅圖來了解下關聯(lián)對象的本質(zhì):

具體到更直觀的數(shù)據(jù)結(jié)構(gòu)上,我們可以看下圖

- 關聯(lián)對象是由
AssociationManager全局類管理并存儲在AssociationHashMap中。 - 所有對象的關聯(lián)內(nèi)容都存在于一個全局容器中,和宿主類是無關的。
Extension 類擴展
一般用類擴展做什么工作?
- 聲明私有屬性
- 聲明私有方法,一般沒有多大用,只是為了閱讀方便而已
- 聲明私有成員變量
類擴展有什么特點?
- 編譯時決議
- 只以聲明的形式存在,多數(shù)情況下存在于宿主類的.m文件中
- 不能為系統(tǒng)類添加擴展
Category和Class Extension 類擴展的區(qū)別
有了上面的特點,我們就方便的將分類和擴展放到一起比較了。
- 分類是在運行時才會將數(shù)據(jù)合并到類信息中。而類擴展在程序編譯的時候,它的數(shù)據(jù)就已經(jīng)包含在類信息中了
- 分類中既可以有聲明也可以有實現(xiàn),而類擴展只以聲明的形式存在,多數(shù)情況下存在于宿主類的.m文件中
- 可以為系統(tǒng)類添加分類,但是不能添加擴展
- 在分類中只能添加“方法”,不能增加成員變量。而擴展中既可以添加方法也可以添加成員變量。
Category中l(wèi)oad方法是什么時候調(diào)用的?

load方法在程序啟動加載類信息的時候就會調(diào)用。它是根據(jù)函數(shù)地址直接調(diào)用的,而不是通過消息機制的。調(diào)用順序如下:
-
先調(diào)用類的+load
- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
- 調(diào)用子類的+load之前會先調(diào)用父類的+load
-
再調(diào)用分類的+load
- 按照編譯的先后順序調(diào)用(先編譯,先調(diào)用)
load、initialize的區(qū)別
1. 調(diào)用順序
以main為分界,load方法在main函數(shù)之前執(zhí)行,initialize在main函數(shù)之后執(zhí)行
2.相同點和不同點
2.1 相同點
- load和initialize會被自動調(diào)用,不能手動調(diào)用它們。
- 子類實現(xiàn)了load和initialize的話,會隱式調(diào)用父類的load和initialize方法
- load和initialize方法內(nèi)部使用了鎖,因此它們是線程安全的。
2.2 不同點
子類中沒有實現(xiàn)load方法的話,不會調(diào)用父類的load方法;而子類如果沒有實現(xiàn)initialize方法的話,也會自動調(diào)用父類的initialize方法。
load方法是在類被裝在進來的時候就會調(diào)用,initialize在第一次給某個類發(fā)送消息時調(diào)用(比如實例化一個對象),并且只會調(diào)用一次,是懶加載模式,如果這個類一直沒有使用,就不回調(diào)用到initialize方法。
3. load
在執(zhí)行l(wèi)oad方法之前,會調(diào)用load_images方法,用來掃描鏡像中的+ load符號,將需要調(diào)用 load 方法的類添加到一個列表中loadable_classes,在這個列表中,會先把父類加入到待加載列表,這樣保證父類在子類前調(diào)用load方法,而分類中的load方法會在類的load的方法后面加入另外一個待加載列表loadable_categories,這樣保證了兩個規(guī)則:
- 父類先于子類調(diào)用
- 類先于分類調(diào)用
在掃描完load方法加入到待加載方法后,會調(diào)用call_load_methods,先從loadable_classes調(diào)用類的load方法,call_class_loads;調(diào)用完loadable_classes后會調(diào)用loadable_categories中分類的load方法,call_category_loads。
調(diào)用順序如下:
- 父類load先于類添加到loadable_classes列表,通過call_class_loads,調(diào)用列表中的load方法,這樣父類的load先于類的load執(zhí)行
- 當loadable_classes為空的時候,查看loadable_classes是否為空,如果不為空則調(diào)用call_category_loads加載分類中的load方法,這樣分類的load在類之后執(zhí)行
4. initialize
initialize 只會在對應類的方法第一次被調(diào)用時,才會調(diào)用,initialize 方法是在 alloc 方法之前調(diào)用的,alloc 的調(diào)用導致了前者的執(zhí)行。
initialize的調(diào)用棧中,直接調(diào)用其方法的其實是_class_initialize 這個C語言函數(shù),在這個方法中,主要是向為初始化的類發(fā)送+initialize消息,不過會強制父類先發(fā)送。
與 load 不同,initialize 方法調(diào)用時,所有的類都已經(jīng)加載到了內(nèi)存中。
5. 使用場景
5.1 load
load一般是用來交換方法Method Swizzle,由于它是線程安全的,而且一定會調(diào)用且只會調(diào)用一次,通常在使用UrlRouter的時候注冊類的時候也在load方法中注冊
5.2 initialize
initialize方法主要用來對一些不方便在編譯期初始化的對象進行賦值,或者說對一些靜態(tài)常量進行初始化操作
總結(jié):
一、調(diào)用方式
1、load是根據(jù)函數(shù)地址直接調(diào)用
2、initialize是通過objc_msgSend調(diào)用
二、調(diào)用時刻
1、load是runtime加載類、分類的時候調(diào)用(只會調(diào)用一次)
2、initialize是類第一次接收到消息的時候調(diào)用, 每一個類只會initialize一次(如果子類沒有實現(xiàn)initialize方法, 會調(diào)用父類的initialize方法, 所以父類的initialize方法可能會調(diào)用多次)
3、load比initialize先調(diào)用
三、使用場景
load 和 initialize 方法本質(zhì)都是做初始化的,只不過級別或者說針對的過程不一樣。load 只會調(diào)用一次,在 main 方法調(diào)用之前做初始化,比如方法交換。initialize 方法針對的是 main 方法之后,而且是懶加載(類第一次接收到消息的時候調(diào)用),使用到時才初始化。鑒于 objc_msgSend 的機制,存在多次調(diào)用的可能,但是可以使用代碼進行判斷。
四、load和initialize的調(diào)用順序
load調(diào)用順序
調(diào)用順序,需要同時滿足四種規(guī)則
先調(diào)用類的load, 在調(diào)用分類的load
先編譯的類, 優(yōu)先調(diào)用load
調(diào)用子類的load之前, 會先調(diào)用父類的load(父類 -> 子類)
沒有父子關系的類及分類之間,按Build Phases ->Complie Source 中的編譯順序
特點:
load在父類,子類,分類之間的調(diào)用不存在覆蓋,只存在先后執(zhí)行順序
initialize調(diào)用順序
initialize調(diào)用的優(yōu)先級為 (父類分類 > 父類) > (子類分類 > 子類)
initialize在父類,子類,父分類,子分類之間的調(diào)用存在覆蓋【(父類與子類)(父分類與子分類)本類與分類,兩兩存在覆蓋關系,兩組相互之間只會調(diào)用優(yōu)先級最高的一個】,有優(yōu)先級
特點:
優(yōu)先級:先分類后本類【分類覆蓋本類】
順序是:先父類后子類【存在先后執(zhí)行,不存在覆蓋】
通過消息機制調(diào)用, 當子類沒有initialize方法時, 會調(diào)用(父類分類>父類)的initialize方法, 所以(父類分類>父類)的initialize方法會調(diào)用多次
