iOS中的Category类别
问题: OC中类别(Category)是什么?
Category类别是Objective-C语言中提供的一个灵活的类扩展机制。类别用于在不获悉、不改变原来代码的情况下往一个已经存在的类中添加新的方法,只需要知道这个类的公开接口,而不需要知道类的源代码。类别只能为已存在的类添加新的功能扩展方法,而不能添加新的属性。类别扩展的新方法有更高的优先级,会覆盖同名的原类的已有方法。
问题: Category类别与其他特性的比较?
与继承Inheritance的比较:
1.子类继承是进行类扩展的另一种常用方法,当然基于子类继承的扩展更加自由、正式,既可以扩展属性也可以扩展方法。类别可以在不获悉、不改变原来代码的情况下往里面添加新的方法,但也只能添加方法,不能添加属性,属于功能上的扩展。类别扩展的优点是不需要创建一个新的类,而是在系统中已有的类上直接扩展、拼装,不需要更改系统类就可以添加并使用扩展方法。
2.相对于子类继承扩展,类别的另一明显优势是实现了功能的局部化封装,扩展的功能只会在本类被引用时看到。例如,假设原类为UIButton,现在要使用类别扩展一些用于模块A的方法,那么这些扩展方法就可以定义在一个叫做UIButton+A.h的头文件中,只有在引用UIButton+A.h的地方,才能看到原UIButton类有模块A添加的那些扩展方法,如果不需要模块A的功能,不引用UIButton+A.h头文件就看不到UIButton的那些扩展方法的存在,实现扩展模块的隔离。
与扩展Extension的比较:
Category类别和Extension扩展的明显不同在于,后者可以添加属性。另外后者添加的方法是必须要实现的。Extension可以认为是一个私有的匿名的Category,因为Extension定义在.m文件头部,添加的属性和方法都没有暴露在头文件,在不考虑运行时特性的前提下这些扩展属性和方法只能类内部使用,一定程度上可以说是私有的。 ***
问题: 类别有什么作用和好处?类别的局限性和使用注意事项
作用:
可以将类的实现分散到多个不同文件或多个不同框架中(扩充新的方法);
可以创建对私有方法的前向引用;
可以向对象添加非正式协议;
局限性:
类别只能向原类中添加新的方法,且只能添加而不能删除或修改原方法,不能向原类中添加新的属性;
类别向原类中添加的方法是全局有效的而且优先级相对最高,如果和原类的方法重名,会无条件覆盖掉原来的方法,造成难以发现的潜在危险,因此使用类别添加方法一定注意保证是单纯的添加新方法,避免覆盖原来的方法(可以通过添加该类别的方法前缀来防止冲突),否则原方法被类别覆盖了,团队中其他成员不知情的情况下用到这个被覆盖的方法会出现意想不到的问题而难以察觉纠正。
问题: Category类别和Extension类扩展的使用方法?
类别和扩展的区别我们已经知道了,最明显的是类别不可以添加新属性而类扩展可以。类别的使用方法很简单,就是新建某个类的类别扩展文件,然后添加新的方法。而类扩展并不常用,只是常用在.m文件中的头部进行头文件的私有属性变量补充,也就是所谓的类的Continuous区域,是将不想暴露给外部的一些变量定义在类扩展中。
创建类别或扩展文件:
为工程添加新文件并选择Objective-C File:
80.png
填写自定义的扩展名后缀,然后选择文件类型,这里选择Category类别或者Extension扩展,最后选择要扩展的已有类:
81.png
82.png
创建之后得到对应的类别文件:
84.png
添加新的扩展方法:
这里以扩展NSString类的方法为例展示类别扩展的具体用法,分别添加一个类方法和实例方法,并在需要的地方调用:
类别头文件方法声明:
/* NSString+Category.h */
#import <Foundation/Foundation.h>
@interface NSString (Category) {
/* 不可以添加实例变量,编译器会直接报错!*/
}
/* 实例变量被禁止,此处添加属性变量是无意义的,定义的新的属性,编译器没有实现存取方法,自己也无法手动实现存取方法,因为无法获取加下划线的实例变量,除非利用运行时强行实现存取方法则可以成功为类别添加属性,代价较高 */
//@property (nonatomic, copy) NSString *newString;
/** * 扩展一个类方法 */
+ (void)categoryClassMethodOfString;
/** * 扩展一个实例方法 */
- (void)categoryInstanceMethodOfString;
类别方法实现,类别的方法可以不实现,不实现则不可调用否则会崩溃:
/* NSString+Category.m */
#import "NSString+Category.h"
@implementation NSString (Category)
+ (void)categoryClassMethodOfString {
NSLog(@"categoryClassMethodOfString");
}
- (void)categoryInstanceMethodOfString {
NSLog(@"categoryInstanceMethodOfString");
}
@end
在需要的类中引入类别头文件NSString+Category.h,然后即可调用新方法:
#import "NSString+Category.h" /* 1.调用类别扩展的类方法 */
[NSString categoryClassMethodOfString];
/* 2.调用类别扩展的实例方法 */
NSString *string = [NSString stringWithFormat:@""];
[string categoryInstanceMethodOfString];
类扩展的一般用法:
类扩展即类的.m文件中@implementation之前开始的部分,所谓的类的continuous区域:
@interface class name ()
// ... @end
类扩展的作用本来是用于私有函数的前向声明,但最新编译器无需声明也有相同的效果,因此私有方法可在.m文件中任意位置直接写实现而无需在此处进行前向声明,如果在此处声明函数那么一定要在后面进行实现,否则编译器会给出警告。现在类扩展区域的作用主要是快速定义类的私有属性,即将暴露给外部的属性变量定义在头文件中,而不想暴露给外部的属性则直接定义在类扩展区域。【注意这里的私有属性和私有方法并不是绝对私有的,OC中没有绝对的私有方法和私有变量,因为即使它们隐藏在.m实现文件里不暴露在头文件中,开发者仍然可以利用runtime运行时机制对其暴力访问,只是一般情况可以达到私有的效果】
/* 类扩展区域 */
@interface ViewController ()
/* 类扩展属性(默认为private, 类外部不可访问) */
@property (nonatomic,copy) NSString *extensionVariable;
/** * 类扩展方法声明,可省略,要在立刻在后面进行函数实现 */
- (void)extensionInstanceMethod;
+ (void)extensionClassMethod;
@end
86.png
问题: 为什么类别只能添加扩展方法而不能添加属性变量?
这个问法有点歧义,实际问的是为什么向类别添加属性会失败,而非官方为何有意不让类别扩展属性,如果是有意,则可能从设计上考虑保持类别特性的单纯,专门用来扩展功能,和继承的角色区别开,防止类别污染被扩展的类。
话说回来,在类别中扩展属性不能成功的原因是无法在类别中取得属性的加下划线的实例变量名,导致无法手动实现实例变量的存取方法。在类别中定义了属性后,属性其实也成功添加到了类的属性列表中,但编译器只为其声明了存取方法,没有实现,同时又没有合成加下划线的实例变量名,导致无法访问实例变量也无法自己手动实现其存取方法,对于一个不能访问的属性则失去了存在的意义。
但是如果使用运行时的武器,我们其实可以强行实现类别中属性的存取方法,实现在类别中扩展属性。这里在运行时,实现为NSString类扩展一个叫做newString的属性:
/* NSString+Category.h */
#import <Foundation/Foundation.h>
@interface NSString (Category)
/* 在类别中扩展属性 */
@property (nonatomic, copy) NSString *newString;
@end
/* NSString+Category.m */
#import "NSString+Category.h" #import <objc/runtime.h>
@implementation NSString (Category)
/** * 运行时强行实现newString的getter和setter */
- (NSString *)newString {
return objc_getAssociatedObject(self, @"newString");
}
- (void)setNewString:(NSString *)newString {
objc_setAssociatedObject(self, @"newString", newString, OBJC_ASSOCIATION_COPY);
}
@end
/* main.m */
#import <Foundation/Foundation.h>
#import "NSString+Category.h"
int main(int argc, const char * argv[]) {
NSString *string;
/* 调用newString的setter方法 */
string.newString = @"newString";
/* 调用newString的getter方法 */
NSLog(@"%@",string.newString);
return 0;
}
问题: iOS中什么是‘扮演者’?
Objective-C允许应用中的一个类完全替代另一个类,并称其为替代类‘扮演’了被替代的类。所有本来发送给被替代的类的消息都会转而被‘扮演者’类所接收,消息也不需要在发送给‘扮演者’之前先发给被替代的类了。对于类的‘扮演’也有一些限制:一个类可能只能扮演一个它的直接或间接父类,‘扮演者’类不能再定义被替代类所没有的新的实例变量(但是可以定义或者重写方法)。
‘扮演’,和Category类别有类似之处,都允许扩展已有的类,但‘扮演’有两个类别所没有的特性:
一个‘扮演者’类可以通过super关键字调用父类中已经覆盖了的方法,因此可以辅助被替代类的功能实现;
一个‘扮演者’类可以覆盖在类别中定义的方法,优先级更高;