全部文章

Objective-C 的 Category 与 Class Extension

Category 在运行时扩展行为,Class Extension 参与类本身的编译。理解两者的生命周期、存储能力和使用边界。

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 集中声明通常更易读。

继承也可以扩展行为,但它会建立更强的类型关系。只有需要真正的多态关系时才值得使用;代码组织和小范围能力扩展,往往不需要增加一层父子类关系。

如何区分与选择

维度CategoryClass Extension
名称有名称匿名
生效阶段Runtime 附加元数据与类一起编译
实现归属独立的 Category 实现类自身的实现
实例存储不能修改原布局,额外状态可用关联对象可以参与属性和实例变量的声明
是否需要原类源码不需要需要参与原类的编译

Extension 与类的接口、实现一起解析,因此可以影响实例布局;它不能这样用于 NSString 等已经编译好的框架类。Category 正好相反:适合扩展这些类型的行为,但不能破坏已有二进制代码对对象布局的假设。

“匿名 Category”只是语法上的类比。维护代码时,更应该用编译期与运行时这条边界来理解两者。