Category(分類)這一Object-C 2.0之后添加的語言特性微饥,在日常開發(fā)中使用頻率非常高埠胖。而且面試時(shí)Category基本上是都會(huì)涉及到的一個(gè)知識(shí)點(diǎn)你雌。下面羅列一下面試中經(jīng)常會(huì)提出的問題止吐,基本上涵蓋了這個(gè)知識(shí)點(diǎn):
- Category和Extension的區(qū)別最住。
- Category底層實(shí)現(xiàn)原理
- Category的加載處理過程
- Category中 + load方法的調(diào)用
- Category中 + initialize方法的調(diào)用
- Category中l(wèi)oad和initialize方法的區(qū)別
- 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;
}
發(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中查看一下編譯文件排序:
發(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é)果:
原因是調(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)用一次
- 先調(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 - 再調(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;
}
編譯的順序是:
// 打印結(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)用方式
- +load是根據(jù)方法函數(shù)的內(nèi)存地址直接調(diào)用
- +initialize是通過objc_msgSend調(diào)用
調(diào)用時(shí)刻
- +load是runtime加載類、分類時(shí)調(diào)用(只會(huì)調(diào)用一次)
- +initialize是類第一次接收消息時(shí)調(diào)用志于,每一個(gè)類只會(huì)初始化(initialize)一次涮因,但是父類的+ initialize方法可能會(huì)調(diào)用多次。
調(diào)用順序
+load
1.1 先調(diào)用類的+load方法
編譯越早恨憎,調(diào)用越早
調(diào)用子類的+load方法時(shí)蕊退,先調(diào)用父類的+load方法
1.2 再調(diào)用分類的+load方法
編譯越早,調(diào)用越早+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件事情:
- 生成一個(gè)成員變量
- 生成成員變量的getter点把、setter聲明
- 生成getter和setter的實(shí)現(xiàn)
但是如果在一個(gè)分類中寫一個(gè)屬性,編譯器只會(huì)做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è):
- 設(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
- 獲取關(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)去搞监,接收的是地址值
- 移除關(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種:
- 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];
}
- 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];
}
- 直接使用屬性名作為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];
}
- 使用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;
}