Category在平時(shí)的工作中也是經(jīng)常用到烁竭,是開發(fā)中不可缺少的一個(gè)利器臀蛛,簡(jiǎn)單介紹务热。
優(yōu)點(diǎn):
不需要通過(guò)增加子類而增加現(xiàn)有類的行為(方法),且分類中的方法與原始類方法基本沒(méi)有區(qū)別;在日常開發(fā)中通過(guò)分類可以將龐大一個(gè)類的方法按照功能模塊進(jìn)行劃分,從而便于代碼的日后的維護(hù)涮坐、更新以及提高代碼的閱讀性
缺點(diǎn):
1.無(wú)法向類目添加實(shí)例變量,如果需要添加實(shí)例變量,只能通過(guò)定義子類的方式焚辅。
2.類目中的方法與原始類以及父類方法相比具有更高優(yōu)先級(jí),如果覆蓋父類的方法,可能導(dǎo)致super消息的斷裂映屋。因此,最好不要覆蓋原始類中的方法苟鸯。
底層探索:
寫一段代碼,新建Presen類 然后為Presen增加兩個(gè)分類棚点,分別是Presen+Text早处,Preson+Eat,代碼如下
Presen類
// Presen.h
#import <Foundation/Foundation.h>
@interface Preson : NSObject
{
int _age;
}
- (void)text;
@end
// Presen.m
#import "Preson.h"
@implementation Preson
- (void) text
{
NSLog(@"Person - text");
}
@end
Presen擴(kuò)展1
// Presen+Test.h
#import "Preson.h"
@interface Preson (Test) <NSCopying>
- (void)test;
+ (void)test2;
@end
// Presen+Test.m
#import "Preson+Test.h"
@implementation Preson (Test)
- (void)test
{
}
+ (void)test2
{
}
@end
Presen分類2
// Preson+Eat.h
#import "Preson.h"
@interface Preson (Eat)
@end
// Preson+Eat.m
#import "Preson+Eat.h"
@implementation Preson (Test2)
- (void)text
{
NSLog(@"Person (Test2) - text");
}
@end
iOS OC對(duì)象的本質(zhì)窺探講到過(guò)實(shí)例對(duì)象的isa指針指向類對(duì)象瘫析,類對(duì)象的isa指針指向元類對(duì)象砌梆,當(dāng)p調(diào)用text方法時(shí),通過(guò)實(shí)例對(duì)象的isa指針找到類對(duì)象贬循,然后在類對(duì)象中查找對(duì)象方法咸包,如果沒(méi)有找到,就通過(guò)類對(duì)象的superclass指針找到父類對(duì)象杖虾,接著去尋找text方法烂瘫。
那么當(dāng)調(diào)用分類的方法時(shí),步驟是否和調(diào)用對(duì)象方法一樣呢亏掀?
分類中的對(duì)象方法依然是存儲(chǔ)在類對(duì)象中的忱反,同本類對(duì)象方法在同一個(gè)地方,調(diào)用步驟也同調(diào)用對(duì)象方法一樣滤愕。如果是類方法的話温算,也同樣是存儲(chǔ)在元類對(duì)象中。
那么分類方法是如何存儲(chǔ)在類對(duì)象中的? 如果一個(gè)類含有多個(gè)分類那么存儲(chǔ)順序是怎么樣的呢间影?我們來(lái)通過(guò)源碼看一下分類的底層結(jié)構(gòu)注竿。
源碼下載地址
分類的底層結(jié)構(gòu)
通過(guò)查看分類的源碼我們可以找到category_t 結(jié)構(gòu)體。
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 對(duì)象方法
struct method_list_t *classMethods; // 類方法
struct protocol_list_t *protocols; // 協(xié)議
struct property_list_t *instanceProperties; // 屬性
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
從源碼基本可以看出我們平時(shí)使用categroy的方式魂贬,對(duì)象方法巩割,類方法,協(xié)議付燥,和屬性都可以找到對(duì)應(yīng)的存儲(chǔ)方式宣谈。并且我們發(fā)現(xiàn)分類結(jié)構(gòu)體中是不存在成員變量的,因此分類中是不允許添加成員變量的键科。分類中添加的屬性并不會(huì)幫助我們自動(dòng)生成成員變量闻丑,只會(huì)生成get set方法的聲明,需要我們自己去實(shí)現(xiàn)勋颖。
通過(guò)源碼我們發(fā)現(xiàn)嗦嗡,分類的方法,協(xié)議饭玲,屬性等好像確實(shí)是存放在categroy結(jié)構(gòu)體里面的侥祭,那么他又是如何存儲(chǔ)在類對(duì)象中的呢?
我們來(lái)看一下底層的內(nèi)部方法探尋其中的原理。
首先我們通過(guò)命令行將Preson+Test.m
文件轉(zhuǎn)化為c++文件矮冬,查看其中的編譯過(guò)程谈宛。
在分類轉(zhuǎn)化為c++文件中可以看出_category_t結(jié)構(gòu)體中,存放著類名欢伏,對(duì)象方法列表入挣,類方法列表,協(xié)議列表硝拧,以及屬性列表。
往下尋找葛假,我們可以看到_method_list_t
類型的結(jié)構(gòu)體障陶,如下圖所示
上圖中我們發(fā)現(xiàn)這個(gè)結(jié)構(gòu)體
_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test
從名稱可以看出是INSTANCE_METHODS
對(duì)象方法,并且一一對(duì)應(yīng)為上面結(jié)構(gòu)體內(nèi)賦值聊训。我們可以看到結(jié)構(gòu)體中存儲(chǔ)了方法占用的內(nèi)存抱究,方法數(shù)量,以及方法列表带斑。并且從上圖中找到分類中我們實(shí)現(xiàn)對(duì)應(yīng)的對(duì)象方法鼓寺,test
個(gè)方法
接下來(lái)我們發(fā)現(xiàn)同樣的_method_list_t
類型的類方法結(jié)構(gòu)體,如下圖所示
同上面對(duì)象方法列表一樣勋磕,這個(gè)我們可以看出是類方法列表結(jié)構(gòu)體
_OBJC_$_CATEGORY_CLASS_METHODS_Preson_$_Test
妈候,同對(duì)象方法結(jié)構(gòu)體相同,同樣可以看到我們實(shí)現(xiàn)的類方法挂滓,abc苦银。往下尋找看到定義了
_OBJC_$_CATEGORY_Preson_$_Test
結(jié)構(gòu)體,并且將我們上面著重分析的結(jié)構(gòu)體一一賦值赶站,我們通過(guò)兩張圖片對(duì)照一下幔虏。上下兩張圖一一對(duì)應(yīng),并且我們看到定義_class_t
類型的OBJC_CLASS_$_Preson
結(jié)構(gòu)體贝椿,最后將_OBJC_$_CATEGORY_Preson_$_Test
的cls指針指向OBJC_CLASS_$_Preson
結(jié)構(gòu)體地址想括。我們這里可以看出,cls
指針指向的應(yīng)該是分類的主類類對(duì)象的地址烙博。
通過(guò)以上分析我們發(fā)現(xiàn)瑟蜈。分類源碼中確實(shí)是將我們定義的對(duì)象方法,類方法习勤,屬性等都存放在catagory_t
結(jié)構(gòu)體中踪栋。接下來(lái)我們?cè)诨氐?code>runtime源碼查看catagory_t
存儲(chǔ)的方法,屬性图毕,協(xié)議等是如何存儲(chǔ)在類對(duì)象中的夷都。
首先來(lái)到runtime初始化函數(shù)
接著我們來(lái)到
&map_images
讀取模塊(images這里代表模塊),來(lái)到map_images_nolock
函數(shù)中找到_read_images
函數(shù),在_read_images
函數(shù)中我們找到分類相關(guān)代碼
從上述代碼中我們可以知道這段代碼是用來(lái)查找有沒(méi)有分類的囤官。通過(guò)_getObjc2CategoryList
函數(shù)獲取到分類列表之后冬阳,進(jìn)行遍歷,獲取其中的方法党饮,協(xié)議肝陪,屬性等⌒趟常可以看到最終都調(diào)用了remethodizeClass(cls)
;函數(shù)氯窍。我們來(lái)到remethodizeClass(cls)
;函數(shù)內(nèi)部查看。
通過(guò)上述代碼我們發(fā)現(xiàn)
attachCategories
函數(shù)接收了類對(duì)象cls和分類數(shù)組cats
蹲堂,如我們一開始寫的代碼所示狼讨,一個(gè)類可以有多個(gè)分類。之前我們說(shuō)到分類信息存儲(chǔ)在category_t
結(jié)構(gòu)體中柒竞,那么多個(gè)分類則保存在category_list
中政供。
來(lái)到attachCategories函數(shù)內(nèi)部。
上述源碼中可以看出朽基,首先根據(jù)方法列表布隔,屬性列表,協(xié)議列表稼虎,
malloc
分配內(nèi)存衅檀,根據(jù)多少個(gè)分類以及每一塊方法需要多少內(nèi)存來(lái)分配相應(yīng)的內(nèi)存地址。之后從分類數(shù)組里面往三個(gè)數(shù)組里面存放分類數(shù)組里面存放的分類方法渡蜻,屬性以及協(xié)議放入對(duì)應(yīng)mlist
术吝、proplists
、protolosts
數(shù)組中茸苇,這三個(gè)數(shù)組放著所有分類的方法排苍,屬性和協(xié)議。之后通過(guò)類對(duì)象的
data()
方法学密,拿到類對(duì)象的class_rw_t
結(jié)構(gòu)體rw
淘衙,在class
結(jié)構(gòu)中我們介紹過(guò),class_rw_t
中存放著類對(duì)象的方法腻暮,屬性和協(xié)議等數(shù)據(jù)彤守,rw
結(jié)構(gòu)體通過(guò)類對(duì)象的data
方法獲取,所以rw
里面存放這類對(duì)象里面的數(shù)據(jù)哭靖。之后分別通過(guò)
rw
調(diào)用方法列表具垫、屬性列表、協(xié)議列表的attachList
函數(shù)试幽,將所有的分類的方法筝蚕、屬性、協(xié)議列表數(shù)組傳進(jìn)去,我們大致可以猜想到在attachList
方法內(nèi)部將分類和本類相應(yīng)的對(duì)象方法起宽,屬性洲胖,和協(xié)議進(jìn)行了合并。
下面來(lái)看一下attachLists
函數(shù)內(nèi)部坯沪。
上述源代碼中有兩個(gè)重要的數(shù)組
array()->lists: 類對(duì)象原來(lái)的方法列表绿映,屬性列表,協(xié)議列表腐晾。
addedLists:傳入所有分類的方法列表叉弦,屬性列表,協(xié)議列表藻糖。
attachLists函數(shù)中最重要的兩個(gè)方法為memmove內(nèi)存移動(dòng)和memcpy內(nèi)存拷貝卸奉。我們先來(lái)分別看一下這兩個(gè)函數(shù)
// memmove :內(nèi)存移動(dòng)。
/* __dst : 移動(dòng)內(nèi)存的目的地
* __src : 被移動(dòng)的內(nèi)存首地址
* __len : 被移動(dòng)的內(nèi)存長(zhǎng)度
* 將__src的內(nèi)存移動(dòng)__len塊內(nèi)存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :內(nèi)存拷貝颖御。
/* __dst : 拷貝內(nèi)存的拷貝目的地
* __src : 被拷貝的內(nèi)存首地址
* __n : 被移動(dòng)的內(nèi)存長(zhǎng)度
* 將__src的內(nèi)存移動(dòng)__n塊內(nèi)存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面解釋下上面重點(diǎn)代碼的含義
memmove: 將array()->lists的內(nèi)存 移動(dòng)oldCount * sizeof(array()->lists[0]) 個(gè)內(nèi)存 到 lists + addedCount。
memcpy: 將addedLists的內(nèi)存 復(fù)制addedCount * sizeof(array()->lists[0]) 個(gè)內(nèi)存 到 array()->lists凝颇。
下面通過(guò)圖示分析下經(jīng)過(guò)memmove和memcpy方法過(guò)后的內(nèi)存變化潘拱。
經(jīng)過(guò)memmove和memcpy方法之前
array()->lists (原來(lái)的方法,屬性拧略,協(xié)議列表)
addedLists (傳入的分類方法芦岂,屬性,協(xié)議列表)
經(jīng)過(guò)memmove方法之后垫蛆,內(nèi)存變化為
// array()->lists 原來(lái)方法禽最、屬性、協(xié)議列表數(shù)組
// addedCount 分類數(shù)組長(zhǎng)度
// oldCount * sizeof(array()->lists[0]) 原來(lái)數(shù)組占據(jù)的空間
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
array()->lists (原來(lái)的方法袱饭,屬性川无,協(xié)議列表)
addedLists (傳入的分類方法,屬性虑乖,協(xié)議列表)未變化
經(jīng)過(guò)memmove方法之后懦趋,我們發(fā)現(xiàn),雖然本類的方法疹味,屬性仅叫,協(xié)議列表會(huì)分別后移,但是本類的對(duì)應(yīng)數(shù)組的指針依然指向原始位置糙捺。
memcpy方法之后诫咱,內(nèi)存變化
// array()->lists 原來(lái)方法、屬性洪灯、協(xié)議列表數(shù)組
// addedLists 分類方法坎缭、屬性、協(xié)議列表數(shù)組
// addedCount * sizeof(array()->lists[0]) 原來(lái)數(shù)組占據(jù)的空間
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
最終結(jié)果
我們發(fā)現(xiàn)原來(lái)指針并沒(méi)有改變,至始至終指向開頭的位置幻锁。并且經(jīng)過(guò)memmove和memcpy方法之后凯亮,分類的方法,屬性哄尔,協(xié)議列表被放在了類對(duì)象中原本存儲(chǔ)的方法假消,屬性,協(xié)議列表前面岭接。
那么為什么要將分類方法的列表追加到本來(lái)的對(duì)象方法前面呢富拗,這樣做的目的是為了保證分類方法優(yōu)先調(diào)用,我們知道當(dāng)分類重寫本類的方法時(shí)鸣戴,會(huì)覆蓋本類的方法啃沪。
其實(shí)經(jīng)過(guò)上面的分析我們知道本質(zhì)上并不是覆蓋,而是優(yōu)先調(diào)用窄锅。本類的方法依然在內(nèi)存中的创千。我們可以通過(guò)打印所有類的所有方法名來(lái)查看
- (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);
}
- (void)viewDidLoad {
[super viewDidLoad];
Preson *p = [[Preson alloc] init];
[p run];
[self printMethodNamesOfClass:[Preson class]];
}
通過(guò)下圖中打印內(nèi)容可以發(fā)現(xiàn),調(diào)用的是Test2中的text方法入偷,并且Person類中存儲(chǔ)著兩個(gè)text方法追驴。
關(guān)于源碼的讀取順序
objc-os.mm
_objc_init
load_images
prepare_load_methods
schedule_class_load
add_class_to_loadable_list
add_category_to_loadable_list
call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)
總結(jié):
1.Category的實(shí)現(xiàn)原理,以及Category為什么只能加方法不能加屬性?
分類的實(shí)現(xiàn)原理是將category中的方法疏之,屬性殿雪,協(xié)議數(shù)據(jù)放在category_t結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對(duì)象的方法列表中锋爪。
Category可以添加屬性丙曙,但是并不會(huì)自動(dòng)生成成員變量及set/get方法。因?yàn)閏ategory_t結(jié)構(gòu)體中并不存在成員變量其骄。通過(guò)之前對(duì)對(duì)象的分析我們知道成員變量是存放在實(shí)例對(duì)象中的亏镰,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運(yùn)行時(shí)才去加載的年栓。那么我們就無(wú)法再程序運(yùn)行時(shí)將分類的成員變量中添加到實(shí)例對(duì)象的結(jié)構(gòu)體中拆挥。因此分類中不可以添加成員變量。
如果Category中的方法和類中的方法重復(fù)某抓,將調(diào)用Category中的方法纸兔,因?yàn)樵趯⒔Y(jié)構(gòu)體內(nèi)的方法列表拷貝到類對(duì)象的方法列表中的時(shí)候放在了類數(shù)據(jù)前面。