Category分類

一、Demo展示

創(chuàng)建一個Person類,在創(chuàng)建一個Person+eatPerson+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 的加載處理過程

  1. 通過 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)用最后參與編譯的分類中的同名方法)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容