Category分類

一竹握、Demo展示

創(chuàng)建一個(gè)Person類啦辐,在創(chuàng)建一個(gè)Person+eatPerson+test兩個(gè)分類。

@implementation Person
- (void)run {
    NSLog(@"run");
}
@end

@implementation Person (test)
- (void)test {
    NSLog(@"test");
}
@end

@implementation Person (eat)
- (void)eat {
    NSLog(@"eat");
}
@end

// 運(yùn)行下面代碼
Person *person = [[Person alloc] init];

[person run];
[person test];
[person eat];
        

當(dāng)然上面代碼续挟,會(huì)打印出”run“/"test"/"eat"
我們知道當(dāng)我們調(diào)用一個(gè)方法是侥衬,底層會(huì)調(diào)用objc_msgSend(person, @selector(xxx))這個(gè)方法轴总,根據(jù)OC對(duì)象的本質(zhì)得知怀樟,具體的實(shí)現(xiàn)是person 的isa 找到類對(duì)象里面的實(shí)例方法,如果是類方法脖含,則會(huì)去元類對(duì)象找類方法。

思考如上 Demo里面的兩個(gè)分類會(huì)生成兩個(gè)新的類嗎征堪?
不會(huì),一個(gè)isa只會(huì)有一個(gè)類對(duì)象庸娱,程序會(huì)通過runtime動(dòng)態(tài)將實(shí)例方法合并到類對(duì)象里面的對(duì)象方法中熟尉,類方法都會(huì)合并到元類對(duì)象的類方法

二斤儿、Category 內(nèi)部實(shí)現(xiàn)

(一)demo1

使用clang編譯器把OC代碼轉(zhuǎn)成C++恐锦,在終端上cd到當(dāng)前項(xiàng)目的目錄一铅,輸入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+eat.m會(huì)自動(dòng)生成 .cpp 文件

打開上面生成的Person+eat.cpp文件潘飘,我們會(huì)看到下面代碼

struct _category_t {
    const char *name;// 類的名稱
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;// 實(shí)例方法列表
    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, // 就是我們的實(shí)例方法 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
{
    
}

然后同樣運(yùn)行上面 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,
};

// 實(shí)例戈擒,_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"}}
};

也就是說赘来,我們寫的分類都會(huì)變成_category_t這種結(jié)構(gòu)體犬辰,在合適的時(shí)機(jī)合并到類的對(duì)象方法或元類的類方法中冰单。

(三)Category 的加載處理過程

  1. 通過 Runtime 加載某個(gè)類的所有Category 數(shù)據(jù)

2.把所有 Category 的方法诫欠、屬性、協(xié)議數(shù)據(jù)轿偎、合并到一個(gè)大數(shù)組中

  • 后面參與編譯的 Category 數(shù)據(jù)坏晦,會(huì)在數(shù)組的前面

3.將合并后的分類數(shù)據(jù)(方法嫁乘、屬性、協(xié)議)仓蛆,插入到類原來數(shù)據(jù)的前面

所以看疙,我們?cè)陂_發(fā)過程中直奋,如果分類和類中的方法名字相同帮碰,會(huì)調(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];

我們運(yùn)行程序發(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)部實(shí)現(xiàn)證明了一死, 如果該分類的方法和該類的方法名一樣投慈,會(huì)優(yōu)先調(diào)用分類的方法,類里面的方法不會(huì)被調(diào)用加袋。為什么 load 里面的方法都會(huì)被調(diào)用呢职烧,而不是像 run 方法一樣蚀之?

  • load 方法的調(diào)用捷泞,是因?yàn)樵诔绦蚣虞d過程中锁右,如果發(fā)現(xiàn)是分類骡湖,會(huì)直接指向分類的類方法列表响蕴,而不是去調(diào)用的組合后的方法列表浦夷。所以會(huì)調(diào)用三次辜王。

  • +load方法是根據(jù)方法地址直接調(diào)用呐馆,并不是經(jīng)過objc_msgSend函數(shù)調(diào)用

  • test 方法的調(diào)用汹来,我們知道方法的調(diào)用實(shí)際就是調(diào)用 objc_megSend([Person class] @selector(test)) 會(huì)通過isa找到當(dāng)前對(duì)應(yīng)的類對(duì)象或元類對(duì)象改艇,調(diào)用里面的方法列表坟岔。因?yàn)橹匦陆M裝的方法列表社付,Person+eat 分類在最前面鸥咖,所以會(huì)調(diào)用 Person+eat 這個(gè)類中的 run 方法

  • +load方法會(huì)在 Runtime 加載類扛或、分類時(shí)間調(diào)用熙兔,并且每個(gè)類住涉、分類的 +load 在程序運(yùn)行過程中只會(huì)調(diào)用一次舆声。

