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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,217評論 30 472
  • 1.Difference between shallow copy and deep copy? 淺復制和深復制的...
    用心在飛閱讀 1,031評論 0 9
  • 喜歡就關注我唄! 1.設計模式是什么? 你知道哪些設計模式,并簡要敘述? 設計模式是一種編碼經驗,就是用比較成熟的...
    iOS白水閱讀 1,132評論 0 2
  • 注:此文章來源:Job_Yang 的簡書 1. Object-c的類可以多重繼承么?可以實現多個接口么?Categ...
    廣益散人閱讀 1,360評論 0 13
  • 學生時代的愛情,可能注定是心酸的吧 還記得,初三的時候,你每周三周日都會跟我聊天,因為只有那個時候,你才有手機,每...
    人生幾味閱讀 404評論 0 1