Category 面試題總結(jié)

Category(分類)這一Object-C 2.0之后添加的語(yǔ)言特性,在日常開發(fā)中使用頻率非常高。而且面試時(shí)Category基本上是都會(huì)涉及到的一個(gè)知識(shí)點(diǎn)。下面羅列一下面試中經(jīng)常會(huì)提出的問題,基本上涵蓋了這個(gè)知識(shí)點(diǎn):

  1. Category和Extension的區(qū)別。
  2. Category底層實(shí)現(xiàn)原理
  3. Category的加載處理過程
  4. Category中 + load方法的調(diào)用
  5. Category中 + initialize方法的調(diào)用
  6. Category中l(wèi)oad和initialize方法的區(qū)別
  7. Category中添加成員變量的實(shí)現(xiàn)

1. Category和Extension的區(qū)別。

  • Category是在程序運(yùn)行的時(shí)候,runtime會(huì)將Category的數(shù)據(jù)合并到類信息匯中。
  • Class Extension 是在編譯的時(shí)候,就已經(jīng)將數(shù)據(jù)包含在類信息中。

2. Category底層實(shí)現(xiàn)原理

Category編譯之后的底層結(jié)構(gòu)是 struct category_t ,里面存儲(chǔ)著分類的對(duì)象方法,類方法,屬性,協(xié)議信息。


3. Category的加載處理過程

下面創(chuàng)建了4個(gè)類,一個(gè)People類和3個(gè)People類的分類(Run、Jump、Eat)。
這4個(gè)類都實(shí)現(xiàn)了 - instanceMethod這個(gè)實(shí)例方法。
調(diào)用People的這個(gè)實(shí)例方法,查看打印結(jié)果。

@interface People : NSObject
- (void)instanceMethod;
@end

@implementation People
- (void)instanceMethod
{
    NSLog(@"people instanceMethod");
}
@end
@interface People (Run)
- (void)instanceMethod;
@end

@implementation People (Run)
- (void)instanceMethod
{
    NSLog(@"people run instanceMethod");
}
@end
@interface People (Jump)
- (void)instanceMethod;
@end

@implementation People (Jump)
- (void)instanceMethod
{
    NSLog(@"people jump instanceMethod");
}
@end
@interface People (Eat)
- (void)instanceMethod;
@end

@implementation People (Eat)
- (void)instanceMethod
{
    NSLog(@"people eat instanceMethod");
}
@end

查看People類中的方法列表:

#import "People.h"
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        unsigned int count;
        Method *methodList = class_copyMethodList([People class], &count);
        for (int i = 0; i < count; i ++) {
            Method method = methodList[I];
            NSLog(@"%@",NSStringFromSelector(method_getName(method)));
        }
        free(methodList);
    }
    return 0;
}
People class method list

發(fā)現(xiàn)People類中有4個(gè)instanceMethod方法,分類中的instanceMethod也在People類中。而且這時(shí)沒有調(diào)用People的實(shí)例方法,是在runtime運(yùn)行中加載了People類之后,Category的所有數(shù)據(jù)插入到了People類中。

下面調(diào)用一下People類的實(shí)例方法:

#import <Foundation/Foundation.h>
#import "People.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        People *people = [[People alloc] init];
        [people instanceMethod];   // 打印結(jié)果為:people run instanceMethod
        
    }
    return 0;

打印結(jié)果為 people run instanceMethod
從結(jié)果來看,調(diào)用People的實(shí)例方法時(shí)調(diào)用了分類的方法,也就是所有分類的方法都合并到一個(gè)數(shù)組中,然后插入到原有類的前面,但是為什么是People (Run)分類覆蓋了實(shí)例方法,而不是其他兩個(gè)?

在TARGETS中查看一下編譯文件排序:

TARGETS - Compile Sources.png

發(fā)現(xiàn) People+Run.m是最后編譯的。也就是說編譯順序在最后的方法會(huì)排在方法列表的最前面。

所以Category的加載處理過程是:
1. 通過runtime加載某個(gè)類的所有的Category數(shù)據(jù)。
2. 將所有的Category數(shù)據(jù)(方法、屬性、協(xié)議)合并成到一個(gè)大數(shù)組中。這些數(shù)據(jù)后面參與編譯的Category數(shù)據(jù),會(huì)保存在數(shù)組的前面。
3. 將合并后的分類數(shù)據(jù)(方法、屬性、協(xié)議)插入到類的原來的數(shù)據(jù)的前面。


4. Category中 + load方法的調(diào)用

