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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,107评论 29 470
  • 1.Difference between shallow copy and deep copy? 浅复制和深复制的...
    用心在飞阅读 986评论 0 9
  • 喜欢就关注我呗! 1.设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的...
    iOS白水阅读 1,081评论 0 2
  • 注:此文章来源:Job_Yang 的简书 1. Object-c的类可以多重继承么?可以实现多个接口么?Categ...
    广益散人阅读 1,333评论 0 13
  • 学生时代的爱情,可能注定是心酸的吧 还记得,初三的时候,你每周三周日都会跟我聊天,因为只有那个时候,你才有手机,每...
    人生几味阅读 372评论 0 1