Category:为已有类型增加行为
Category 适合在拿不到类源码时添加小范围能力,例如为 NSString、UIButton 或 UILabel 增加便捷方法。它不需要创建子类,通常比继承更轻,也更容易按功能组织。
声明与实现
类名后面括号中的名称就是 Category 名。头文件声明新增接口:
// NSString+Trimmed.h
@interface NSString (Trimmed)
- (NSString *)ys_stringByTrimmingWhitespace;
@end
实现文件使用相同的 Category 名:
// NSString+Trimmed.m
@implementation NSString (Trimmed)
- (NSString *)ys_stringByTrimmingWhitespace {
return [self stringByTrimmingCharactersInSet:
NSCharacterSet.whitespaceAndNewlineCharacterSet];
}
@end
给新增方法加前缀,可以降低与系统未来新增方法或其他 Category 冲突的概率。
常见使用场景
- 为不能修改源码的框架类增加集中、明确的行为。
- 将大型类的实现按功能拆到多个文件。
- 声明在别处实现的方法,让特定编译单元知道这些接口。这里的可见性组织不等于真正的运行时隐私边界。
// A.m
- (void)test {
// A simple implementation for demonstration.
// NSLog(@"test");
}
// A+CategoryA.h
- (void)test;
为什么不能直接增加实例变量
原类编译后,实例内存布局已经确定。Category 可以附加方法,却不能扩展这份布局。因此,在 Category 里声明 property,并不会自动生成 backing ivar 和访问器。
需要额外存储时,可以使用关联对象。objc_setAssociatedObject 按 key 将值关联到一个已有对象,objc_getAssociatedObject 再取回。使用关联存储的属性必须自行实现 getter 和 setter:
#import <objc/runtime.h>
static const void *YSMyStringKey = &YSMyStringKey;
@implementation NSObject (YSMyString)
- (void)ys_setMyString:(NSString *)myString {
objc_setAssociatedObject(
self,
YSMyStringKey,
myString,
OBJC_ASSOCIATION_COPY_NONATOMIC
);
}
- (NSString *)ys_myString {
return objc_getAssociatedObject(self, YSMyStringKey);
}
@end
四个关键参数分别是源对象、唯一 key、关联值,以及决定内存管理和原子性行为的关联策略。
如果 Category 和原类实现了相同 selector,运行时附加的方法可能覆盖原实现。多个 Category 提供同名方法时,结果还涉及加载顺序,不应依赖这种顺序建立业务设计。最稳妥的方式是主动避免冲突。
Class Extension:类自身实现的一部分
Class Extension 的语法类似 Category,但括号里没有名字,因此有时被称为“匿名分类”。它常用于声明属于类内部实现的属性、实例变量和方法。
两者更重要的区别在生命周期:Extension 在编译期与类一起处理,Category 则以元数据形式在运行时附加。 Extension 声明的成员应由类本身实现,编译器可以检查缺失的实现。
放在私有头文件中
多个实现文件需要共享内部接口时,可以将 Extension 放到单独的头文件中。但任何导入这个头文件的源码都能看到声明,因此这是一种组织方式,不是绝对访问控制。
放在 .m 文件中
最常见的做法,是直接在类的 .m 文件里写 Extension。其他文件通常只导入公开头文件,因此内部成员不会进入公开接口。Objective-C 的运行时仍然是动态的,但这个结构能给编译器和维护者提供清晰边界。
只在 @implementation 中定义方法
少量辅助方法也可以直接写在 @implementation 中,不放进公开接口。内部成员较多时,用 Extension 集中声明通常更易读。
继承也可以扩展行为,但它会建立更强的类型关系。只有需要真正的多态关系时才值得使用;代码组织和小范围能力扩展,往往不需要增加一层父子类关系。
如何区分与选择
| 维度 | Category | Class Extension |
|---|---|---|
| 名称 | 有名称 | 匿名 |
| 生效阶段 | Runtime 附加元数据 | 与类一起编译 |
| 实现归属 | 独立的 Category 实现 | 类自身的实现 |
| 实例存储 | 不能修改原布局,额外状态可用关联对象 | 可以参与属性和实例变量的声明 |
| 是否需要原类源码 | 不需要 | 需要参与原类的编译 |
Extension 与类的接口、实现一起解析,因此可以影响实例布局;它不能这样用于 NSString 等已经编译好的框架类。Category 正好相反:适合扩展这些类型的行为,但不能破坏已有二进制代码对对象布局的假设。
“匿名 Category”只是语法上的类比。维护代码时,更应该用编译期与运行时这条边界来理解两者。