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