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 的对象其实是属于哪些类:

iOS套路面试题之Category_第1张图片
这些对象属于哪些类

因此,当我们尝试建立Foundation 对象的子类之后,像是继承 NSString,建立我们自己的MyString,假如我们并没有重载原本关于新建实例的方法,我们也不能保证,建立出来的就是MyString的实例。

用工厂模式实现的对象

工厂模式是一套用来解决不用指定特定是哪一个类,就可以新建对象的方法。比如说,某个类下,其实有一堆的子类,但对外部来说并不需要确切知道这些子类而只要对最上层的类,输入致电该的条件,就会挑选出一个符合指定条件的子类,新建实例回调。
在UIKit中,UIButton 就是很好的例子,我们建立 UIButton对象的时候,并不是调用init或者是initWithFrame:,而是调用UIButton 的类方法:buttonWithType:,通过传递按钮的type新建按钮对象。在大多数状况下,会返回UIButton 的对象,但假如我们传入的type是UIButtonTypeRoundedRect,却会返回继承自UIButton的UIRoundedRectButton
验证下:

iOS套路面试题之Category_第2张图片
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 

@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:其实并不是那么灵验,好比你去问产品:这到底还要修改需求吗?

你可能感兴趣的:(iOS套路面试题之Category)