iOS套路面試題之Category

weixin_34337265發表於2016-10-31

面試中筆試題和麵試題好多都問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 的物件其實是屬於哪些類:

1346720-c412f50a7b731607.png
這些物件屬於哪些類

因此,當我們嘗試建立Foundation 物件的子類之後,像是繼承 NSString,建立我們自己的MyString,假如我們並沒有過載原本關於新建例項的方法,我們也不能保證,建立出來的就是MyString的例項。

用工廠模式實現的物件

工廠模式是一套用來解決不用指定特定是哪一個類,就可以新建物件的方法。比如說,某個類下,其實有一堆的子類,但對外部來說並不需要確切知道這些子類而只要對最上層的類,輸入致電該的條件,就會挑選出一個符合指定條件的子類,新建例項回撥。
在UIKit中,UIButton 就是很好的例子,我們建立 UIButton物件的時候,並不是呼叫init或者是initWithFrame:,而是呼叫UIButton 的類方法:buttonWithType:,通過傳遞按鈕的type新建按鈕物件。在大多數狀況下,會返回UIButton 的物件,但假如我們傳入的type是UIButtonTypeRoundedRect,卻會返回繼承自UIButton的UIRoundedRectButton
驗證下:

1346720-6720479a42bee054.png
UIButton

我們要擴充套件的是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_ASSIGNOBJC_ASSOCIATION_COPY_NONATOMICOBJC_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:其實並不是那麼靈驗,好比你去問產品:這到底還要修改需求嗎?

相關文章