(二)load 調(diào)用順序

1、先調(diào)用的 +load 方法

  • 按照編譯先后順序調(diào)用(先編譯蛾找,先調(diào)用)
  • 調(diào)用子類的 +load 之前會(huì)先調(diào)用父類的 +load

2打毛、在調(diào)用分類的 +load 方法

  • 按照編譯先后順序調(diào)用(先編譯柿赊,先調(diào)用)

擴(kuò)展

打印出某個(gè)類中的所有方法

- (void)printMethodNamesOfClass:(Class )cls {
    
    unsigned int count;
    // 獲取方法數(shù)組
    Method *methodList = class_copyMethodList(cls, &count);
    // 存儲(chǔ)方法名
    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、給一個(gè)存在的類添加兩個(gè)分類幻枉,會(huì)生成兩個(gè)新的類嗎碰声?

不會(huì),一個(gè)isa只會(huì)有一個(gè)類對(duì)象熬甫,程序會(huì)通過runtime動(dòng)態(tài)將實(shí)例方法合并到類對(duì)象里面的對(duì)象方法中胰挑,類方法都會(huì)合并到元類對(duì)象的類方法中。

2、Category 的使用場(chǎng)合

  • 給一個(gè)類添加新的方法洽腺,可以為系統(tǒng)的類擴(kuò)展功能脚粟。
  • 分解體積龐大的類文件,可以將一個(gè)類按照功能拆解成多個(gè)模塊蘸朋,方便代碼管理核无。
  • 創(chuàng)建對(duì)私有方法的前向引用:聲明私有方法团南,把Framework的私有方法公開等,直接調(diào)用其他類的私有方法時(shí)編譯器會(huì)報(bào)錯(cuò)拷橘,這時(shí)候可以創(chuàng)建一個(gè)該類的分類,在分類中聲明這些私有方法(不必提供方法實(shí)現(xiàn)),接著導(dǎo)入這個(gè)分類的頭文件就可以正常調(diào)用這些私有方法。
  • 向?qū)ο筇砑臃钦絽f(xié)議:創(chuàng)建一個(gè) NSObject 或其子類的分類稱為 “創(chuàng)建一個(gè)非正式協(xié)議”。

正式協(xié)議是通過 protocol 指定的一系列方法的聲明没龙,然后由遵守該協(xié)議的類自己去實(shí)現(xiàn)這些方法碘梢。而非正式協(xié)議是通過給 NSObject 或其子類添加一個(gè)分類來實(shí)現(xiàn)。非正式協(xié)議已經(jīng)漸漸被正式協(xié)議取代在扰,正式協(xié)議最大的優(yōu)點(diǎn)就是可以使用泛型約束,而非正式協(xié)議不可以。)

3娜汁、Category中都可以添加哪些內(nèi)容

  • 實(shí)例方法傅事、類方法、協(xié)議般又、屬性(只生成setter和getter方法的聲明萤衰,不會(huì)生成setter和getter方法的實(shí)現(xiàn)以及下劃線成員變量)倦卖。
  • 默認(rèn)情況下,因?yàn)榉诸惖讓咏Y(jié)構(gòu)的限制掸茅,不能添加成員變量到分類中,但可以通過關(guān)聯(lián)對(duì)象來間接實(shí)現(xiàn)這種效果。

4、Category的優(yōu)缺點(diǎn)沪悲、特點(diǎn)涉馁、注意點(diǎn)

