Objective-C 类别 category 简介
status
category
date
summary
slug
icon
tags
password
什么是类别
一个类无论设计的多么精妙,总会遇到设计之初没有预料到的功能扩展,如何优雅地为类添加方法呢?利用Objective-C的Runtime分配机制,可以为现有的类添加新的方法,这些新的方法被称为类别(category)。其实我认为这个翻译不怎么恰当,但又不能像Swift的Extension一样翻译为扩展,因为会和Objective-C本身的Class Extension混淆。
Category
就像一个许可证,表明这个类可以进行的特定行为。为什么使用类别
当类需要添加方法时时,继承是一个很好的选择,通过编写子类的方法,可以完成对类的扩展。我们还可以创建工具类,传入需要扩展的类,在工具类中进行需要的操作,然后返回结果。但这两个方式都有各自的局限性。
比如我们需要为
NSString
添加一个获得字符串长度的方法,可以创建子类MyString : NSString
,但想要使用这个方法,就必须创建子类MyString
的实例,之前已经创建的实例都要进行修改,也不利于将来的扩展。另外,如果我们通过某些API获得了一个NSString
对象,就要先转化为子类再进行方法调用,这样显得非常繁琐和不优雅,更重要的是不具备兼容性。如果使用工具类呢?确实,我们可以把需要的操作全部放入工具类,然后接受工具类返回的结果。但是久而久之,工具类会变得非常臃肿,而且工具类中的方法和需要操作的类本身联系不大。想要获得字符串的长度,首先想到的不是
NSString
的属性和方法,而是想到工具类,不太符合常规的编程思想和流程。而使用category就没有以上的顾虑了,首先它具有兼容性,能够让所有已有的、即将创建的实例都能够使用被添加的方法,同时方法和类保持了极大的关联性。还有另外一个好处,那就是可以根据方法的不同,将庞大的类拆分为多个类别,保存在不同的文件中,提高了代码的可读性。
类别的使用姿势
类的扩展
还是以返回
NSString
的长度为例,比如我们需要把该值存入NSArray
或者NSDictionary
中,就需要将长度包装在NSNumber
中:当然实际运用时不用这么麻烦,但这不在本文的讨论范围内,只是举个例子。接下来使用category实现,首先创建类别文件,一般的文件命名规范为
类名+类别名
:在这段代码中,我们给
NSString
添加了NumberConvenience
类别。只要保证类别名称唯一,可以向一个类添加任意数量的类别。接下来在@implementation
中实现这个方法:使用时:
main
函数创建了一个新的NSMutableDictionary
对象,然后添加了“hello”
作为key,并将它的长度作为value。通过
@"字符串"
这种方式创建的字符串,是地地道道的NSString
对象,所以它可以使用类别。我们已经为整个NSString
类创建了相应的类别,这种兼容性保证了所有的NSString
实例都可以使用lengthAsNumber
属性。类的拆分
我们可以将类的接口放入头文件,将类的实现代码放入.m文件中,但是不能将
@implementation
分散到多个.m文件中。如果想要将大型的单个类拆分到多个不同的.m文件中,可以使用类别,将多个方法拆分到不同的文件中,这样做有很多好处:- 可以减少单个文件的体积
- 可以按逻辑将功能组织到不同的
Category
中
- 可以由多个开发者共同开发一个类
- 可以按需加载想要的
Category
举一个🌰来说,现在有一个很庞大的类
CategoryThing
,它的头文件和.m文件如下:假设这两个
NSInteger
也非常复杂,那么我们需要用类别分别实现它们:Thing2
类别的实现完全一致,具体使用时:我们将“非常庞大的”
Thing1
和Thing2
拆分出来之后,在使用时除了需要导入类别的头文件,其他地方的用法基本可以和拆分之前一样。当然在这个例子中CategoryThing
还不够大,NSInteger
也非常简单,完全用不着类别拆分,但是我们可以想象一下很大类,例如AppKit中的NSWindow
。NSWindow
的官方文档打印出来就有60页之多,但通过类别的拆分,将键盘响应(NSKeyboardUI
)有关的代码放在一个类别文件,工具栏(NSToolbarSupport
)位于另一个文件,还有拖拽功能(NSDrag
)等等都可以分文件存放,这样以来便可以小规模地管理庞大的NSWindow
了。实现非正式协议
这里涉及到协议和委托,协议可以理解为接口,委托是一种设计模式,A可以委托B去做本来A自己要做的事情。假设我们需要一个
UILabel
,在文字颜色发生改变时,发送一个通知,首先自定义一个UILabel
的子类,然后创建NSObject
的类别,并声明代理方法:上面
MyLabel
中添加了类型为id
的delegate
,表示可以接受任何类型。接下来实现改变文字颜色的实例方法,类别的.m
空着不实现:在具体使用时,需要指定
MyLabel
的delegate
,并实现代理方法,例如在ViewController
中使用:在
viewDidLoad
方法中,创建了label
实例并指定了它的delegate
为self
,即当前的ViewController
。然后调用之前定义并实现的changeTextColor
方法,再实现textColorChanged
代理方法。运行时,因为速度比较快,所以会直接显示为红色,但通过输出Text color is changed to UIExtendedSRGBColorSpace 1 0 0 1
,可以验证我们的简单协议生效了。这里利用了所有类都是
NSObject
的子类这一特点,delegate
可以指定为任意对象,当然也包括例子中的ViewController
,实现了简单的非正式协议。这种协议不像正式协议,需要类似<NSCopying>
的声明。但非正式协议有一个致命的缺点,那就是如果在ViewController
中没有实现textColorChanged
代理方法,程序就会崩溃,所以需要检查代理方法是否被实现:引用父类未公开的方法
如果在子类中直接调用父类中未公开的方法,编译器自然会报错,不能通过编译。但如果我们在子类的类别中声明父类的方法,子类就可以进行调用了。我们先分别创建父类:
子类:
编译器报错:Method 'fatherCall' is defined in Class 'Father' and is not visible.这里我使用的是AppCode,Xcode应该会报类似的错误,只是描述方法不太一样。
现在我们创建一个父类的类别:
再在
Son
中引入Father+PrivateCall.h
,这时就可以通过编译,并且可以正常使用了:运行的结果是打印出
Call from father
,说明我们通过给父类添加类别,让子类访问到了父类未公开的方法。当然这只是通过方法名匹配做到的,在平时不应该使用。苹果官方会拒绝使用系统私有API的应用上架,因此即使学会了如何调用私有方法,在遇到调用其它类的私有方法时,要谨慎处理,尽量用其它方法替代。
类别的缺陷
属性和实例变量
可以在类别中添加属性,但不能添加实例变量,而且属性必须是
@dynamic
类型的。添加属性的好处是,可以使用点符号(.)来访问getter
和setter
方法。这里说的属性指的是成员变量,而不是@property
。@dynamic:属性的 setter 与 getter 方法由用户自己实现,不自动生成。假如一个属性被声明为 @dynamic var,而且你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
方法覆盖
当类别中的方法与现有方法重名,类别具有更高的优先级,类别方法将完全取代初始方法,所以推荐在类别方法名中加一个前缀,以确保不会发生名称冲突。
当我们真正需要去重写一个方法的时候,尽量使用子类继承或者
Method Swizzling
去实现,不要通过类别方法重名进行覆盖。因为通过类别方法去覆盖方法,不管是覆盖类本身的方法,还是覆盖已有类别的方法,都会出现问题:Category
没有办法代替子类,它不能通过super
去调用父类方法,而且覆盖之后类的现有方法将永远不会执行。load
方法是一个例外,它会在当前类执行后,再执行Category
中的方法。
Category
方法不能可靠地覆盖另一个Category
中同名的方法,也就是一个Undefined Behavior。例如UIViewController+A
和UIViewController+B
都覆盖了viewDidLoad
,但不知道哪个可以执行。
勘误:Category的方法会在运行时插入到方法列表的前面,也就是最后编译的方法会被执行,所以根据Build Phases中的compile source的顺序可以确定。
总结
Category
的诞生是为了让开发者更方便拓展一个类,或是更优雅地组织一个类,而不是彻底地改变它。和接口不同的是,类别是扩展,只是允许对象可以调用更多的方法,如果部分方法“可以实现,但没必要”,也可以选择不实现;而接口是规定了对象必须实现某些方法。参考:《Objective-C基础教程》
Loading...