很多人都知道类别、分类的用法,但是对于一些细节就不是很清楚了,本文主要梳理下这3个概念的细节
类别(Category)
文件特征
- 类别文件有2个,分别为 .h 和 .m
- 命名为: “类名+类别名.h”和“类名+类别名.m”
文件内容格式
.h 文件格式
#import "类名.h"
@interface 类名 (类别名)
// 在此处声明方法
@end
.m 文件格式
#import "类名+类别名.h"
@implementation 类名 (类别名)
// 在此处实现声明的方法
@end
类别的作用
拓展当前类,为类添加方法
类别的局限性
- 无法向现有的类添加实例变量(编译器报“instance variables may not be placed in categories”)。Category 一般只为类提供方法的拓展,不提供属性的拓展。但是利用 Runtime 可以在 Category 中添加属性
- 方法名称冲突的情况下,如果 Category 中的方法与当前类的方法名称重名,Category 具有更高的优先级,类别中的方法将完全取代现有类中的方法(调用方法的时候不会去调用现有类里面的方法实现)。
- 当现有类具有多个 Category 的时候,如果每个 Category 都有同名的方法,那么在调用方法的时候肯定不会调用现有类的方法实现。系统根据编译顺序决定调用哪个 Category 下的方法实现。(可以在 Targets -> Build phases -> Compile Sources 下给多个 Category 更换顺序看看到底在执行哪个方法)
Category 的使用和注意
- Category 中的方法如果和现有类方法一致,工程中任何调用当前类的方法的时候都会去调用 Category 里面的方法(比如:UIViewCtroller、UITableView这些)的方法时要慎重。因为用Category重写类中的方法会对子类造成很大的影响。比如:用Category 重写了 UIViewCtroller 的方法 A,那么如果你在工程中用到的所有继承自 UIViewCtroller 的子类,去调用方法 A 时,执行的都是 Category 中重写的方法 A,如果不幸的是,你写的方法 A 有 Bug,那么会造成整个工程中调用该方法的所有 UIViewCtroller 子类的不正常。除非你在子类中重写了父类的方法 A,这样子类调用方法 A 时是调用的自己重写的方法 A,消除了父类 Category 中重写方法对自己的影响
- Category拓展方法按照有没有重写当前类中的方法,分为未重写的拓展方法和重写拓展方法。且类引用自己的 Category 时,只能在 .m 文件中引用(.h 文件引用自己的类别会报错)。子类引用父类的 Category 在 .h 或 .m 都可以。如果类调用 Category 中重写的方法,不用引入 Category 头文件,系统会自动调用 Category 中的重写方法
- Category 中如果重写了 A 类从父类继承来的某方法,不会影响与 A 同层级的 B 类
- 子类会不会继承父类的 Category: Category 中重写的方法会对子类造成影响,但是子类不会继承非重写的方法(现有类中没有的方法)。但是在子类中引入父类 Category 的声明文件后,子类就会继承 Category 的非重写方法。继承的表现是:当子类的方法和父类 Category 中的方法名完全相同,那么子类里的方法会覆盖掉父类 Category,相当于子类重写了继承自父类的方法
- Category 的作用是向下有效的。即只会影响到该类的所有子类。比如 A 类和 B 类是继承自 Super 类的2个子类,当给 A 类添加一个 Category sayHello 方法,仅有A 类的子类才可以使用 sayHello 方法
拓展(Extension)
文件特征
- 只存在一个文件
- 命名方式:“类名_拓展名.h”
#import "类名.h"
@interface 类名 ()
// 在此添加私有成员变量、属性、声明方法
@end
拓展的作用
- 为类增加额外的属性、成员变量、方法声明
- 一般将类拓展直接写到当前类的 .m 文件中。不单独创建
- 一般私有的属性和方法写到类拓展中
- 和 Category 类似,但是小括号里面没有拓展的名字
拓展的局限性
- Extension 中添加的属性、成员变量、方法属于私有(只可以在本类的 .m 文件中访问、调用。在其他类里面是无法访问的,同时子类也是无法继承的)。假如我们有这样一个需求,一个属性对外是只读的,对内是可以读写的,那么我们可以通过 Extension 实现。
- 通常 Extension 都写在 .m 文件中,不会单独建立一个 Extension 文件。而且 Extension 必须写到 @implementation 上方,否则编译报错
- 类拓展定义的方法和属性必须在类的实现文件中实现。如果单独定义类扩展的文件并且只定义属性的话,也需要将类实现文件中包含进类扩展文件,否则会找不到属性的 setter 和 getter 方法。
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END
//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
self.skills = @"iOS && Web && Node && Hybrid";
self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
NSLog(@"%@",self.skillStacks);
}
@end
总结
- Category 只能拓充方法,不能拓展属性和成员变量(包含成员变量会报错。属性虽然不可以直接拓展,利用 Runtime 可以实现)
- 如果 Category 中声明了1个属性,那么 Category 只会生成 setter 和 getter 的声明,不会有实现
- Extension 也被成为匿名的 Category
- 分类的方法本质是追加在当前类方法列表后,所以分类的方法会覆盖当前类的方法。
「小插曲」:为 Category 实现属性的 Setter 和 Getter
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Person (Student)
/**< 学号*/
@property (nonatomic, strong) NSString *studyNumber;
@end
NS_ASSUME_NONNULL_END
#import "Person+Student.h"
#import <objc/message.h>
@implementation Person (Student)
- (void)sayHi {
NSLog(@"大家好,我叫%@,我今年%zd岁了",self.name,self.age);
}
/*
* 传统的做法是在 setter 里面这样写
_studyNumber = studyNumber;
ARC 自动管理内存
MRC
[_studyNumber release];
[studyNumber retain];
_studyNumber = studyNumber;
但是在 Category里面不会生成对应的实例变量,因此我们可以利用 Runtime 为我们的 category 关联属性的值
setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
getter:objc_getAssociatedObject(self, @selector(firstView));
}
*/
- (void)setStudyNumber:(NSString *)studyNumber {
objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
, OBJC_ASSOCIATION_RETAIN);
}
//@selector(studyNumber)
- (NSString *)studyNumber {
return objc_getAssociatedObject(self, @selector(studyNumber));
}
@end
说明:
objc_setAssociatedObject
的第二个参数是
const void * _Nonnull key
所以可以用 “studyNumber” 或者利用
@selector()
的特性返回的数据类型也满足,所以示例代码选用第二种方式
给分类添加属性的时候,为了避免多人开发对于属性添加造成的覆盖,我们需要为属性起一个独特的名字。比如我们的工程是组件化、模块化开展的工程,那么我们可以为属性命名的时候在前面添加当前模块的前缀。
比如我们在 Login-Register-Module 模块为 NSURL 的 Category 添加一个 title 的属性的时候,可以这样命名 LR_Title。请查看下面的代码
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (Title)
@property (nonatomic, copy) NSString *LR_title;
@end
NS_ASSUME_NONNULL_END
#import "NSURL+Title.h"
#import <objc/runtime.h>
@implementation NSURL (Title)
- (void)setLR_title:(NSString *)LR_title
{
objc_setAssociatedObject(self, @selector(LR_title), LR_title
, OBJC_ASSOCIATION_RETAIN);
}
- (NSString *)LR_title
{
return objc_getAssociatedObject(self, @selector(LR_title));
}
@end