iOS套路面試題之Category
面試中筆試題和麵試題好多都問Category,剛入行比較納悶,心裡就犯嘀咕:這麼簡單還問。之前一般都是背一背結合簡單用法直接脫口而出,結果就是:回去等通知吧!!!
Category:不用繼承物件,就可以增加新的方法,或原本的方法。
Objective-C語言中,每一個類有哪些方法,都是在runtime時加入的,我們可以通過runtime提供的一個叫做class_addMethod
的function,加入對應的某個selector的實現。而在runtime加入新的方法,使用category會更容易理解與實現的方法,因為可以使用
與宣告類時差不多的語法,同時也以一般實現的方法,實現我們加入的方法。
至於Swift語言中,Swift的Extension 特性,也與Objective-C的Category差不多。
什麼時候應該要使用Category呢?
如果想要擴充套件某個類的功能,增加新的成員變數與方法,我們又沒有這些類的原始碼,正規的做法就是繼承、建立新的子類。那我們需要子啊不用繼承,就直接新增method這種做法的重要理由,就是我們要擴充套件的類很難繼承。
可能有以下幾種狀況:
1.Foundation 物件
2.用工廠模式實現的對zai象
3.單利物件
4.在工程中出現多次已經不計其數的物件
Foundation物件
Foundation裡面的基本物件,像是NSString、NSArray、NSDictionary等類的底層實現,除了可以通過Objective-C的層面呼叫之外,也可以通過另外一個C的層面,叫做Core Foundation,像是NSString其實會對應到Core Foundation裡面的CFStringRef,NSArray對應到CFArrayRef,而甚至可以直接把Foundation物件轉換(cast)成Core Foundation的型別,當你遇到一個需要傳入CFStringRef的function的時候,只要建立NSString然後轉換(cast)成CFStringRef 傳入就可以了。
所以,當你使用alloc、init產生一個Foundation物件的時候,其實會得到一個有Foundation與Core Foundation 實現的子類,而實際生成的物件,往往和我們所認知的有很大差距,例如,我們認為一個NSMutableString繼承自NSString,但是建立 NSString ,呼叫alloc、init的時候,我們真正拿到的是__NSCFConstantString,而建立NSMutableString ,拿到的__NSCFString,而__NSCFConstantString其實繼承__NSCFString!
以下程式碼說明Foundation 的物件其實是屬於哪些類:
因此,當我們嘗試建立Foundation 物件的子類之後,像是繼承 NSString,建立我們自己的MyString,假如我們並沒有過載原本關於新建例項的方法,我們也不能保證,建立出來的就是MyString的例項。
用工廠模式實現的物件
工廠模式是一套用來解決不用指定特定是哪一個類,就可以新建物件的方法。比如說,某個類下,其實有一堆的子類,但對外部來說並不需要確切知道這些子類而只要對最上層的類,輸入致電該的條件,就會挑選出一個符合指定條件的子類,新建例項回撥。
在UIKit中,UIButton 就是很好的例子,我們建立 UIButton物件的時候,並不是呼叫init
或者是initWithFrame:
,而是呼叫UIButton 的類方法:buttonWithType:
,通過傳遞按鈕的type新建按鈕物件。在大多數狀況下,會返回UIButton 的物件,但假如我們傳入的type是UIButtonTypeRoundedRect
,卻會返回繼承自UIButton的UIRoundedRectButton
。
驗證下:
我們要擴充套件的是UIButton,但是拿到的卻是
UIRoundedRectButton
,而UIRoundedRectButton
卻無法繼承,因為這些物件不在公開的標頭檔案裡,我們也不能保證以後傳入UIButtonTypeRoundedRect
就一定會拿到UIRoundedRectButton
。如此一來,就造成我們難以繼承UIButton
。或這麼說:假使我們的需求就是想要改動某個上層的類,讓底下所有的子類也都增加了一個新的方法,我們又無法改變這個上層的類程式,就會採用category。比方說,我們要做所有的
UIViewController
都有一個新的方法,如此我們整個應用程式中每個UIViewController
的子類都可以呼叫這個方法,但是我們就是無法改動UIViewController
。
單例模式
單例物件是指:某個類只要、也只該有一個例項,每次都只對這個例項操作,而不是建立新的例項。
像UIApplication、 NSUserDefault、NSNotificationCenter都是採用單例設計。
之所以說單例物件很難繼承,我們先來看怎麼實現單例:我們會有一個static物件,然後沒戲都返回這個物件。宣告部分如下:
@interface MyClass : NSObject
+ (MyClass *)sharedInstance;
@end
實現部分:
static MyClass *sharedInstance = nil;
@implementation MyClass
+ (MyClass *)sharedInstance
{
return sharedInstance ?
sharedInstance :
(sharedInstance = [[MyClass alloc] init]);
}
@end
其實目前單例大多使用GCD的dispatch_once
實現,之後再寫吧。
如果我們子類化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
,就很有可能有事沒做,而產生不符合預期的結果。
在工程中出現次數不計其數的物件
隨著對工程專案的不斷開發,某些類已經頻繁使用到了到處都是,而我們現在需求改變,我們要增加新的方法,但是把所有的用到的地方統統換成新的子類。Category 就是解決這種狀況的救星。
實現Category
Category的語法很簡單,一樣使用@interface關鍵字宣告標頭檔案,在@implementation與@end關鍵字當中的範圍是實現,然後在原本的類名後面,用中括號表示Category名稱。
舉例說明:
@interface NSObject (Test)
- (void)printTest;
@end
@implementation NSObject (Test)
- (void)printTest
{
NSLog(@"%@", self);
}
@end
這樣每個物件都增加了printTest這個方法,可以呼叫[myObject printTest];
排列字串的時候,可以呼叫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
在儲存的時候,檔名的命名規則是原本的類名加上category的名稱,中間用“+”連線,以我們新建CustomCompare為例子,儲存的時候就要儲存為NSString+CustomCompare.h以及NSString+CustomCompare.m。
Category還有啥用處呢?
除了幫原有的類增加新的方法,我們也會在多種狀況下使用Category。
將一個很大的類切割成多個部分
由於我們可以在新建類之後,繼續通過Category增加方法,所以,加入一個類很大,裡面又十幾個方法 ,實現有千百行之多,我們就可以考慮將這些類的方法拆分成若干個category,讓整個類的實現分開在不同的檔案裡,以便知道某一群方法屬於什麼用途。
切割一個很大的類的好處包括以下:
跨工程
如果你手上有好多工程,我們在開發的時候,由於之前寫的一些程式碼可以重複使用,造成了好多工程可以共用一個類,但是每個工程又不見都會用到這個類的所有的實現,我們就可以考慮將屬於某個專案的實現,拆分到某一個category。
跨平臺
如果我們的某段程式碼用到在Mac OS X 和iOS 都有的library 與 framework ,那麼這就可以在Mac OS X 和iOS 使用。
替換原來的實現
由於一個類有哪些方法,是在runtime 時加入,所以除了可以加入新的方法之外,假如我們嘗試再加入一個selector與已經存在的方法名稱相同的實現,我們可以把已經存在的方法實現,換成我們要加入的實現。這麼做在Objective-C語言中是完全可以的,如果category 裡面出現了名稱相同的方法,編譯器會允許編譯成功,只會跳出簡單的警告⚠️。
實際操作上,這樣的做法很危險,假如我們自己寫了一個類,我們又另外自己寫了一個category 替換掉方法,當我們日後想修改這個方法的內容,很容易忽略掉category 中同名的方法,結果就是不管我們如何修改原本方法中的程式,結果都是什麼也沒改。
除了在某一個category 中可以出現與原本類中名稱相同的方法,我們甚至可以在好幾個category 中,都出現名稱一樣的方法,哪一個category 在執行的時候都會被最後載入,這就會造成是這個category 中的實現。那麼,如果有多個category ,我們如何知道哪一個category 才會是最後被載入的哪一個?Objective-C runtime並不保證category 的載入順序,所以必須避免寫出這樣的程式。
Extensions
Objective-C語言中有一項叫做extensions 的設計,也可以拆分一個很大的類,語法與category非常相似,但是不是太一樣。在語法上,extensions 像是一個沒有名字的category,在class名稱之後直接加上一個空的括號,而extensions 定義的方法,需要放到原本的類實現中。
例如:
@interface MyClass : NSObject
@end
@interface MyClass()
- (void)doSomthing;
@end
@implementation MyClass
- (void)doSomthing
{
}
@end
在@interface MyClass ()
這段宣告中,我們並沒有在括號中定義任何名稱,接著doSomthing
有是MyClass
中實現。extensions 可以有多個用途。
拆分 Header
如果我們就是打算實現一個很大的類,但是覺得 header裡面已經列出的太多的方法,我們可以將一部分方法搬到extensions的定義裡面。
另外,extension除了可以放方法之外,還可以放成員變數,而一個類可以擁有不止一個extension,所以一個類有很多的方法可成員變數,就可以把這些方法與成員變數,放在多個extension中。
管理私有方法( Private Methods)
最常見的,我們在寫一個類的時候,內部有一些方法不需要、我們也不想放在public header 中,但是如果不將這些方法放到header裡,又會出現一個問題:Xcode 4.3 之前,如果這些私有方法在程式程式碼中不放在其他的方法前面,其他的方法在呼叫這些方法的時候,編譯器會不斷跳出警告,而這種無關緊要的警告一多,會覆蓋掉重要的警告。
要想避免這種警告,要不就是把私有方法都最在最前面,但這樣也不能完全解決問題,因為私有方法之間可以互相呼叫,湖事件確認每個方法之間相互呼叫,花時間確認每個方法的呼叫順序並不是很有效率的事情;要不就是都用performSelector:
呼叫,這樣問題更大,就像,在方法改名、呼叫重構工具的時候,這樣的做法很危險。
蘋果提供的建議,就是.m或者.mm檔案開頭的地方宣告一個extensions
,將私有方法都放在這個地方,如此一來,其他的方法就可以找到私有方法的宣告。在Xcode提供的file template 中,如果建立一個UIViewController 的子類,就可以看到在.m檔案的最前面,幫你預留一塊extensions``的宣告。 在這裡順便也寫一下Swift的
extensions。在Swift語言中,我們可以直接用
extensions關鍵字,建立一個類的
extensions,擴充套件一個類;Swift的
extensions與Object-C的
category 的主要差別是:Object-C的
category 要給定一個名字,而Objective-C的
extensions是沒有名字的
category ,至於Swift 的
extensions```則是沒有統一的名字。
所以,如果有一個Swift類叫做MyClass
class MyClass {
}
這樣就可以直接建立extensions
extension MyClass {
}
此外,Swift除了可以用extensions
擴充套件類之外,甚至可以擴充protocol與結構體(struct)。例如:
protocol MyProtocol {
}
extension MyProtocol {
}
struct MyStruct {
}
extension MyStruct {
}
Category是否可以增加新的成員變數或屬性?
因為Objective-C物件會被編譯成C 的結構體,我們可以在category中增加新的方法,但是我們卻不可以增加成員變數。
在iOS4之後,蘋果的辦法是關聯物件(Associated Objects)的辦法。可以讓我們在Category中增加新的getter/setter,其實原理差不多:既然我們可以用一張表記錄類有哪些方法。那麼我們也可以建立另外一個表格,記錄哪些物件與這個類相關。
要使用關聯物件(Associated Objects),我們需要匯入objc/runtime.h
,然後呼叫objc_setAssociatedObject
建立setter,用getAssociatedObject
建立getter,呼叫時傳入:我們要讓那個物件與那個物件之間建立聯絡,連通時使用的是哪一個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:
中呼叫objc_setAssociatedObject
時,最後的一個引數隨是OBJC_ASSOCIATION_RETAIN_NONATOMIC
,是用來決定要用哪一個記憶體管理方法,管理我們傳入的引數,在示例中,傳入的是NSString
,是一個Objective-C物件,所以必須要retain起來。這裡可以傳入的引數還可以是OBJC_ASSOCIATION_ASSIGN
、OBJC_ASSOCIATION_COPY_NONATOMIC
、OBJC_ASSOCIATION_RETAIN
以及OBJC_ASSOCIATION_COPY
,與property
語法使用的記憶體管理方法是一致,而當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]
產生data task
物件,之後,對這個物件呼叫category 的方法,奇怪的是,會找不到任何selector錯誤。照理說一個data task是NSURLSessionDataTask,繼承自NSURLSessionTask,為什麼我們寫NSURLSessionTask category 沒用呢?
切換到iOS 8的環境下又正常了,可以對這個物件呼叫NSURLSessionTask category 裡面的方法,但是如果寫成NSURLSessionDataTask 的 category,結果又遇到找不到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];
結果:
*****缺圖一張****
如果有一個category不是直接寫在App裡面,而是寫在某個靜態庫(static library),在編譯時app的最後才把這個庫連結進來,預想category 並不會讓連結器(linker)連結(link)進來,你必須要另外在Xcode工程設定的修改連結引數(other linker flag),加上-ObjC
或者-all_load
。會是這樣嗎?但是試了下,並沒有收到unsupported selector
的錯誤。
NSURLSessionTask是一個Foundation物件,而Foundation物件往往不是真正的實現與最上層的介面並是同一個。所以,我們可以查一個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 的結果是:
__NSCFLocalDataTask
__NSCFLocalSessionTask
NSURLSessionTask
NSObject
在iOS7 的結果是:
__NSCFLocalDataTask
__NSCFLocalSessionTask
__NSCFURLSessionTask
NSObject
結論,無論是iOS 8 或 iOS 7,我們新建的data task,都不是直接產生NSURLSessionDataTask
物件,而是產生__NSCFLocalDataTask
這樣的私有物件。iOS 8 上,__NSCFLocalDataTask
並不繼承自NSURLSessionDataTask
,而iOS 7上的__NSCFLocalDataTask
甚至連NSURLSessionTask都不是。
想知道建立的data task到底是不是NSURLSessionDataTask
,可以呼叫“[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:
其實並不是那麼靈驗,好比你去問產品:這到底還要修改需求嗎?
相關文章
- iOS問題整理03----CategoryiOSGo
- IOS category 與 extensioniOSGo
- Category:從底層原理研究到面試題分析Go面試題
- iOS底層原理-CategoryiOSGo
- iOS設計模式——CategoryiOS設計模式Go
- 《iOS面試題 - 老生常談》之提示答案iOS面試題
- iOS面試題iOS面試題
- iOS面試旗開得勝之問題篇iOS面試
- iOS面試之@propertyiOS面試
- iOS Extension Category Protrol 例子理解iOSGo
- iOS 面試問題iOS面試
- iOS Runloop(面試題)iOSOOP面試題
- 知乎iOS面試題iOS面試題
- 技術面試中,當面試官「套路」你時,怎麼「反套路」回去?面試
- iOS 效能優化套路iOS優化
- 關於iOS Class Category的整理iOSGo
- 【iOS】category重寫方法的呼叫iOSGo
- iOS使用Category新增@property變數iOSGo變數
- iOS面試題精選iOS面試題
- iOS面試題 --- 中級iOS面試題
- iOS 面試題彙總iOS面試題
- iOS 面試題總結iOS面試題
- iOS 中級面試題iOS面試題
- iOS 面試題解答二iOS面試題
- iOS基礎面試題iOS面試題
- iOS 面試題整理(一)iOS面試題
- iOS runtime 給 Category 加屬性iOSGo
- 刨根究底之CategoryGo
- iOS面試題答案 --- 底層iOS面試題
- iOS底層面試題–RunLoopiOS面試題OOP
- iOS面試題 — 老生常談iOS面試題
- iOS底層面試題--RunLoopiOS面試題OOP
- iOS面試題總結(七)iOS面試題
- iOS面試題總結(三)iOS面試題
- iOS面試題總結(五)iOS面試題
- iOS面試題總結(六)iOS面試題
- iOS面試題總結(四)iOS面試題
- iOS面試題總結(二)iOS面試題