Category 描述
優(yōu)點(diǎn) 1糠悯、使用場(chǎng)合,
2、可以按照需求加載不同的類阅悍。
缺點(diǎn) 1假栓、不能直接添加成員變量,可以通過關(guān)聯(lián)對(duì)象實(shí)現(xiàn)這種效果,
2贞滨、分類方法會(huì)“覆蓋”同名的宿主類方法骄噪,如果使用不當(dāng)會(huì)造成問題
特點(diǎn) 1、運(yùn)行時(shí)決議,
2掌实、可以有聲明忱嘹、可以有實(shí)現(xiàn)础米。
3蘑斧、可以為系統(tǒng)的類添加分類花颗,
運(yùn)行時(shí)決議:Category 編譯之后的底層結(jié)構(gòu)是struct category_t,里面存儲(chǔ)著分類的對(duì)象方法、類方法、屬性、協(xié)議信息搀矫,這時(shí)候分類中的數(shù)據(jù)還沒有合并到類中噪馏,而是在程序運(yùn)行的時(shí)候通過Runtime機(jī)制將所有分類數(shù)據(jù)合并到類(類對(duì)象、元類對(duì)象)中去桃移。(這是分類最大的特點(diǎn)疮装,也是分類和擴(kuò)展的最大區(qū)別呻纹,擴(kuò)展是在編譯的時(shí)候就將所有數(shù)據(jù)都合并到類中去了)
注意點(diǎn) 1、分類方法會(huì)“覆蓋”同名的宿主類方法渔工,如果使用不當(dāng)會(huì)造成問題兰吟;
2遵湖、同名分類方法誰能生效取決于編譯順序,最后參與編譯的分類中的同名方法會(huì)最終生效;
3赦颇、名字相同的分類會(huì)引起編譯報(bào)錯(cuò)沪摄。

5、Category 的實(shí)現(xiàn)原理

  • 分類的實(shí)現(xiàn)原理取決于運(yùn)行時(shí)決議屋吨;
  • 同名分類方法誰能生效取決于編譯順序直秆,最后參與編譯的分類中的同名方法會(huì)最終生效歇竟;
  • 分類方法會(huì)“覆蓋”同名的宿主類(原類)方法,這里說的“覆蓋”并不是指原來的方法沒了梯醒。消息傳遞過程中優(yōu)先查找宿主類中靠前的元素籽慢,找到同名方法就進(jìn)行調(diào)用箱亿,但實(shí)際上宿主類中原有同名方法的實(shí)現(xiàn)仍然是存在的脑豹。我們可以通過一些手段來調(diào)用到宿主類原有同名方法的實(shí)現(xiàn)击碗,如可以通過Runtime的class_copyMethodList方法打印類的方法列表甲馋,找到宿主類方法的imp,進(jìn)行調(diào)用(可以交換方法實(shí)現(xiàn))液茎。

6逞姿、Category的加載處理過程

在編譯時(shí),Category 中的數(shù)據(jù)還沒有合并到類中捆等,而是在程序運(yùn)行的時(shí)候通過Runtime機(jī)制將所有分類數(shù)據(jù)合并到類(類對(duì)象滞造、元類對(duì)象)中去。下面我們來看一下 Category 的加載處理過程栋烤。

① 通過Runtime加載某個(gè)類的所有 Category 數(shù)據(jù)断部;
② 把所有的分類數(shù)據(jù)(方法、屬性班缎、協(xié)議)蝴光,合并到一個(gè)大數(shù)組中;(后面參與編譯的 Category 數(shù)據(jù)达址,會(huì)在數(shù)組的前面)
③ 將合并后的分類數(shù)據(jù)(方法蔑祟、屬性、協(xié)議)沉唠,插入到宿主類原來數(shù)據(jù)的前面疆虚。(所以會(huì)優(yōu)先調(diào)用最后參與編譯的分類中的同名方法)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子径簿,更是在濱河造成了極大的恐慌罢屈,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篇亭,死亡現(xiàn)場(chǎng)離奇詭異缠捌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)译蒂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門曼月,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柔昼,你說我怎么就攤上這事哑芹。” “怎么了捕透?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵聪姿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我乙嘀,道長(zhǎng)末购,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任乒躺,我火速辦了婚禮招盲,結(jié)果婚禮上低缩,老公的妹妹穿的比我還像新娘嘉冒。我一直安慰自己,他們只是感情好咆繁,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布讳推。 她就那樣靜靜地躺著,像睡著了一般玩般。 火紅的嫁衣襯著肌膚如雪银觅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天坏为,我揣著相機(jī)與錄音究驴,去河邊找鬼。 笑死匀伏,一個(gè)胖子當(dāng)著我的面吹牛洒忧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播够颠,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼熙侍,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蛉抓,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤庆尘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后巷送,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體驶忌,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年惩系,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了位岔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡堡牡,死狀恐怖抒抬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晤柄,我是刑警寧澤擦剑,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站芥颈,受9級(jí)特大地震影響惠勒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜爬坑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一纠屋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盾计,春花似錦售担、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哭尝,卻和暖如春哥攘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背材鹦。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國打工逝淹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人桶唐。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓栅葡,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親莽红。 傳聞我的和親對(duì)象是個(gè)殘疾皇子妥畏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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