一、Category(類別,分類)的使用場景
- 為已經(jīng)存在的類添加方法和屬性
- 把不同的功能組織到不同的
category
- 模擬多繼承(另外模擬多繼承的還有
protocol
)
1)給類擴展:類別或者繼承
category
相比繼承的缺點
1.新的擴展方法還需要父類的實現(xiàn),與原方法重名。若使用類別則會
覆蓋
。所謂覆蓋
其實也不是很準確,具體解釋見原理。
2.category
不能添加實例變量,具體解釋見原理。
3.如果一個類有多個category
,不同的類別同時定義同一個方法,則調(diào)用哪個不一定,具體解釋見原理。
category
相比inherit
的優(yōu)點
1.
category
可以修改原類基礎(chǔ)上擴展類的方法,實現(xiàn)私有類 方法擴展以及一些不能修改源碼類的擴展,也就是說不會入侵原類的代碼結(jié)構(gòu),比繼承更為輕量級。
2.category
可以將類的實現(xiàn)分散到多個文件。
3.category
可以創(chuàng)建私有方法的前向引用:在類別里聲明一個類的私有方法,不需要實現(xiàn),編譯不會報錯,運行時會調(diào)用私有方法。繼承不能繼承私有方法。
4.category
可以向類添加非正式協(xié)議:因為Objective-C
里面的類大都是NSObject
的子類,所以在NSObject
的類別里定義函數(shù),所有對象都能使用
2)添加屬性:runtime
直接在category
的.h
中添加這時候會有warning
?? Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category
類別不會自動幫屬性生成getter/setter
方法,也不能生成帶下劃線的實例變量。我們可以用runtime
【關(guān)聯(lián)對象】去實現(xiàn)category
唯一有的類添加新的屬性并生成getter/settet
方法。
runtime我們提供了以下方法
參數(shù): id object:被關(guān)聯(lián)的對象
const void *key:關(guān)聯(lián)的key,要求唯一
id value:關(guān)聯(lián)的對象(如dog)
objc_AssociationPolicy policy:內(nèi)存管理的策略
//關(guān)聯(lián)對象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//獲取關(guān)聯(lián)的對象
id objc_getAssociatedObject(id object, const void *key)
//移除關(guān)聯(lián)的對象
void objc_removeAssociatedObjects(id object)
當對象被釋放時,會根據(jù)這個策略來決定是否釋放關(guān)聯(lián)的對象,當策略是RETAIN/COPY
時,會釋放(release
)關(guān)聯(lián)的對象,當是ASSIGN
,將不會釋放。
值得注意的是,我們不需要主動調(diào)用removeAssociated
來接觸關(guān)聯(lián)的對象,如果需要解除指定的對象,可以使用setAssociatedObject
置nil
來實現(xiàn)。
舉個??
我為Student+teach.h
類添加了個name
屬性,.h
文件如下
#import "Student.h"
@interface Student (teach)
//不會生成添加屬性的getter和setter方法,必須我們手動生成
@property (nonatomic, copy) NSString * name;
@end
.m
文件如下
#import "Student+teach.h"
//導(dǎo)入頭文件
#import <objc/runtime.h>
@implementation Student (teach)
//定義關(guān)聯(lián)的key
static const char *key = "name";
//getter方法
- (NSString *)name {
//根據(jù)key獲取關(guān)聯(lián)的值
return objc_getAssociatedObject(self, key);
}
//setter方法
- (void)setName:(NSString *)name {
//參數(shù)設(shè)置
objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
運行測試代碼
Student *xiaoming = [[Student alloc] init];
xiaoming.name = @"xiaoming";
NSLog(@"%@",xiaoming.name);
打印結(jié)果
2017-05-23 13:49:52.166 RuntimeTest[3009:178563] xiaoming
注:這里區(qū)分一下屬性和成員變量的區(qū)別
1.我聲明了一個屬性name
。
@property (nonatomic, copy) NSString * name;
會為我生成一個以下劃線為開頭的實例變量_name
,也不需要寫@synthesize name;
,會自動還是你歌城getter/setter
方法。那么在.m
文件中可以直接使用_name
實例變量,也可以self.name
通過屬性的getter/setter
設(shè)置。
2.大括號
@interface MyViewController:UIViewController
{
NSString *name;
}
@end
此時用點語法會提示,編輯器會提示讓你把.改為->。因為點語法是調(diào)用方法,為代碼里沒有g(shù)etter/setter方法。
3.點語法的說明
如果點表達式出現(xiàn)在=
左邊,則該屬性的setter
的方法將會被調(diào)用。右邊則為getter
方法。
4.synthesize
的用法
@synthesize name = myname;
會自動生成實例變量myname
而不是默認的_name
,自動生成getter/setter
方法,所以它的作用是指定實例變量名稱。
3)模擬多繼承:消息轉(zhuǎn)發(fā)、delegate和protocol、類別
oc
不支持多線程,由于消息機制消息名字查找發(fā)生在運行時而非編譯時,多個基類可能導(dǎo)致二義性問題。模擬多線程有以下幾個方法: 消息轉(zhuǎn)發(fā),delegate
和protocol
,類別。
類別:
如上??。
delegate和protocol:
委托協(xié)助主體完成操作任務(wù),將需要定制化的操作預(yù)留給委托對象來自定義實現(xiàn),類似子類化主體。
除此之外,可以用作事件監(jiān)聽。
消息轉(zhuǎn)發(fā):
當向某對象發(fā)送消息但是runtime system
在當前類和父類中都找不到該方法的實現(xiàn)時,runtime
不會立即報錯,而是有以下步驟,
簡述流程:
1.動態(tài)方法解析:向當前類發(fā)送resolveInstanceMethod:的信號,檢查是否像這個類動態(tài)添加了方法。
相關(guān)鏈接:http://blog.csdn.net/haishu_zheng/article/details/12873151
2.快速消息發(fā)送:檢查這個類是否實現(xiàn)了forwardingTargetForSelector:方法,是吸納了就調(diào)用。若返回值對象非空nil或者非self,則向該返回對象重新發(fā)送信息。
利用快速消息轉(zhuǎn)發(fā)forwardingTargetForSelector:方法重寫,結(jié)合【類別】或者【類型強轉(zhuǎn)】可以實現(xiàn)多繼承方法
思考問題:如果將其類型轉(zhuǎn)成`id ,也可以編譯通過,并實現(xiàn)轉(zhuǎn)發(fā)。可是會帶來什么隱患呢?
3.標準信息發(fā)送:runtime發(fā)送methodSignatureForSelector:消息獲取Selector對應(yīng)的方法簽名。返回值非空則通過forwardInvocation:轉(zhuǎn)發(fā)消息,返回值為空則向當前對象發(fā)送doesNotRecognizeSelector:消息,程序崩潰退出。
標準消息轉(zhuǎn)發(fā)需要重寫 methodSignatureForSelector: 和 forwardInvocation: 兩個方法即可。
二、category的原理和特性
1)原理
runtime
的源碼里可以找到_read_images
函數(shù):
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
...
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
bool hasClassProperties = hi->info()->hasCategoryClassProperties();
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (!cls) {
// Category's target class is missing (probably weak-linked).
// Disavow any knowledge of this category.
catlist[i] = nil;
if (PrintConnecting) {
_objc_inform("CLASS: IGNORING category \?\?\?(%s) %p with "
"missing weak-linked target class",
cat->name, cat);
}
continue;
}
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
bool classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
classExists = YES;
}
if (PrintConnecting) {
_objc_inform("CLASS: found category -%s(%s) %s",
cls->nameForLogging(), cat->name,
classExists ? "on existing class" : "");
}
}
if (cat->classMethods || cat->protocols
|| (hasClassProperties && cat->_classProperties))
{
addUnattachedCategoryForClass(cat, cls->ISA(), hi);
if (cls->ISA()->isRealized()) {
remethodizeClass(cls->ISA());
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
cls->nameForLogging(), cat->name);
}
}
}
}
...
}
里面提到了兩個比較重要的函數(shù):
addUnattachedCategoryForClass()
remethodizeClass()
-
addUnattachedCategoryForClass()
主要是構(gòu)建class和category
的哈希表,如果不存在則生成,如果存在則重新構(gòu)建,主要是class到category
的一個映射關(guān)系。 -
remethodizeClass
調(diào)用了attachCategories
方法,這個方法里面把category
里的方法,屬性和協(xié)議列表都添加到相應(yīng)的類里面,從本質(zhì)來說,調(diào)用類的方法和分類的方法都是一樣的。
2)category重名函數(shù)覆蓋
在看源碼的attachLists
方法
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
第一個判斷語句中C函數(shù) memmove(dest, src, len)
和memcpy(des, src, size)
完成了將舊函數(shù)數(shù)組后移空出位置,把新的函數(shù)數(shù)組添加到原數(shù)組空位的過程。
下一個判斷語句意思為如果list
沒有值,那么直接賦值給它。
最后一個判斷語句是找到多個函數(shù)名一樣的,進行插入。
綜上所述,
category
里新增的和原來類重名的函數(shù)將會被添加在原函數(shù)列表的前面,并不是覆蓋。如果想要調(diào)用原函數(shù)的方法,只需要便利函數(shù)列表,調(diào)用最后一個來執(zhí)行。這就是category
重名函數(shù)覆蓋,并且同個類多個category
重名調(diào)用不確定的原因。(有些文章指出執(zhí)行順序跟編譯順序有關(guān))
3)Category和Extension的區(qū)別
1、Extension
在編譯期決議,它就是類的一部分,在編譯期和頭文件里的@interface
以及實現(xiàn)文件里的@implement
一起形成一個完整的類,它伴隨類的產(chǎn)生而產(chǎn)生,亦隨之一起消亡。Extension
一般用來隱藏類的私有信息,你必須有一個類才能為這個類添加Extension
,所以你無法為系統(tǒng)的類比如NSString
添加Extension
。
2、Category
則完全不一樣,它是在運行期決議的。
3、Extension
可以添加屬性、成員變量,而Category
一般不可以。
總之,就
Category
和Extension
的區(qū)別來看,Extension
可以添加實例變量,而Category
是無法添加實例變量的。因為Category
在運行期,對象的內(nèi)存布局已經(jīng)確定,如果添加實例變量就會破壞類的內(nèi)部布局,這對編譯型語言來說是災(zāi)難性的。
4)Category不能添加成員變量
因為方法和屬性并不“屬于”類實例,而成員變量“屬于”類實例。我們所說的“類實例”概念,指的是一塊內(nèi)存區(qū)域,包含了isa指針和所有的成員變量。所以假如允許動態(tài)修改類成員變量布局,已經(jīng)創(chuàng)建出的類實例就不符合類定義了,變成了無效對象。但方法定義是在objc_class
中管理的,不管如何增刪類方法,都不影響類實例的內(nèi)存布局,已經(jīng)創(chuàng)建出的類實例仍然可正常使用。
第一次發(fā)簡書文章,如果所述有錯誤或者不明確的地方,望大家不吝賜教?? ~
參考:
美團技術(shù)文檔: http://tech.meituan.com/DiveIntoCategory.html