- Category有l(wèi)oad方法。
- load方法在Runtime加載類、分類時(shí)就會(huì)調(diào)用。
- 每個(gè)類、分類在程序運(yùn)行過程中,只調(diào)用一次load方法。

創(chuàng)建6個(gè)類,之間的關(guān)系是:
Animal : NSObject
People : NSObject
Student : People
People Category : People+Run , People+Jump , People+Eat

Animal 、 People 繼承自 NSObject;
Student 繼承自People
People+Run , People+Jump , People+Eat 是People的分類

分別實(shí)現(xiàn)一下load方法:

@implementation Animal
+ (void)load
{
    NSLog(@"animal load method");
}
@end
@implementation People
+ (void)load
{
    NSLog(@"people load method");
}
@end
@interface Student : People

@end

@implementation Student
+ (void)load
{
    NSLog(@"student load method");
}
@end
@implementation People (Run)
+ (void)load
{
    NSLog(@"people run load method");
}
@end
@implementation People (Jump)
+ (void)load
{
    NSLog(@"people jump load method");
}
@end
@implementation People (Eat)
+ (void)load
{
    NSLog(@"people eat load method");
}
@end

然后在main.m中不引入類的頭文件:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
}

類的編譯順序是:

編譯順序

按照之前的思路,打印的順序應(yīng)該是:
student、jump、animal、eat、People、run
或者是:
run、People、eat、animal、jump、student

但是打印結(jié)果不是這樣,打印出結(jié)果:

+ load method result

原因是調(diào)用+load 方法不是通過消息發(fā)送機(jī)制(objc_msgSend),而是根據(jù)內(nèi)存中函數(shù)地址直接調(diào)用。而且是在runtime加載類、分類時(shí)調(diào)用。

+load方法調(diào)用順序總結(jié)如下:

  • +load方法時(shí)在runtime加載類、分類的時(shí)候調(diào)用。
  • 每個(gè)類、分類的+load方法在程序運(yùn)行中只調(diào)用一次
  1. 先調(diào)用類的+load方法
    1.1 調(diào)用類的+load方法時(shí),按照編譯先后順序調(diào)用(先調(diào)用Student再調(diào)用Animal)
    1.2 調(diào)用子類的+load方法時(shí),先調(diào)用父類的+load方法(調(diào)用Student時(shí),先調(diào)用People,再調(diào)用Student)
    于是調(diào)用順序是:People、Student、Animal
  2. 再調(diào)用分類的+load方法
    2.1 調(diào)用分類+load方法時(shí),按照編譯先后順序調(diào)用

PS. 如果是手動(dòng)調(diào)用 load方法,則會(huì)觸發(fā)消息機(jī)制(objc_msgSend)調(diào)用。按照消息機(jī)制調(diào)用順序執(zhí)行。但是一般不會(huì)手動(dòng)調(diào)用load方法。


5. Category中+ initialize方法的調(diào)用

+initialize是在類第一次接收消息時(shí)調(diào)用的。

創(chuàng)建幾個(gè)類,他們之間的關(guān)系是:
People : NSObject
Student : People
People Category : People+Run , People+Jump , People+Eat

People 繼承自 NSObject;
Student 繼承自People
People+Run , People+Jump , People+Eat 是People的分類

分別實(shí)現(xiàn) + initialize 方法:

@interface People : NSObject
@end

@implementation People
+(void)initialize
{
    NSLog(@"people initialize");
}
@end
@interface Student : People
@end

@implementation Student
+(void)initialize
{
    NSLog(@"student initialize");
}
@end
@implementation People (Run)
+(void)initialize
{
    NSLog(@"people run initialize");
}
@end
@implementation People (Jump)
+(void)initialize
{
    NSLog(@"people jump initialize");
}
@end
@implementation People (Eat)
+(void)initialize
{
    NSLog(@"people eat initialize");
}
@end

分別調(diào)用People的alloc方法和Student的alloc方法:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [People alloc];
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [Student alloc];
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 分別調(diào)用People和Student的alloc
        [People alloc]; 
        [Student alloc];
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 調(diào)用一次People allocation,三次Student allocation
        [People alloc];
        [Student alloc];
        [Student alloc];
        [Student alloc];
    }
    return 0;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 調(diào)用三次Student allocation
        [Student alloc];
        [Student alloc];
        [Student alloc];
    }
    return 0;
}

編譯的順序是:

initialize類編譯順序
// 打印結(jié)果
[People alloc];
 --> people run initialize

[Student alloc]; 
--> people run initialize 
--> student initialize

[People alloc];
[Student alloc]; 
--> people run initialize 
--> student initialize

[People alloc];
[Student alloc]; 
[Student alloc]; 
[Student alloc]; 
--> people run initialize 
--> student initialize

[Student alloc]; 
[Student alloc]; 
[Student alloc]; 
--> people run initialize 
--> student initialize

發(fā)現(xiàn)有幾個(gè)現(xiàn)象:

  • 調(diào)用People alloc時(shí)打印的是People分類Run的 initialize方法
  • 調(diào)用Student alloc時(shí)打印的是People分類Run的initialize方法和Student initialize方法
  • 調(diào)用People 和 Student的alloc時(shí)打印的還是和調(diào)用Student alloc一樣的結(jié)果
  • 多次調(diào)用Student alloc時(shí)打印的結(jié)果和調(diào)用一次Student alloc的一樣

所以得出以下幾個(gè)結(jié)論:

  • +initialize是類第一次接收消息的時(shí)候調(diào)用
  • +initialize是通過objc_msgSend(消息機(jī)制)調(diào)用,所以分類方法會(huì)覆蓋類方法
  • 調(diào)用子類(Student)的+initialize方法時(shí)底層會(huì)先調(diào)用父類(People)的+initialize方法,再調(diào)用子類的方法
    objc_msgSend([People class], @selector(initialize));
    objc_msgSend([People class], @selector(initialize));
  • 每個(gè)類只會(huì)初始化一次(只調(diào)用一次initialize),多次接收消息只調(diào)用一次+initialize方法

因?yàn)? initialize是通過objc_msgSend調(diào)用的,所以會(huì)有以下特點(diǎn):

  • 如果子類沒有實(shí)現(xiàn) + initialize方法,會(huì)調(diào)用父類的 + initialize方法。所以當(dāng)多個(gè)子類都沒有實(shí)現(xiàn) + initialize方法的話,會(huì)多次調(diào)用父類 + initialize方法。

  • 當(dāng)分類實(shí)現(xiàn)了 + initialize方法,會(huì)覆蓋類本身的 + initialize方法調(diào)用。因?yàn)镃ategory的加載過程是將所有的Category的方法、屬性、協(xié)議信息合成一個(gè)大數(shù)組,再將這個(gè)大數(shù)組插入到類信息的前面。Category中編譯越靠后越優(yōu)先調(diào)用。


6. Category中l(wèi)oad和initialize方法的區(qū)別

Category 中 + load 和 + initialize 方法的區(qū)別總結(jié)如下:

調(diào)用方式
  1. +load是根據(jù)方法函數(shù)的內(nèi)存地址直接調(diào)用
  2. +initialize是通過objc_msgSend調(diào)用
調(diào)用時(shí)刻
  1. +load是runtime加載類、分類時(shí)調(diào)用(只會(huì)調(diào)用一次)
  2. +initialize是類第一次接收消息時(shí)調(diào)用,每一個(gè)類只會(huì)初始化(initialize)一次,但是父類的+ initialize方法可能會(huì)調(diào)用多次。
調(diào)用順序
  1. +load
    1.1 先調(diào)用類的+load方法
    編譯越早,調(diào)用越早
    調(diào)用子類的+load方法時(shí),先調(diào)用父類的+load方法
    1.2 再調(diào)用分類的+load方法
    編譯越早,調(diào)用越早

  2. +initialize
    2.1 先初始化父類
    2.2 再初始化子類,若子類沒有實(shí)現(xiàn)+initialize方法,最終還是會(huì)調(diào)用父類的+initialize方法
    2.3 如果分類實(shí)現(xiàn)了+initialize方法,會(huì)覆蓋類的+initialize方法。編譯越晚,調(diào)用越早。


7. Category中添加成員變量的實(shí)現(xiàn)

一個(gè)類中如果寫一個(gè)屬性的話,編譯器會(huì)自動(dòng)做3件事情:

  1. 生成一個(gè)成員變量
  2. 生成成員變量的getter、setter聲明
  3. 生成getter和setter的實(shí)現(xiàn)

但是如果在一個(gè)分類中寫一個(gè)屬性,編譯器只會(huì)做1件事情:

  1. 生成getter和setter的聲明

根據(jù)分類的結(jié)構(gòu),不能直接給分類添加一個(gè)成員變量,但是可以間接實(shí)現(xiàn)分類有成員變量的效果:使用關(guān)聯(lián)對(duì)象(Association Object)。

