廢話不多說,老規(guī)矩,還是來兩道面試題:
一,Category的實(shí)現(xiàn)原理;
二,Category和Extension的區(qū)別
帶著問題我們對Category一探究竟
在我們平時開發(fā)中,會經(jīng)常用到Category,一般是在什么情況下使用的呢?
也就是說,Category一般會在哪些場合使用?
- 我不想繼承父類的方法實(shí)現(xiàn),只是單純的想加一些新的方法實(shí)現(xiàn);
- 如果這個類包含了很多方法實(shí)現(xiàn),而這些方法實(shí)現(xiàn)又需要多個人來完成,就可以用到分類。
- 想減少單個文件的體積
- 聲明一些私有方法
大家有沒有想過,分類中的方法會存放在什么地方,方法在調(diào)用的時候,是先調(diào)用本身類的方法,還是先調(diào)用分類的方法?帶著這個疑惑,咋們一起探究下分類的本質(zhì)吧。
一,Category的簡單調(diào)用
1. Category簡單使用
1>創(chuàng)建一個Person類
// 聲明
@interface Person : NSObject
- (void)play;
@end
// 實(shí)現(xiàn)
@implementation Person
- (void)play {
NSLog(@"Person - 玩");
}
@end
2> 創(chuàng)建兩個Person的分類Run
// 聲明分類Run
@interface Person (Run)
- (void)run;
+ (void)run2;
@end
// 實(shí)現(xiàn)
@implementation Person (Run)
- (void)run {
NSLog(@"Person (Run) - run");
}
+ (void)run2 {
NSLog(@"Person (Run) - run2");
}
@end
3> 調(diào)用
- (void)viewDidLoad {
[super viewDidLoad];
// Category 的簡單使用
Person *person = [[Person alloc]init];
[person play];
[person run];
[Person run2];
}
查看打印結(jié)果:
Person - 玩
Person (Run) - run
Person (Run) - run2
通過打印結(jié)果可以看出,分類和類的方法都會調(diào)用,各自不影響。
在02 iOS底層原理 - isa和superclass指針探究中講到,
- 對象方法存儲在類的方法列表中
- 類方法存儲在元類的類方法列表中
那么,大家想想,分類中的對象方法和類方法都會以什么樣的數(shù)據(jù)格式存儲呢,還有最后會存儲在什么地方的呢?
首先看看,分類信息的存儲數(shù)據(jù)格式:
二,Category信息的存儲格式
為了能將所有的信息涵蓋進(jìn)去,重新創(chuàng)建一個分類Eat,
包含了,對象方法、類方法、協(xié)議、屬性
// 聲明
@interface Person (Eat) <NSCopying, NSCoding>
- (void)eat;
- (void)eat1;
+ (void)eat2;
+ (void)eat3;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end
// 實(shí)現(xiàn)
@implementation Person (Eat)
- (void)eat {
NSLog(@"吃");
}
- (void)eat1 {
NSLog(@"吃");
}
+ (void)eat2 {
NSLog(@"吃2");
}
+ (void)eat3 {
NSLog(@"吃2");
}
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
return nil;
}
- (void)encodeWithCoder:(nonnull NSCoder *)coder { }
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
return nil;
}
在終端輸入:
& xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Person+Eat.m
可以得到Person+Eat.cpp代碼,分析該文件發(fā)現(xiàn)有這么一個結(jié)構(gòu)體
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; // 屬性列表
};
我總結(jié)了一張示意圖,這個比較清晰:

