Category
也叫分類
或類別
,是OC提供的一種擴展類的方式。不論是自定義的類仍是系統的類,咱們均可以經過Category
給原有類擴展方法(實例方法和類方法均可以),並且擴展的方法和原有的方法的調用方式是如出一轍的。好比我項目中常常須要統計一個字符串中字母的個數,可是系統沒有提供這個方法,那咱們就能夠用Category
給NSString
類擴展一個方法,而後只需引入Category
的頭文件就能夠和調用系統方法同樣來調用擴展的方法。算法
// 給NSString類添加一個Category,並擴展一個實例方法
@interface NSString (QJAdd)
- (NSInteger)letterCount;
@end
複製代碼
// 在須要使用這個擴展方法的地方引入頭文件 #import "NSString+QJAdd.h",而後就能夠調用這個擴展方法了
- (void)test{
NSString *testStr = @"sdfjshdfjk.,d.889";
NSInteger letterCount = [testStr letterCount];
}
複製代碼
Category
除了用來給類進行擴展外,還有一種比較高級的用法,就是用來拆分模塊
,將一個大的模塊拆分紅多個小的模塊,方便進行維護和管理。什麼意思呢?我就舉一個不少開發人員都會存在的問題,就是AppDelegate
這個類。這個類是剛建立項目時自動生成的,用來管理程序生命週期的。在剛建立項目時,這個類中是沒有多少代碼的,可是隨着項目的進行,愈來愈多的代碼會被放在這個類裏面。好比說集成極光推送、友盟、百度地圖、微信SDK等各類第三方框架時,這些第三方框架的初始化工做,甚至是相關的業務邏輯代碼都會放在這個類裏面,這就致使隨着APP的功能愈來愈複雜,AppDelegate
中的代碼就會愈來愈多,有的甚至有幾千行,看着就讓人頭皮發麻。數組
這時咱們就能夠利用Category
來對AppDelegate
進行拆分,首先咱們就須要對AppDelegate
中的代碼進行劃分,把同一種功能的代碼抽取出來放在一個分類裏面。好比說我能夠新建一個極光推送的分類,而後把全部和極光推送有關的代碼都抽出來放入這個分類,把全部和微信相關的代碼抽出來放進微信的分類中,後面又有新的功要添加的話我只須要新建分類就行了。維護的時候要改什麼功能的代碼就直接找相應的分類就行了。bash
// 把全部和極光推送有關的代碼都抽出來放入這個分類
#import "AppDelegate.h"
@interface AppDelegate (JPush)
@end
複製代碼
在講解這個問題以前,咱們須要對OC方法調用的底層機制以及類對象的內存存儲結構有必定了解,對於這一塊不熟悉的能夠先去看看個人另一篇博客OC對象的本質。微信
咱們先來思考這樣一個問題,當咱們經過Category
給一個類擴展了一個實例方法,咱們調用這個實例方法時,它也是經過實例的isa指針找到類對象,而後在類對象的方法列表中去查找這個方法。那麼Category
擴展的方法是如何被添加到類對象的方法列表中去的呢?是編譯的時候添加進去的仍是運行的時候添加進去的呢?app
首先咱們來看下Category
的內存存儲結構,Category
底層其實就是一個category_t
類型的結構體,咱們能夠在objc4
源碼的objc-runtime-new.h
文件中看到它的定義:框架
// 定義在objc-runtime-new.h文件中
struct category_t {
const char *name; // 好比給Student添加分類,name就是Student的類名
classref_t cls;
struct method_list_t *instanceMethods; // 分類的實例方法列表
struct method_list_t *classMethods; // 分類的類方法列表
struct protocol_list_t *protocols; // 分類的協議列表
struct property_list_t *instanceProperties; // 分類的實例屬性列表
struct property_list_t *_classProperties; // 分類的類屬性列表
};
複製代碼
從這個結構體能夠看出,Category
中存儲的不只有方法列表,還有協議列表和屬性列表。函數
咱們每建立一個分類,在編譯時都會生成這樣一個結構體並將分類的方法列表等信息存入這個結構體。在編譯階段分類的相關信息和本類的相關信息是分開的。等到運行階段,會經過runtime加載某個類的全部Category數據,把全部Category的方法、屬性、協議數據分別合併到一個數組中,而後再將分類合併後的數據插入到本類的數據的前面。post
想要了解詳細的流程,能夠去看源碼,因爲源碼太多,這裏就不貼出來了,這裏給你們提供一下解讀源碼時整個流程的函數調用過程:ui
從objc-os.mm
文件的_objc_init
函數開始-->map_images
-->map_images_nolock
-->_read_images
-->remethodizeClass
-->attachCategories
-->attachLists
-->realloc、memmove、 memcpy
。atom
下面我根據個人理解,舉個例子描述一下整個流程,我這裏只講解實例方法列表的合併流程,類方法列表、屬性列表、協議列表等信息的合併流程都是同樣的。
首先咱們聲明一個Student
類,而後建立2個分類:Student (aaa)
和Student (bbb)
,原本和分類中都有實現2個方法,以下代碼所示:
// Student.m文件
#import "Student.h"
@implementation Student
- (void)study{
NSLog(@"%s",__func__);
}
- (void)studentTest{
NSLog(@"%s",__func__);
}
@end
// Student+aaa.m文件
#import "Student+aaa.h"
@implementation Student (aaa)
- (void)study{
NSLog(@"%s",__func__);
}
- (void)studentAaaTest{
NSLog(@"%s",__func__);
}
@end
// Student+bbb.m文件
#import "Student+bbb.h"
@implementation Student (bbb)
- (void)study{
NSLog(@"%s",__func__);
}
- (void)studentBbbTest{
NSLog(@"%s",__func__);
}
@end
複製代碼
aaa->instanceMethods = @[@"study",@"studentAaaTest"]
和bbb->instanceMethods = @[@"study",@"studentBbbTest"]
。而此時Student
類對象的方法列表是存在class_ro_t
結構體的baseMethodList
中。因此在編譯階段各個方法列表都是分開存儲的
。Student
類對象會初始化class_rw_t
結構體,這個結構體中也有個方法列表methods
,它是一個二維數組,它初始化後首先將class_ro_t
中的baseMethodList
拷貝過來,此時methods = @[baseMethodList]
。categoryMethodList
)中的順序是和他們參與編譯的順序有關的,若是先編譯aaa,再編譯bbb,那麼在合併後的數組中,bbb的方法列表在前面,aaa的方法列表在後面,因此此時categoryMethodList = @[bbb->instanceMethods,aaa->instanceMethods]
。categoryMethodList
的數據添加到methods
中。添加以前methods
的容量大小是1,它會先根據categoryMethodList
中方法列表的個數(也就是有幾個分類,這裏是2個分類)進行擴容,methods
擴容後的大小是3,它先將methods
中原來的數據(baseMethodList
)移到最後,而後再將categoryMethodList
中的數據插入進來,因此最後的結果就是methods = @[bbb->instanceMethods,aaa->instanceMethods,baseMethodList]
。這樣就完成了分類方法列表和本類方法列表的合併。因此合併後分類的方法在前面
(最後參與編譯的那個分類的方法列表在最前面),本類的方法列表在最後面
。因此當分類中有和本類同名的方法時,調用這個方法執行的就是分類中的方法。從這個現象來看,就好像本類的方法被分類中同名的方法給覆蓋了,實際上並無覆蓋,只是調用方法時最早查找到了分類的方法因此就執行分類的方法。好比上面的例子,本類和2個分類中都有study
這個方法,若是咱們打印這個類的方法列表就會發現裏面有3個叫study
的方法。
咱們先來看一下普通類定義一個屬性,好比當咱們給一個Student
類定義一個屬性@property (nonatomic , assign) NSInteger score;
時,編譯器會自動幫咱們生成一個叫_score
的成員變量,而且會自動實現這個屬性的setter/getter方法:
@implementation Student
{
NSInteger _score;
}
- (void)setScore:(NSInteger)score{
_score = score;
}
- (NSInteger)score{
return _score;
}
@end
複製代碼
那咱們可不能夠用Category
以一樣的方式來給類擴展屬性和成員變量呢?咱們從Category
的底層結構體category_t
能夠看出,這個結構體中有方法列表、協議列表和屬性列表,可是沒有成員變量列表,因此咱們能夠在Category
中定義屬性,可是不能定義成員變量,定義成員變量的話編譯器會直接報錯。
若是咱們在Student
的分類中定義一個屬性@property (nonatomic , strong) NSString *name;
,那編譯器會爲咱們作什麼呢?編譯器只會幫咱們聲明- (void)setName:(NSString *)name;
和- (NSString *)name;
這兩個方法,而不會實現這兩個方法,也不會定義成員變量。因此此時若是咱們在外面給一個實例對象設置name屬性值student.name = @"Jack"
,編譯器並不會報錯,由於setter方法是有聲明的,可是一旦程序運行,就會拋出unrecognized selector
的異常,由於setter方法沒有實現。
那要如何作才能讓咱們正常的使用name
這個屬性呢?咱們能夠手動去實現setter/getter方法,實現這兩個方法時關鍵就在於如何將屬性值給保存起來,在普通的類中咱們是定義一個成員變量_name
來保存這個屬性值,可是在分類中咱們沒法定義成員變量,因此須要想其餘辦法來保存。咱們能夠經過如下幾種方式來實現這個需求。
什麼叫以本類中已有的屬性來進行存儲呢?咱們直接舉一個例子,好比我要給UIView
擴展x
和y
這兩個屬性,那麼咱們能夠添加一個分類來實現:
// .h文件
@interface UIView (Add)
@property (nonatomic , assign) CGFloat x;
@property (nonatomic , assign) CGFloat y;
@end
// .m文件
#import "UIView+Add.h"
@implementation UIView (Add)
- (void)setX:(CGFloat)x{
CGRect origionRect = self.frame;
CGRect newRect = CGRectMake(x, origionRect.origin.y, origionRect.size.width, origionRect.size.height);
self.frame = newRect;
}
- (CGFloat)x{
return self.frame.origin.x;
}
- (void)setY:(CGFloat)y{
CGRect origionRect = self.frame;
CGRect newRect = CGRectMake(origionRect.origin.x, y, origionRect.size.width, origionRect.size.height);
self.frame = newRect;
}
- (CGFloat)y{
return self.frame.origin.y;
}
@end
複製代碼
這種方式就是經過UIView
原有的屬性frame
來對新添加的屬性x
、y
的值進行存取操做,很顯然這種方式有很大的侷限性,只有在上面這種特殊狀況下才能使用。
好比給Student
添加了一個分類Student (add)
,分類中定義了2個屬性name
和age
,那咱們就能夠在分類的.m
文件中定義2個全局字典nameDic
和ageDic
,nameDic
用來存儲全部實例對象的name
屬性值,其中以實例對象的指針做爲key,name
屬性值做爲value。ageDic
用來存儲全部實例對象的age
屬性值。代碼以下所示:
#import "Student+add.h"
// 以實例對象的指針做爲key
#define QJKey [NSString stringWithFormat:@"%p",self]
@implementation Student (add)
// 定義2個全局字典用來存儲2個新增的屬性的值
NSMutableDictionary *nameDic;
NSMutableDictionary *ageDic;
+ (void)load{
nameDic = [NSMutableDictionary dictionary];
ageDic = [NSMutableDictionary dictionary];
}
//
- (void)setName:(NSString *)name{
nameDic[QJKey] = name;
}
- (NSString *)name{
return nameDic[QJKey];
}
- (void)setAge:(NSInteger)age{
ageDic[QJKey] = @(age);
}
- (NSInteger)age{
return [ageDic[QJKey] integerValue];
}
@end
複製代碼
這種方法雖然能夠實現咱們的需求,可是有個問題,每當咱們實例化一個對象時都會往那兩個全局字典中添加一個元素,而實例化的對象銷燬時,全局字典中與之對應的元素又沒有被移除,這樣就會致使這兩個字典佔用內存會愈來愈大,有內存溢出的風險。
關聯對象是runtime
提供的一組API,因此須要引入頭文件#import <objc/runtime.h>
。
添加關聯對象API:
void objc_setAssociatedObject(id object,
const void * key,
id value,
objc_AssociationPolicy policy);
複製代碼
好比Student的分類中新增了一個name屬性,我要給一個實例對象的stu的name屬性賦值爲Jack:
object
):關聯的對象,也就是上面的stu
key
):這裏傳入一個void *
類型的指針做爲key,這個key是本身隨便設置的,後面獲取關聯對象也是根據這個key來獲取的。後面我會列出幾種經常使用的設置key的方式。value
):要設置的屬性值,也就是上面的Jackpolicy
):要設置的屬性的修飾類型,好比name的修飾類型是strong, nonatomic
,那這裏對應的的policy就是OBJC_ASSOCIATION_RETAIN_NONATOMIC
。具體對應關係以下(注意是沒有和weak對應的policy的):objc_AssociationPolicy | 對應的修飾符 |
---|---|
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 |
獲取關聯對象:
id objc_getAssociatedObject(id object, const void * key);
複製代碼
移除全部的關聯對象:
void objc_removeAssociatedObjects(id object);
複製代碼
設置key的經常使用方式: key是一個void *
類型的指針,原則上來講隨便設置一個指針均可以,只要保證設置關聯對象時和獲取關聯對象時的key同樣就好了。可是爲了提升代碼的可讀性,咱們能夠採用如下幾種方式來設置key:
好比給stu
實例對象的name
屬性賦值Jack
。
方式一:
對每個屬性都聲明一個靜態全局的void *
類型的變量,這個變量中存儲的值是它本身的地址,這樣就能夠保證這個變量值的惟一性。
// 聲明全局靜態變量
static void *_name = &_name;
// 設置關聯對象
objc_setAssociatedObject(stu, _name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 獲取關聯對象
NSString *temName = objc_getAssociatedObject(stu, _name);
複製代碼
方式二: 這種方式和上面那種方式差很少,只不過聲明全局變量後不賦值,直接將變量的地址值設置爲key:
static void *_name;
objc_setAssociatedObject(stu, &_name, @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, &_name);
複製代碼
方式三:
用屬性名字符串的地址做爲key,注意key = @"name"
這種是將字符串的地址賦值給key,而不是將字符串自己賦值給key。在iOS中,不管定義多少個指針變量指向@"name"
這個字符串,這些指針指向的其實都是同一個內存空間,因此能夠保證key的惟一性。
objc_setAssociatedObject(stu, @"name", @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, @"name");
複製代碼
方式四:
用屬性的getter方法個的@selector
做爲key,由於一個類中同一個方法名對應的SEL
的地址始終是同一個。比較推薦使用這種方式,由於在寫代碼的時候會有提示。
objc_setAssociatedObject(stu, @selector(name), @"Jack", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSString *temName = objc_getAssociatedObject(stu, @selector(name));
複製代碼
// .h文件
#import "Student.h"
@interface Student (Add)
@property (nonatomic , strong) NSString *name;
@property (nonatomic , assign) NSInteger age;
@end
// .m文件
#import "Student+Add.h"
#import <objc/runtime.h>
@implementation Student (Add)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, @selector(name));
}
- (void)setAge:(NSInteger)age{
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (NSInteger)age{
return [objc_getAssociatedObject(self, @selector(age)) integerValue];
}
@end
複製代碼
有人可能會有疑問,關聯對象底層是怎麼實現的呢,它是否是經過屬性生成了成員變量,而後合併到了類對象的成員屬性列表中去呢?其實不是的,關聯對象是另外單獨存儲的,底層實現關聯對象技術的核心對象有4個:
ObjcAssociation
:這個對象裏面有2個成員uintptr_t _policy
和id _value
,這兩個很顯然就是咱們設置關聯對象傳入的參數policy
和value
。ObjectAssociationMap
:這是一個HashMap
(以鍵值對方式存儲,能夠理解爲是一個字典),以設置關聯對象時傳入的key
值做爲HashMap
的鍵
,以ObjcAssociation
對象做爲HashMap
的值
。好比一個分類添加了3個屬性,那一個實例對象給這3個屬性都賦值了,那麼這個HashMap
中就有3個元素,若是給這個實例對象的其中一個屬性賦值爲nil,那這個HashMap
就會把這個屬性對應的鍵值對給移除,而後HashMap
中就還剩2個元素。AssociationsHashMap
:這也是一個HashMap
,以設置關聯屬性時傳入的參數object
做爲鍵
(實際是對object對象經過某個算法計算出一個值做爲鍵
)。以ObjectAssociationMap
做爲值
。因此當某個類(前提是這個類的分類中有設置關聯對象)每實例化一個對象,這個HashMap
就會新增一個元素,當某個實例化對象被釋放時,其對應的鍵值對也會被這個HashMap
給移除。注意整個程序運行期間,AssociationsHashMap
只會有一個,也就是說全部的類的關聯對象信息都是存儲在這個HashMap
中。AssociationsManager
:從名字就能夠看出它是一個管理者,注意整個程序運行期間它也只有一個,他就只包含一個AssociationsHashMap
。這4個對象的關係圖以下圖所示: