Category在平時的工作中也是經(jīng)常用到,是開發(fā)中不可缺少的一個利器,簡單介紹。
優(yōu)點(diǎn):
不需要通過增加子類而增加現(xiàn)有類的行為(方法),且分類中的方法與原始類方法基本沒有區(qū)別;在日常開發(fā)中通過分類可以將龐大一個類的方法按照功能模塊進(jìn)行劃分,從而便于代碼的日后的維護(hù)、更新以及提高代碼的閱讀性
缺點(diǎn):
1.無法向類目添加實(shí)例變量,如果需要添加實(shí)例變量,只能通過定義子類的方式。
2.類目中的方法與原始類以及父類方法相比具有更高優(yōu)先級,如果覆蓋父類的方法,可能導(dǎo)致super消息的斷裂。因此,最好不要覆蓋原始類中的方法。
底層探索:
寫一段代碼,新建Presen類 然后為Presen增加兩個分類,分別是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對象的本質(zhì)窺探講到過實(shí)例對象的isa指針指向類對象,類對象的isa指針指向元類對象,當(dāng)p調(diào)用text方法時,通過實(shí)例對象的isa指針找到類對象,然后在類對象中查找對象方法,如果沒有找到,就通過類對象的superclass指針找到父類對象,接著去尋找text方法。
那么當(dāng)調(diào)用分類的方法時,步驟是否和調(diào)用對象方法一樣呢?
分類中的對象方法依然是存儲在類對象中的,同本類對象方法在同一個地方,調(diào)用步驟也同調(diào)用對象方法一樣。如果是類方法的話,也同樣是存儲在元類對象中。
那么分類方法是如何存儲在類對象中的? 如果一個類含有多個分類那么存儲順序是怎么樣的呢?我們來通過源碼看一下分類的底層結(jié)構(gòu)。
源碼下載地址
分類的底層結(jié)構(gòu)
通過查看分類的源碼我們可以找到category_t 結(jié)構(gòu)體。
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 對象方法
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);
};
從源碼基本可以看出我們平時使用categroy的方式,對象方法,類方法,協(xié)議,和屬性都可以找到對應(yīng)的存儲方式。并且我們發(fā)現(xiàn)分類結(jié)構(gòu)體中是不存在成員變量的,因此分類中是不允許添加成員變量的。分類中添加的屬性并不會幫助我們自動生成成員變量,只會生成get set方法的聲明,需要我們自己去實(shí)現(xiàn)。
通過源碼我們發(fā)現(xiàn),分類的方法,協(xié)議,屬性等好像確實(shí)是存放在categroy結(jié)構(gòu)體里面的,那么他又是如何存儲在類對象中的呢?
我們來看一下底層的內(nèi)部方法探尋其中的原理。
首先我們通過命令行將Preson+Test.m文件轉(zhuǎn)化為c++文件,查看其中的編譯過程。

在分類轉(zhuǎn)化為c++文件中可以看出_category_t結(jié)構(gòu)體中,存放著類名,對象方法列表,類方法列表,協(xié)議列表,以及屬性列表。
往下尋找,我們可以看到_method_list_t類型的結(jié)構(gòu)體,如下圖所示

上圖中我們發(fā)現(xiàn)這個結(jié)構(gòu)體
_OBJC_$_CATEGORY_INSTANCE_METHODS_Preson_$_Test從名稱可以看出是INSTANCE_METHODS對象方法,并且一一對應(yīng)為上面結(jié)構(gòu)體內(nèi)賦值。我們可以看到結(jié)構(gòu)體中存儲了方法占用的內(nèi)存,方法數(shù)量,以及方法列表。并且從上圖中找到分類中我們實(shí)現(xiàn)對應(yīng)的對象方法,test個方法
接下來我們發(fā)現(xiàn)同樣的_method_list_t類型的類方法結(jié)構(gòu)體,如下圖所示

