一、Demo展示
創(chuàng)建一個Person類,在創(chuàng)建一個Person+eat和Person+test兩個分類。
@implementation Person
- (void)run {
NSLog(@"run");
}
@end
@implementation Person (test)
- (void)test {
NSLog(@"test");
}
@end
@implementation Person (eat)
- (void)eat {
NSLog(@"eat");
}
@end
// 運行下面代碼
Person *person = [[Person alloc] init];
[person run];
[person test];
[person eat];
當(dāng)然上面代碼,會打印出”run“/"test"/"eat"
我們知道當(dāng)我們調(diào)用一個方法是,底層會調(diào)用objc_msgSend(person, @selector(xxx))這個方法,根據(jù)OC對象的本質(zhì)得知,具體的實現(xiàn)是person 的isa 找到類對象里面的實例方法,如果是類方法,則會去元類對象找類方法。
思考如上 Demo里面的兩個分類會生成兩個新的類嗎?
不會,一個isa只會有一個類對象,程序會通過runtime動態(tài)將實例方法合并到類對象里面的對象方法中,類方法都會合并到元類對象的類方法中
二、Category 內(nèi)部實現(xiàn)
(一)demo1
使用clang編譯器把OC代碼轉(zhuǎn)成C++,在終端上cd到當(dāng)前項目的目錄,輸入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+eat.m會自動生成 .cpp 文件
打開上面生成的Person+eat.cpp文件,我們會看到下面代碼
struct _category_t {
const char *name;// 類的名稱
struct _class_t *cls;
const struct _method_list_t *instance_methods;// 實例方法列表
const struct _method_list_t *class_methods; // 類方法列表
const struct _protocol_list_t *protocols; // 協(xié)議列表
const struct _prop_list_t *properties; // 屬性屬性列表
};
// 生成的 Category
static struct _category_t _OBJC_$_CATEGORY_Person_$_eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person", // 給了我們上面的name
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_eat, // 就是我們的實例方法 instance_methods
0,
0,
0,
};
// _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_eat
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eat", "v16@0:8", (void *)_I_Person_eat_eat}}
};
(二)demo2
在Person+test.h類中添加
@property (assign, nonatomic) int weight;
@property (assign, nonatomic) double height;
在Person+test.m類中添加
+ (void)test {
NSLog(@"+test");
}
- (void)test1
{
NSLog(@"eat1");
}
+ (void)test2
{
}
+ (void)test3
{
}
然后同樣運行上面 clang 代碼xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+test.m 生成.cpp文件
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
//
static struct _category_t _OBJC_$_CATEGORY_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_test,
0,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_test,
};
// 實例,_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[2];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
2,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_I_Person_test_test},
{(struct objc_selector *)"test1", "v16@0:8", (void *)_I_Person_test_test1}}
};
// 類方法 _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_test
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[3];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
3,
{{(struct objc_selector *)"test", "v16@0:8", (void *)_C_Person_test_test},
{(struct objc_selector *)"test2", "v16@0:8", (void *)_C_Person_test_test2},
{(struct objc_selector *)"test3", "v16@0:8", (void *)_C_Person_test_test3}}
};
// 屬性列表 _OBJC_$_PROP_LIST_Person_$_test
static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[2];
} _OBJC_$_PROP_LIST_Person_$_test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
2,
{{"weight","Ti,N"},
{"height","Td,N"}}
};
也就是說,我們寫的分類都會變成_category_t這種結(jié)構(gòu)體,在合適的時機合并到類的對象方法或元類的類方法中。
(三)Category 的加載處理過程
- 通過 Runtime 加載某個類的所有Category 數(shù)據(jù)
2.把所有 Category 的方法、屬性、協(xié)議數(shù)據(jù)、合并到一個大數(shù)組中
- 后面參與編譯的 Category 數(shù)據(jù),會在數(shù)組的前面
3.將合并后的分類數(shù)據(jù)(方法、屬性、協(xié)議),插入到類原來數(shù)據(jù)的前面
所以,我們在開發(fā)過程中,如果分類和類中的方法名字相同,會調(diào)用分類里面的。
三、load函數(shù)在Category 中的加載
(一)demo
// Person
@implementation Person
+ (void)run {
NSLog(@"Person +run");
}
+ (void)load {
NSLog(@"Person +load");
}
@end
// Person+test
@implementation Person (test)
+ (void)load {
NSLog(@"Person (test) +load");
}
+ (void)test {
NSLog(@"Person (test) +test");
}
@end
// Person+eat
@implementation Person (eat)
+ (void)load {
NSLog(@"Person (eat) +load");
}
+ (void)eat {
NSLog(@"Person (eat) +eat");
}
@end
// 調(diào)用 Person 的 +run方法
[Person run];
我們運行程序發(fā)現(xiàn)
2020-06-11 23:18:48.786224+0800 TestDemo[18522:2035515] Person +load
2020-06-11 23:18:48.786723+0800 TestDemo[18522:2035515] Person (test) +load
2020-06-11 23:18:48.786785+0800 TestDemo[18522:2035515] Person (eat) +load
2020-06-11 23:18:48.786926+0800 TestDemo[18522:2035515] Person (eat) +run
思考在上面的 Category 內(nèi)部實現(xiàn)證明了, 如果該分類的方法和該類的方法名一樣,會優(yōu)先調(diào)用分類的方法,類里面的方法不會被調(diào)用。為什么 load 里面的方法都會被調(diào)用呢,而不是像 run 方法一樣?
load 方法的調(diào)用,是因為在程序加載過程中,如果發(fā)現(xiàn)是分類,會直接指向分類的類方法列表,而不是去調(diào)用的組合后的方法列表。所以會調(diào)用三次。
+load方法是根據(jù)方法地址直接調(diào)用,并不是經(jīng)過objc_msgSend函數(shù)調(diào)用
test 方法的調(diào)用,我們知道方法的調(diào)用實際就是調(diào)用
objc_megSend([Person class] @selector(test))會通過isa找到當(dāng)前對應(yīng)的類對象或元類對象,調(diào)用里面的方法列表。因為重新組裝的方法列表,Person+eat 分類在最前面,所以會調(diào)用 Person+eat 這個類中的 run 方法+load方法會在 Runtime 加載類、分類時間調(diào)用,并且每個類、分類的 +load 在程序運行過程中只會調(diào)用一次。
(二)load 調(diào)用順序
1、先調(diào)用類的 +load 方法
- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
- 調(diào)用子類的 +load 之前會先調(diào)用父類的 +load
2、在調(diào)用分類的 +load 方法
- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
擴展
打印出某個類中的所有方法
- (void)printMethodNamesOfClass:(Class )cls {
unsigned int count;
// 獲取方法數(shù)組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍歷所有的方法
for (int i = 0; i < count; i++) {
// 獲得方法
Method method = methodList[i];
// 獲得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 釋放
free(methodList);
// 打印方法名
NSLog(@"%@, %@", cls, methodNames);
}
問題
1、給一個存在的類添加兩個分類,會生成兩個新的類嗎?
不會,一個isa只會有一個類對象,程序會通過runtime動態(tài)將實例方法合并到類對象里面的對象方法中,類方法都會合并到元類對象的類方法中。
2、Category 的使用場合
- 給一個類添加新的方法,可以為系統(tǒng)的類擴展功能。
- 分解體積龐大的類文件,可以將一個類按照功能拆解成多個模塊,方便代碼管理。
- 創(chuàng)建對私有方法的前向引用:聲明私有方法,把Framework的私有方法公開等,直接調(diào)用其他類的私有方法時編譯器會報錯,這時候可以創(chuàng)建一個該類的分類,在分類中聲明這些私有方法(不必提供方法實現(xiàn)),接著導(dǎo)入這個分類的頭文件就可以正常調(diào)用這些私有方法。
- 向?qū)ο筇砑臃钦絽f(xié)議:創(chuàng)建一個 NSObject 或其子類的分類稱為 “創(chuàng)建一個非正式協(xié)議”。
正式協(xié)議是通過 protocol 指定的一系列方法的聲明,然后由遵守該協(xié)議的類自己去實現(xiàn)這些方法。而非正式協(xié)議是通過給 NSObject 或其子類添加一個分類來實現(xiàn)。非正式協(xié)議已經(jīng)漸漸被正式協(xié)議取代,正式協(xié)議最大的優(yōu)點就是可以使用泛型約束,而非正式協(xié)議不可以。)
3、Category中都可以添加哪些內(nèi)容
- 實例方法、類方法、協(xié)議、屬性(只生成setter和getter方法的聲明,不會生成setter和getter方法的實現(xiàn)以及下劃線成員變量)。
- 默認情況下,因為分類底層結(jié)構(gòu)的限制,不能添加成員變量到分類中,但可以通過關(guān)聯(lián)對象來間接實現(xiàn)這種效果。
4、Category的優(yōu)缺點、特點、注意點
| Category | 描述 |
|---|---|
| 優(yōu)點 | 1、使用場合, 2、可以按照需求加載不同的類。 |
| 缺點 | 1、不能直接添加成員變量,可以通過關(guān)聯(lián)對象實現(xiàn)這種效果, 2、分類方法會“覆蓋”同名的宿主類方法,如果使用不當(dāng)會造成問題 |
| 特點 | 1、運行時決議, 2、可以有聲明、可以有實現(xiàn)。 3、可以為系統(tǒng)的類添加分類, 運行時決議:Category 編譯之后的底層結(jié)構(gòu)是struct category_t,里面存儲著分類的對象方法、類方法、屬性、協(xié)議信息,這時候分類中的數(shù)據(jù)還沒有合并到類中,而是在程序運行的時候通過Runtime機制將所有分類數(shù)據(jù)合并到類(類對象、元類對象)中去。(這是分類最大的特點,也是分類和擴展的最大區(qū)別,擴展是在編譯的時候就將所有數(shù)據(jù)都合并到類中去了) |
| 注意點 | 1、分類方法會“覆蓋”同名的宿主類方法,如果使用不當(dāng)會造成問題; 2、同名分類方法誰能生效取決于編譯順序,最后參與編譯的分類中的同名方法會最終生效; 3、名字相同的分類會引起編譯報錯。 |
5、Category 的實現(xiàn)原理
- 分類的實現(xiàn)原理取決于運行時決議;
- 同名分類方法誰能生效取決于編譯順序,最后參與編譯的分類中的同名方法會最終生效;
- 分類方法會“覆蓋”同名的宿主類(原類)方法,這里說的“覆蓋”并不是指原來的方法沒了。消息傳遞過程中優(yōu)先查找宿主類中靠前的元素,找到同名方法就進行調(diào)用,但實際上宿主類中原有同名方法的實現(xiàn)仍然是存在的。我們可以通過一些手段來調(diào)用到宿主類原有同名方法的實現(xiàn),如可以通過Runtime的class_copyMethodList方法打印類的方法列表,找到宿主類方法的imp,進行調(diào)用(可以交換方法實現(xiàn))。
6、Category的加載處理過程
在編譯時,Category 中的數(shù)據(jù)還沒有合并到類中,而是在程序運行的時候通過Runtime機制將所有分類數(shù)據(jù)合并到類(類對象、元類對象)中去。下面我們來看一下 Category 的加載處理過程。
① 通過Runtime加載某個類的所有 Category 數(shù)據(jù);
② 把所有的分類數(shù)據(jù)(方法、屬性、協(xié)議),合并到一個大數(shù)組中;(后面參與編譯的 Category 數(shù)據(jù),會在數(shù)組的前面)
③ 將合并后的分類數(shù)據(jù)(方法、屬性、協(xié)議),插入到宿主類原來數(shù)據(jù)的前面。(所以會優(yōu)先調(diào)用最后參與編譯的分類中的同名方法)