面試中筆試題和面試題好多都問Category代虾,剛?cè)胄斜容^納悶,心里就犯嘀咕:這么簡單還問激蹲。之前一般都是背一背結(jié)合簡單用法直接脫口而出棉磨,結(jié)果就是:回去等通知吧!Q琛乘瓤!
Category:不用繼承對象,就可以增加新的方法策泣,或原本的方法衙傀。
Objective-C語言中,每一個類有哪些方法萨咕,都是在runtime時加入的统抬,我們可以通過runtime提供的一個叫做class_addMethod
的function,加入對應(yīng)的某個selector的實現(xiàn)。而在runtime加入新的方法聪建,使用category會更容易理解與實現(xiàn)的方法钙畔,因為可以使用
與聲明類時差不多的語法,同時也以一般實現(xiàn)的方法金麸,實現(xiàn)我們加入的方法擎析。
至于Swift語言中,Swift的Extension 特性挥下,也與Objective-C的Category差不多叔锐。
什么時候應(yīng)該要使用Category呢?
如果想要擴展某個類的功能见秽,增加新的成員變量與方法愉烙,我們又沒有這些類的源代碼,正規(guī)的做法就是繼承解取、建立新的子類步责。那我們需要子啊不用繼承,就直接添加method這種做法的重要理由禀苦,就是我們要擴展的類很難繼承蔓肯。
可能有以下幾種狀況:
1.Foundation 對象
2.用工廠模式實現(xiàn)的對zai象
3.單利對象
4.在工程中出現(xiàn)多次已經(jīng)不計其數(shù)的對象
Foundation對象
Foundation里面的基本對象,像是NSString振乏、NSArray蔗包、NSDictionary等類的底層實現(xiàn),除了可以通過Objective-C的層面調(diào)用之外慧邮,也可以通過另外一個C的層面调限,叫做Core Foundation,像是NSString其實會對應(yīng)到Core Foundation里面的CFStringRef误澳,NSArray對應(yīng)到CFArrayRef耻矮,而甚至可以直接把Foundation對象轉(zhuǎn)換(cast)成Core Foundation的類型,當(dāng)你遇到一個需要傳入CFStringRef的function的時候忆谓,只要建立NSString然后轉(zhuǎn)換(cast)成CFStringRef 傳入就可以了裆装。
所以,當(dāng)你使用alloc倡缠、init產(chǎn)生一個Foundation對象的時候哨免,其實會得到一個有Foundation與Core Foundation 實現(xiàn)的子類,而實際生成的對象昙沦,往往和我們所認知的有很大差距琢唾,例如,我們認為一個NSMutableString繼承自NSString桅滋,但是建立 NSString 慧耍,調(diào)用alloc、init的時候丐谋,我們真正拿到的是__NSCFConstantString芍碧,而建立NSMutableString ,拿到的__NSCFString号俐,而__NSCFConstantString其實繼承__NSCFString泌豆!
以下代碼說明Foundation 的對象其實是屬于哪些類:
因此,當(dāng)我們嘗試建立Foundation 對象的子類之后吏饿,像是繼承 NSString踪危,建立我們自己的MyString,假如我們并沒有重載原本關(guān)于新建實例的方法猪落,我們也不能保證贞远,建立出來的就是MyString的實例。
用工廠模式實現(xiàn)的對象
工廠模式是一套用來解決不用指定特定是哪一個類笨忌,就可以新建對象的方法。比如說,某個類下胜宇,其實有一堆的子類迂卢,但對外部來說并不需要確切知道這些子類而只要對最上層的類,輸入致電該的條件途凫,就會挑選出一個符合指定條件的子類垢夹,新建實例回調(diào)。
在UIKit中维费,UIButton 就是很好的例子果元,我們建立 UIButton對象的時候,并不是調(diào)用init
或者是initWithFrame:
,而是調(diào)用UIButton 的類方法:buttonWithType:
,通過傳遞按鈕的type新建按鈕對象犀盟。在大多數(shù)狀況下噪漾,會返回UIButton 的對象,但假如我們傳入的type是UIButtonTypeRoundedRect
,卻會返回繼承自UIButton的UIRoundedRectButton
且蓬。
驗證下:
我們要擴展的是UIButton欣硼,但是拿到的卻是
UIRoundedRectButton
,而UIRoundedRectButton
卻無法繼承,因為這些對象不在公開的頭文件里恶阴,我們也不能保證以后傳入UIButtonTypeRoundedRect
就一定會拿到UIRoundedRectButton
诈胜。如此一來,就造成我們難以繼承UIButton
冯事。或這么說:假使我們的需求就是想要改動某個上層的類焦匈,讓底下所有的子類也都增加了一個新的方法,我們又無法改變這個上層的類程序昵仅,就會采用category缓熟。比方說累魔,我們要做所有的
UIViewController
都有一個新的方法,如此我們整個應(yīng)用程序中每個UIViewController
的子類都可以調(diào)用這個方法够滑,但是我們就是無法改動UIViewController
垦写。
單例模式
單例對象是指:某個類只要、也只該有一個實例彰触,每次都只對這個實例操作梯投,而不是建立新的實例。
像UIApplication况毅、 NSUserDefault分蓖、NSNotificationCenter都是采用單例設(shè)計。
之所以說單例對象很難繼承尔许,我們先來看怎么實現(xiàn)單例:我們會有一個static對象么鹤,然后沒戲都返回這個對象。聲明部分如下:
@interface MyClass : NSObject
+ (MyClass *)sharedInstance;
@end
實現(xiàn)部分:
static MyClass *sharedInstance = nil;
@implementation MyClass
+ (MyClass *)sharedInstance
{
return sharedInstance ?
sharedInstance :
(sharedInstance = [[MyClass alloc] init]);
}
@end
其實目前單例大多使用GCD的dispatch_once
實現(xiàn)味廊,之后再寫吧午磁。
如果我們子類化MyClass,卻沒有重寫(override)掉sharedInstance
,那么sharedInstance
返回的還是MyClass 的單例實例毡们。而想要重寫(override)掉sharedInstance
又不見得那么簡單迅皇,因為這個方法里面很可能又做了許多其他的事情,很可能會把這些initiailize時該做的事情衙熔,按照以下的寫法登颓。例如MyClass 可能這樣寫:
+ (MyClass *)sharedInstance
{
if (!sharedInstance) {
sharedInstance = [[MyClass alloc] init];
[sharedInstance doSomething];
[sharedInstance doAnotherThine];
}
return sharedInstance;
}
如果我們并沒有MyClass的源代碼,這個類是在其他的library或是framework 中红氯,我們直接重寫(override)了sharedInstance
框咙,就很有可能有事沒做,而產(chǎn)生不符合預(yù)期的結(jié)果痢甘。
在工程中出現(xiàn)次數(shù)不計其數(shù)的對象
隨著對工程項目的不斷開發(fā)喇嘱,某些類已經(jīng)頻繁使用到了到處都是,而我們現(xiàn)在需求改變塞栅,我們要增加新的方法者铜,但是把所有的用到的地方統(tǒng)統(tǒng)換成新的子類。Category 就是解決這種狀況的救星放椰。
實現(xiàn)Category
Category的語法很簡單作烟,一樣使用@interface關(guān)鍵字聲明頭文件,在@implementation與@end關(guān)鍵字當(dāng)中的范圍是實現(xiàn)砾医,然后在原本的類名后面拿撩,用中括號表示Category名稱。
舉例說明:
@interface NSObject (Test)
- (void)printTest;
@end
@implementation NSObject (Test)
- (void)printTest
{
NSLog(@"%@", self);
}
@end
這樣每個對象都增加了printTest這個方法如蚜,可以調(diào)用[myObject printTest];
排列字符串的時候压恒,可以調(diào)用localizedCompare:
,但是假如我們希望所有的字符串都按照中文筆畫 順序排列影暴,我們可以寫一個自己的方法,例如:strokeCompare:
探赫。
@interface NSString (CustomCompare)
- (NSComparisonResult)strokeCompare:(NSString *)anotherString;
@end
@implementation NSString (CustomCompare)
- (NSComparisonResult)strokeCompare:(NSString *)anotherString
{
NSLocale *strokeSortingLocale = [[[NSLocale alloc]
initWithLocaleIdentifier:@"zh@collation=stroke"]
autorelease];
return [self compare:anotherString
options:0
range:NSMakeRange(0, [self length])
locale:strokeSortingLocale];
}
@end
在保存的時候型宙,文件名的命名規(guī)則是原本的類名加上category的名稱,中間用“+”連接期吓,以我們新建CustomCompare為例子,保存的時候就要保存為NSString+CustomCompare.h以及NSString+CustomCompare.m倾芝。
Category還有啥用處呢讨勤?
除了幫原有的類增加新的方法,我們也會在多種狀況下使用Category晨另。
將一個很大的類切割成多個部分
由于我們可以在新建類之后潭千,繼續(xù)通過Category增加方法,所以借尿,加入一個類很大刨晴,里面又十幾個方法 ,實現(xiàn)有千百行之多路翻,我們就可以考慮將這些類的方法拆分成若干個category狈癞,讓整個類的實現(xiàn)分開在不同的文件里,以便知道某一群方法屬于什么用途茂契。
切割一個很大的類的好處包括以下:
跨工程
如果你手上有好多工程蝶桶,我們在開發(fā)的時候,由于之前寫的一些代碼可以重復(fù)使用掉冶,造成了好多工程可以共用一個類真竖,但是每個工程又不見都會用到這個類的所有的實現(xiàn),我們就可以考慮將屬于某個項目的實現(xiàn)厌小,拆分到某一個category恢共。
跨平臺
如果我們的某段代碼用到在Mac OS X 和iOS 都有的library 與 framework ,那么這就可以在Mac OS X 和iOS 使用璧亚。
替換原來的實現(xiàn)
由于一個類有哪些方法讨韭,是在runtime 時加入,所以除了可以加入新的方法之外癣蟋,假如我們嘗試再加入一個selector與已經(jīng)存在的方法名稱相同的實現(xiàn)拐袜,我們可以把已經(jīng)存在的方法實現(xiàn),換成我們要加入的實現(xiàn)梢薪。這么做在Objective-C語言中是完全可以的蹬铺,如果category 里面出現(xiàn)了名稱相同的方法,編譯器會允許編譯成功秉撇,只會跳出簡單的警告??甜攀。
實際操作上秋泄,這樣的做法很危險,假如我們自己寫了一個類规阀,我們又另外自己寫了一個category 替換掉方法恒序,當(dāng)我們?nèi)蘸笙胄薷倪@個方法的內(nèi)容,很容易忽略掉category 中同名的方法谁撼,結(jié)果就是不管我們?nèi)绾涡薷脑痉椒ㄖ械某绦蚱缧玻Y(jié)果都是什么也沒改。
除了在某一個category 中可以出現(xiàn)與原本類中名稱相同的方法厉碟,我們甚至可以在好幾個category 中喊巍,都出現(xiàn)名稱一樣的方法,哪一個category 在執(zhí)行的時候都會被最后載入箍鼓,這就會造成是這個category 中的實現(xiàn)崭参。那么,如果有多個category 款咖,我們?nèi)绾沃滥囊粋€category 才會是最后被載入的哪一個何暮?Objective-C runtime并不保證category 的載入順序,所以必須避免寫出這樣的程序铐殃。
Extensions
Objective-C語言中有一項叫做extensions 的設(shè)計海洼,也可以拆分一個很大的類,語法與category非常相似富腊,但是不是太一樣贰军。在語法上,extensions 像是一個沒有名字的category蟹肘,在class名稱之后直接加上一個空的括號词疼,而extensions 定義的方法,需要放到原本的類實現(xiàn)中帘腹。
例如:
@interface MyClass : NSObject
@end
@interface MyClass()
- (void)doSomthing;
@end
@implementation MyClass
- (void)doSomthing
{
}
@end
在@interface MyClass ()
這段聲明中贰盗,我們并沒有在括號中定義任何名稱,接著doSomthing
有是MyClass
中實現(xiàn)阳欲。extensions 可以有多個用途舵盈。
拆分 Header
如果我們就是打算實現(xiàn)一個很大的類,但是覺得 header里面已經(jīng)列出的太多的方法球化,我們可以將一部分方法搬到extensions的定義里面秽晚。
另外,extension除了可以放方法之外筒愚,還可以放成員變量赴蝇,而一個類可以擁有不止一個extension,所以一個類有很多的方法可成員變量巢掺,就可以把這些方法與成員變量句伶,放在多個extension中劲蜻。
管理私有方法( Private Methods)
最常見的,我們在寫一個類的時候考余,內(nèi)部有一些方法不需要先嬉、我們也不想放在public header 中,但是如果不將這些方法放到header里楚堤,又會出現(xiàn)一個問題:Xcode 4.3 之前疫蔓,如果這些私有方法在程序代碼中不放在其他的方法前面,其他的方法在調(diào)用這些方法的時候身冬,編譯器會不斷跳出警告衅胀,而這種無關(guān)緊要的警告一多,會覆蓋掉重要的警告吏恭。
要想避免這種警告拗小,要不就是把私有方法都最在最前面重罪,但這樣也不能完全解決問題樱哼,因為私有方法之間可以互相調(diào)用,湖事件確認每個方法之間相互調(diào)用剿配,花時間確認每個方法的調(diào)用順序并不是很有效率的事情;要不就是都用performSelector:
調(diào)用搅幅,這樣問題更大,就像呼胚,在方法改名茄唐、調(diào)用重構(gòu)工具的時候,這樣的做法很危險蝇更。
蘋果提供的建議沪编,就是.m或者.mm文件開頭的地方聲明一個extensions
,將私有方法都放在這個地方,如此一來年扩,其他的方法就可以找到私有方法的聲明蚁廓。在Xcode提供的file template 中,如果建立一個UIViewController 的子類厨幻,就可以看到在.m文件的最前面相嵌,幫你預(yù)留一塊extensions``的聲明。 在這里順便也寫一下Swift的
extensions况脆。在Swift語言中饭宾,我們可以直接用
extensions關(guān)鍵字,建立一個類的
extensions格了,擴展一個類看铆;Swift的
extensions與Object-C的
category 的主要差別是:Object-C的
category 要給定一個名字,而Objective-C的
extensions是沒有名字的
category 盛末,至于Swift 的
extensions```則是沒有統(tǒng)一的名字性湿。
所以纬傲,如果有一個Swift類叫做MyClass
class MyClass {
}
這樣就可以直接建立extensions
extension MyClass {
}
此外,Swift除了可以用extensions
擴展類之外肤频,甚至可以擴充protocol與結(jié)構(gòu)體(struct)叹括。例如:
protocol MyProtocol {
}
extension MyProtocol {
}
struct MyStruct {
}
extension MyStruct {
}
Category是否可以增加新的成員變量或?qū)傩裕?/h2>
因為Objective-C對象會被編譯成C 的結(jié)構(gòu)體,我們可以在category中增加新的方法宵荒,但是我們卻不可以增加成員變量汁雷。
在iOS4之后,蘋果的辦法是關(guān)聯(lián)對象(Associated Objects)的辦法报咳∠姥叮可以讓我們在Category中增加新的getter/setter,其實原理差不多:既然我們可以用一張表記錄類有哪些方法暑刃。那么我們也可以建立另外一個表格厢漩,記錄哪些對象與這個類相關(guān)。
要使用關(guān)聯(lián)對象(Associated Objects)岩臣,我們需要導(dǎo)入objc/runtime.h
溜嗜,然后調(diào)用objc_setAssociatedObject
建立setter,用getAssociatedObject
建立getter架谎,調(diào)用時傳入:我們要讓那個對象與那個對象之間建立聯(lián)系炸宵,連通時使用的是哪一個key(類型為C字符串)。在以下的例子中谷扣,在MyCategory
這個category里面土全,增加一個叫做myVar的屬性(property)。
#import <objc/runtime.h>
@interface MyClass(MyCategory)
@property (retain, nonatomic) NSString *myVar;
@end
@implementation MyClass
- (void)setMyVar:(NSString *)inMyVar
{
objc_setAssociatedObject(self, "myVar",
inMyVar, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)myVar
{
return objc_getAssociatedObject(self, "myVar");
}
@end
在setMyVar:
中調(diào)用objc_setAssociatedObject
時会涎,最后的一個參數(shù)隨是OBJC_ASSOCIATION_RETAIN_NONATOMIC
,是用來決定要用哪一個內(nèi)存管理方法裹匙,管理我們傳入的參數(shù),在示例中末秃,傳入的是NSString
,是一個Objective-C對象概页,所以必須要retain起來。這里可以傳入的參數(shù)還可以是OBJC_ASSOCIATION_ASSIGN
蛔溃、OBJC_ASSOCIATION_COPY_NONATOMIC
绰沥、OBJC_ASSOCIATION_RETAIN
以及OBJC_ASSOCIATION_COPY
,與property
語法使用的內(nèi)存管理方法是一致贺待,而當(dāng)MyClass
對象在dealloc的時候徽曲,所有通過objc_setAssociatedObject
而retain的對象,也都被遺棄釋放麸塞。
雖然不可以在category增加成員變量秃臣,但是卻可以在extensions
中聲明。例如:
@interface MyClass()
{
NSString *myVar;
}
@end
我們還可以將成員變量直接放在@implementation
的代碼中:
@implementation MyClass
{
NSString *myVar;
}
@end
對NSURLSessionTask編寫Category
在寫category的時候,可能會遇到NSURLSessionTask 這個坑鞍麓恕;“ァ!稚虎!
假如在iOS 7以上撤嫩,對NSURLSessionTask寫一個category之后,如果從[NSURLSession sharedSession]
產(chǎn)生data task
對象蠢终,之后序攘,對這個對象調(diào)用category 的方法,奇怪的是寻拂,會找不到任何selector錯誤程奠。照理說一個data task是NSURLSessionDataTask,繼承自NSURLSessionTask祭钉,為什么我們寫NSURLSessionTask category 沒用呢瞄沙?
切換到iOS 8的環(huán)境下又正常了,可以對這個對象調(diào)用NSURLSessionTask category 里面的方法慌核,但是如果寫成NSURLSessionDataTask 的 category距境,結(jié)果又遇到找不到selector的錯誤。
例如:
@interface NSURLSessionTask (Test)
- (void)test;
@end
@implementation NSURLSessionTask (Test)
- (void)test
{
NSLog(@"test");
}
@end
然后跑一下:
NSURLSessionDataTask *task = [[NSURLSession sharedSession];
dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
[task test];
結(jié)果:
*****缺圖一張****
如果有一個category不是直接寫在App里面遂铡,而是寫在某個靜態(tài)庫(static library)肮疗,在編譯時app的最后才把這個庫鏈接進來晶姊,預(yù)想category 并不會讓鏈接器(linker)鏈接(link)進來扒接,你必須要另外在Xcode工程設(shè)定的修改鏈接參數(shù)(other linker flag),加上-ObjC
或者-all_load
们衙。會是這樣嗎钾怔?但是試了下,并沒有收到unsupported selector
的錯誤蒙挑。
NSURLSessionTask是一個Foundation對象宗侦,而Foundation對象往往不是真正的實現(xiàn)與最上層的界面并是同一個。所以忆蚀,我們可以查一個NSURLSessionTask的繼承:
NSURLSessionDataTask *task = [[NSURLSession sharedSession]
dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"]];
NSLog(@"%@", [task class]);
NSLog(@"%@", [task superclass]);
NSLog(@"%@", [[task superclass] superclass]);
NSLog(@"%@", [[[task superclass] superclass] superclass]);
在iOS8 的結(jié)果是:
__NSCFLocalDataTask
__NSCFLocalSessionTask
NSURLSessionTask
NSObject
在iOS7 的結(jié)果是:
__NSCFLocalDataTask
__NSCFLocalSessionTask
__NSCFURLSessionTask
NSObject
結(jié)論矾利,無論是iOS 8 或 iOS 7,我們新建的data task馋袜,都不是直接產(chǎn)生NSURLSessionDataTask
對象男旗,而是產(chǎn)生__NSCFLocalDataTask
這樣的私有對象。iOS 8 上欣鳖,__NSCFLocalDataTask
并不繼承自NSURLSessionDataTask
,而iOS 7上的__NSCFLocalDataTask
甚至連NSURLSessionTask都不是察皇。
想知道建立的data task到底是不是NSURLSessionDataTask
,可以調(diào)用“[task isKindOfClass:[NSURLSessionDataTask class]]
,還是會返回YES。其實什荣,-isKindOfClass:
是可以重寫掉的矾缓,所以,即使__NSCFLocalDataTask
根本就不是 NSURLSessionDataTask稻爬,但是我們還是把__NSCFLocalDataTask
的-isKindOfClass:
寫成:
- (BOOL)isKindOfClass:(Class)aClass
{
if (aClass == NSClassFromString(@"NSURLSessionDataTask")) {
return YES;
}
if (aClass == NSClassFromString(@"NSURLSessionTask")) {
return YES;
}
return [super isKindOfClass:aClass];
}
也就是說嗜闻,-isKindOfClass:
其實并不是那么靈驗,好比你去問產(chǎn)品:這到底還要修改需求嗎桅锄?