同上面對象方法列表一樣,這個我們可以看出是類方法列表結(jié)構(gòu)體
_OBJC_$_CATEGORY_CLASS_METHODS_Preson_$_Test,同對象方法結(jié)構(gòu)體相同,同樣可以看到我們實(shí)現(xiàn)的類方法,abc。往下尋找看到定義了
_OBJC_$_CATEGORY_Preson_$_Test結(jié)構(gòu)體,并且將我們上面著重分析的結(jié)構(gòu)體一一賦值,我們通過兩張圖片對照一下。

上下兩張圖一一對應(yīng),并且我們看到定義_class_t類型的OBJC_CLASS_$_Preson結(jié)構(gòu)體,最后將_OBJC_$_CATEGORY_Preson_$_Test的cls指針指向OBJC_CLASS_$_Preson結(jié)構(gòu)體地址。我們這里可以看出,cls指針指向的應(yīng)該是分類的主類類對象的地址。
通過以上分析我們發(fā)現(xiàn)。分類源碼中確實(shí)是將我們定義的對象方法,類方法,屬性等都存放在catagory_t結(jié)構(gòu)體中。接下來我們在回到runtime源碼查看catagory_t存儲的方法,屬性,協(xié)議等是如何存儲在類對象中的。
首先來到runtime初始化函數(shù)

接著我們來到
&map_images讀取模塊(images這里代表模塊),來到map_images_nolock函數(shù)中找到_read_images函數(shù),在_read_images函數(shù)中我們找到分類相關(guān)代碼

從上述代碼中我們可以知道這段代碼是用來查找有沒有分類的。通過_getObjc2CategoryList函數(shù)獲取到分類列表之后,進(jìn)行遍歷,獲取其中的方法,協(xié)議,屬性等??梢钥吹阶罱K都調(diào)用了remethodizeClass(cls);函數(shù)。我們來到remethodizeClass(cls);函數(shù)內(nèi)部查看。

通過上述代碼我們發(fā)現(xiàn)
attachCategories函數(shù)接收了類對象cls和分類數(shù)組cats,如我們一開始寫的代碼所示,一個類可以有多個分類。之前我們說到分類信息存儲在category_t結(jié)構(gòu)體中,那么多個分類則保存在category_list中。
來到attachCategories函數(shù)內(nèi)部。

上述源碼中可以看出,首先根據(jù)方法列表,屬性列表,協(xié)議列表,
malloc分配內(nèi)存,根據(jù)多少個分類以及每一塊方法需要多少內(nèi)存來分配相應(yīng)的內(nèi)存地址。之后從分類數(shù)組里面往三個數(shù)組里面存放分類數(shù)組里面存放的分類方法,屬性以及協(xié)議放入對應(yīng)mlist、proplists、protolosts數(shù)組中,這三個數(shù)組放著所有分類的方法,屬性和協(xié)議。之后通過類對象的
data()方法,拿到類對象的class_rw_t結(jié)構(gòu)體rw,在class結(jié)構(gòu)中我們介紹過,class_rw_t中存放著類對象的方法,屬性和協(xié)議等數(shù)據(jù),rw結(jié)構(gòu)體通過類對象的data方法獲取,所以rw里面存放這類對象里面的數(shù)據(jù)。之后分別通過
rw調(diào)用方法列表、屬性列表、協(xié)議列表的attachList函數(shù),將所有的分類的方法、屬性、協(xié)議列表數(shù)組傳進(jìn)去,我們大致可以猜想到在attachList方法內(nèi)部將分類和本類相應(yīng)的對象方法,屬性,和協(xié)議進(jìn)行了合并。
下面來看一下attachLists函數(shù)內(nèi)部。