關(guān)聯(lián)對(duì)象是runtime中的方法,使用時(shí)需要引入<objc/runtime.h>

關(guān)聯(lián)對(duì)象主要的方法有3個(gè):

  1. 設(shè)置關(guān)聯(lián)對(duì)象
    OBJC_EXPORT void
    objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)

返回類型為 void,其中有4個(gè)參數(shù):
id _Nonnull object : 給哪一個(gè)對(duì)象添加關(guān)聯(lián)對(duì)象
const void * _Nonnull key :傳入一個(gè)指針進(jìn)去,接收的是地址值
id _Nullable value :關(guān)聯(lián)什么值
objc_AssociationPolicy policy :關(guān)聯(lián)的策略

關(guān)聯(lián)策略:

objc_AssociationPolicy :

// 給關(guān)聯(lián)對(duì)象指向一個(gè)弱引用
OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
// 給關(guān)聯(lián)對(duì)象指向一個(gè)強(qiáng)引用,這個(gè)關(guān)聯(lián)對(duì)象是非原子性
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
// 給關(guān)聯(lián)對(duì)象指向copy,這個(gè)關(guān)聯(lián)對(duì)象是非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
// 給關(guān)聯(lián)對(duì)象指向一個(gè)強(qiáng)引用,這個(gè)關(guān)聯(lián)對(duì)象是原子性
OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
// 給關(guān)聯(lián)對(duì)象指向copy,這個(gè)關(guān)聯(lián)對(duì)象是非原子性
OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};

// 關(guān)聯(lián)對(duì)象策略對(duì)應(yīng)的修飾符:
// 關(guān)聯(lián)對(duì)象策略中沒有weak修飾符,沒有弱引用這種效果
OBJC_ASSOCIATION_ASSIGN            === assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC  === strong,nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC    === copy,nonatomic
OBJC_ASSOCIATION_RETAIN            === strong,atomic
OBJC_ASSOCIATION_COPY              === copy,atomic
  1. 獲取關(guān)聯(lián)對(duì)象
    OBJC_EXPORT id _Nullable
    objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)

返回類型為 id,其中有2個(gè)參數(shù):
id _Nonnull object : 獲取哪一個(gè)對(duì)象的關(guān)聯(lián)對(duì)象
const void * _Nonnull key :傳入一個(gè)指針進(jìn)去,接收的是地址值

  1. 移除關(guān)聯(lián)對(duì)象
    OBJC_EXPORT void
    objc_removeAssociatedObjects(id _Nonnull object)

返回類型為 void,其中有1個(gè)參數(shù):
id _Nonnull object : 移除哪一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象

其他3個(gè)參數(shù)比較明了,說一下key這個(gè)參數(shù)的用法,一般key的常見用法有4種:

  1. static void *myKey = &myKey;
- (void)setAge:(int)age
{
    objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
    objc_setAssociatedObject(self, myKey, @(age), policy);
}

- (int)age
{
    return [objc_getAssociatedObject(self, myKey) intValue];
}
  1. static char myKey;
- (void)setAge:(int)age
{
    objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
    objc_setAssociatedObject(self, &myKey, @(age), policy);
}

- (int)age
{
    return [objc_getAssociatedObject(self, &myKey) intValue];
}
  1. 直接使用屬性名作為key
    使用屬性名可以防止名稱沖突,而且每一個(gè)不同的字符串的地址不一樣
- (void)setAge:(int)age
{
    objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
    objc_setAssociatedObject(self, @"age", @(age), policy);
}

- (int)age
{
    return [objc_getAssociatedObject(self, @"age") intValue];
}
  1. 使用get方法的@selector作為key
- (void)setAge:(int)age
{
    objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
    objc_setAssociatedObject(self, @selector(age), @(age), policy);
}

- (int)age
{
    return [objc_getAssociatedObject(self, @selector(age)) intValue];
}

// 在getter中可以使用隱式參數(shù)_cmd,_cmd對(duì)應(yīng)當(dāng)前方法的selector
- (void)setAge:(int)age
{
    objc_AssociationPolicy policy = OBJC_ASSOCIATION_ASSIGN;
    objc_setAssociatedObject(self, @selector(age), @(age), policy);
}

- (int)age
{
    return [objc_getAssociatedObject(self, _cmd) intValue];
}

這樣就可以在分類中實(shí)現(xiàn)有成員變量的效果:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        People *people  = [[People alloc] init];
        people.age = 10;
        
        NSLog(@"age = %d",people.age); // age = 10
    }
    return 0;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。