總結(jié):
Category編譯之后的底層數(shù)據(jù)結(jié)構(gòu)是一個 struct category_t,里面存儲著本身類的類名、類、對象方法列表、類方法列表、屬性列表、協(xié)議列表
我們知道了Category的數(shù)據(jù)結(jié)構(gòu),那么這個數(shù)據(jù)最后會存儲到什么地方法呢?下面我們從runtime源碼分析Category的本質(zhì)。
三,runtime源碼分析Category
在看源碼之前,先做一個代碼測試:
1. 給分類Run中加一個對象方法play
@interface Person (Run)
- (void)run;
+ (void)run2;
// 新加入的
- (void)play;
@end
@implementation Person (Run)
- (void)run {
NSLog(@"Person (Run) - run");
}
+ (void)run2 {
NSLog(@"Person (Run) - run2");
}
// 新加方法實(shí)現(xiàn)
- (void)play {
NSLog(@"Person (Run) - 玩");
}
@end
調(diào)用代碼
- (void)viewDidLoad {
[super viewDidLoad];
// Category 的簡單使用
Person *person = [[Person alloc]init];
[person play];
[person run];
[Person run2];
}
再次運(yùn)行程序,查看打印結(jié)果:
Person (Run) - 玩
Person (Run) - run
Person (Run) - run2
// 第一次的打印結(jié)果
Person - 玩
Person (Run) - run
Person (Run) - run2
我們會發(fā)現(xiàn),第一條的打印結(jié)果是不一樣的,說明分類中出現(xiàn)與本身類重名的方法,在調(diào)用時,只會調(diào)用分類的方法,而本身類的方法就不會調(diào)用了。這是為啥呢?
帶著這個疑問,我們正式研究源碼。
2. 研究runtime源碼
1> 源碼分析查找流程:

2>Category實(shí)現(xiàn)原理示意圖

簡單說明下上圖要表達(dá)的意思:
- 在編譯階段,分類Run和Eat會以struct category_t的形式存儲,并按照從上往下的順序編譯(這個順序會影響到后面調(diào)用的順序);
- 在運(yùn)行階段,會將后編譯的信息放到大數(shù)組前面,先編譯的放到大數(shù)組后面;
- 最終,這個大數(shù)組就會插到本身類和元類的對應(yīng)列表的第一個元素中去,也就是說,Category的信息最終還是會和本身類和元類的信息存儲到一起。
現(xiàn)在就可以解釋:分類中出現(xiàn)與本身類重名的方法,在調(diào)用時,只會調(diào)用分類的方法,而本身類的方法就不會調(diào)用了。
原因:分類中的信息根據(jù)runtime源碼得知,分類中的信息最終會存儲在本身類和元類的對應(yīng)數(shù)據(jù)列表中去,并且是插入到第0個元素,所以在調(diào)用時,會先調(diào)用分類的信息,并且調(diào)用后,本身類和元類的信息就會被覆蓋(沒有銷毀),從而不會再被調(diào)用了。
3> Category實(shí)現(xiàn)原理核心源碼分析

4> Category原理實(shí)現(xiàn)總結(jié)
- 通過runtime加載某個類的所有Category數(shù)據(jù);
- 將所有Category數(shù)據(jù)中的方法、屬性、協(xié)議數(shù)據(jù)列表,合并到一個大數(shù)組里面去;
注意:后編譯的Category會存放在這個大數(shù)組的最前面,編譯順序如圖所示:

四,回答文章開頭的面試題
一,Category的實(shí)現(xiàn)原理
答:Category編譯之后的底層數(shù)據(jù)結(jié)構(gòu)是個 struct catory_t,里面存放著本身類的類名、類、對象方法列表、類方法列表、屬性列表、協(xié)議列表等信息;
在程序運(yùn)行的時候,runtime會將Category數(shù)據(jù),合并到本身類和元類中相關(guān)數(shù)據(jù)列表中去(插入到第0個元素);
Category中的方法,是按照后編譯先執(zhí)行的順序調(diào)用的,如果分類和本身類出現(xiàn)重名的方法,先找到先調(diào)用,之后就不會再繼續(xù)查找了。
二,Category和Extension的區(qū)別
Category是在程序運(yùn)行的時候,將數(shù)據(jù)合并到本身類和元類中的,
Extension在編譯的時候,它的信息就包含在了本身類中了。
順便看下Extension,
給Person類添加一個Extension
// 類的擴(kuò)展
/*
編譯的時候,就會合并到.h里面去,編譯之后就會存在于類對象中
作用:就是將原來公開的成員變量、屬性、方法,放到了.m文件中去,成為私有的
*/
@interface Person()
@property (nonatomic, assign) int age;
- (void)abc;
@end
@implementation Person
// 擴(kuò)展的方法實(shí)現(xiàn)
- (void)abc {
NSLog(@"實(shí)現(xiàn)類擴(kuò)展的方法");
}
- (void)play {
NSLog(@"Person - 玩");
}
@end