上述源代碼中有兩個重要的數(shù)組array()->lists: 類對象原來的方法列表,屬性列表,協(xié)議列表。addedLists:傳入所有分類的方法列表,屬性列表,協(xié)議列表。
attachLists函數(shù)中最重要的兩個方法為memmove內(nèi)存移動和memcpy內(nèi)存拷貝。我們先來分別看一下這兩個函數(shù)
// memmove :內(nèi)存移動。
/* __dst : 移動內(nèi)存的目的地
* __src : 被移動的內(nèi)存首地址
* __len : 被移動的內(nèi)存長度
* 將__src的內(nèi)存移動__len塊內(nèi)存到__dst中
*/
void *memmove(void *__dst, const void *__src, size_t __len);
// memcpy :內(nèi)存拷貝。
/* __dst : 拷貝內(nèi)存的拷貝目的地
* __src : 被拷貝的內(nèi)存首地址
* __n : 被移動的內(nèi)存長度
* 將__src的內(nèi)存移動__n塊內(nèi)存到__dst中
*/
void *memcpy(void *__dst, const void *__src, size_t __n);
下面解釋下上面重點(diǎn)代碼的含義
memmove: 將array()->lists的內(nèi)存 移動oldCount * sizeof(array()->lists[0]) 個內(nèi)存 到 lists + addedCount。
memcpy: 將addedLists的內(nèi)存 復(fù)制addedCount * sizeof(array()->lists[0]) 個內(nèi)存 到 array()->lists。
下面通過圖示分析下經(jīng)過memmove和memcpy方法過后的內(nèi)存變化。
經(jīng)過memmove和memcpy方法之前
array()->lists (原來的方法,屬性,協(xié)議列表)

addedLists (傳入的分類方法,屬性,協(xié)議列表)

經(jīng)過memmove方法之后,內(nèi)存變化為
// array()->lists 原來方法、屬性、協(xié)議列表數(shù)組
// addedCount 分類數(shù)組長度
// oldCount * sizeof(array()->lists[0]) 原來數(shù)組占據(jù)的空間
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
array()->lists (原來的方法,屬性,協(xié)議列表)

addedLists (傳入的分類方法,屬性,協(xié)議列表)未變化
經(jīng)過memmove方法之后,我們發(fā)現(xiàn),雖然本類的方法,屬性,協(xié)議列表會分別后移,但是本類的對應(yīng)數(shù)組的指針依然指向原始位置。
memcpy方法之后,內(nèi)存變化
// array()->lists 原來方法、屬性、協(xié)議列表數(shù)組
// addedLists 分類方法、屬性、協(xié)議列表數(shù)組
// addedCount * sizeof(array()->lists[0]) 原來數(shù)組占據(jù)的空間
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
最終結(jié)果

我們發(fā)現(xiàn)原來指針并沒有改變,至始至終指向開頭的位置。并且經(jīng)過memmove和memcpy方法之后,分類的方法,屬性,協(xié)議列表被放在了類對象中原本存儲的方法,屬性,協(xié)議列表前面。
那么為什么要將分類方法的列表追加到本來的對象方法前面呢,這樣做的目的是為了保證分類方法優(yōu)先調(diào)用,我們知道當(dāng)分類重寫本類的方法時,會覆蓋本類的方法。
其實(shí)經(jīng)過上面的分析我們知道本質(zhì)上并不是覆蓋,而是優(yōu)先調(diào)用。本類的方法依然在內(nèi)存中的。我們可以通過打印所有類的所有方法名來查看
- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 獲得方法數(shù)組
Method *methodList = class_copyMethodList(cls, &count);
// 存儲方法名
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]];
}
通過下圖中打印內(nèi)容可以發(fā)現(xiàn),調(diào)用的是Test2中的text方法,并且Person類中存儲著兩個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)的方法列表拷貝到類對象的方法列表中。
Category可以添加屬性,但是并不會自動生成成員變量及set/get方法。因?yàn)閏ategory_t結(jié)構(gòu)體中并不存在成員變量。通過之前對對象的分析我們知道成員變量是存放在實(shí)例對象中的,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運(yùn)行時才去加載的。那么我們就無法再程序運(yùn)行時將分類的成員變量中添加到實(shí)例對象的結(jié)構(gòu)體中。因此分類中不可以添加成員變量。
如果Category中的方法和類中的方法重復(fù),將調(diào)用Category中的方法,因?yàn)樵趯⒔Y(jié)構(gòu)體內(nèi)的方法列表拷貝到類對象的方法列表中的時候放在了類數(shù)據(jù)前面。