Objective-C 分类(Category)与扩展(Extension)

Objective-C 分类与扩展:一篇讲透它们的本质区别

同为 @interface,一个括号里有名字,一个括号里是空的。它们到底有什么不同?本文从底层原理到实战场景,带你彻底搞懂 Category 和 Extension。


一、引言:一个容易混淆的起点

如果你刚接触 Objective-C,看到下面两段代码一定会困惑:

objectivec 复制代码
// 这段代码是什么?
@interface NSString (Reverse)
- (NSString *)reversedString;
@end

// 这段代码又是什么?
@interface MyViewController ()
@property (nonatomic, strong) NSMutableArray *cache;
@end

它们都用了 @interface,都有括号,看起来很相似。但它们是完全不同 的两种机制:分类(Category) 和 扩展(Extension)。

搞混它们,不仅会让代码难以维护,还可能引入难以追踪的 bug。本文将从语法、能力、加载时机、使用场景四个维度,彻底讲透它们的区别。


二、一句话概括

维度 Category(分类) Extension(扩展/匿名分类)
本质 运行时给类添加方法 编译时给类添加私有声明
目的 扩展一个类(不修改原类) 隐藏类的私有实现细节
括号 有名字:@interface 类名 (分类名) 没名字:@interface 类名 ()
文件位置 通常在独立的 .h/.m 中 通常在主类的 .m 文件顶部
加载时机 运行时(Runtime 动态加载) 编译时(和类一起编译)
方法实现是否必须 ❌ 不需要全部实现 ✅ 必须全部实现

三、语法与创建方式

3.1 Category(分类)

语法格式:

objectivec 复制代码
// 头文件:类名+分类名.h
@interface 原类名 (分类名)
// 方法声明
@end

// 实现文件:类名+分类名.m
@implementation 原类名 (分类名)
// 方法实现
@end

在 Xcode 中创建:

  1. File → New → File...(Command + N)
  2. 选择 Objective-C File
  3. File Type 选择 Category
  4. Class 输入要扩展的类名(如 NSString)
  5. File Name 输入分类名(如 Reverse)

Xcode 会自动生成两个文件:NSString+Reverse.h 和 NSString+Reverse.m。

完整示例:

objectivec 复制代码
// NSString+Reverse.h
#import <Foundation/Foundation.h>

@interface NSString (Reverse)
- (NSString *)reversedString;
+ (NSString *)reverseString:(NSString *)str;
@end
objectivec 复制代码
// NSString+Reverse.m
#import "NSString+Reverse.h"

@implementation NSString (Reverse)

- (NSString *)reversedString {
    NSMutableString *result = [NSMutableString string];
    for (NSInteger i = self.length - 1; i >= 0; i--) {
        [result appendString:[self substringWithRange:NSMakeRange(i, 1)]];
    }
    return result;
}

+ (NSString *)reverseString:(NSString *)str {
    return [str reversedString];
}

@end

使用:

objectivec 复制代码
#import "NSString+Reverse.h"

NSString *str = @"Hello";
NSString *reversed = [str reversedString];  // "olleH"

3.2 Extension(扩展)

语法格式:

objectivec 复制代码
// 通常在 .m 文件顶部,@implementation 之前
@interface 原类名 ()
// 属性、方法声明
@end

特点:括号里是空的,没有名字。这就是它被称为"匿名分类"的原因。

完整示例:

objectivec 复制代码
// Person.h - 公开头文件
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
@end
objectivec 复制代码
// Person.m - 实现文件
#import "Person.h"

// ⚠️ 扩展:括号里是空的
@interface Person ()
// 私有属性(会生成 _privateAge 实例变量和 setter/getter)
@property (nonatomic, assign) NSInteger privateAge;
// 私有方法声明
- (void)privateMethod;
@end

@implementation Person

- (void)sayHello {
    NSLog(@"Hello, I'm %@", self.name);
    [self privateMethod];  // 内部调用
}

// ✅ 必须实现扩展中声明的方法
- (void)privateMethod {
    self.privateAge = 18;
    NSLog(@"私有方法被调用,年龄:%ld", (long)self.privateAge);
}

@end

四、核心能力对比

4.1 能否添加实例变量?

Category Extension
添加实例变量 ❌ 不能 ✅ 可以

为什么 Category 不能添加实例变量?

因为类的内存布局在编译阶段 就已经确定了。Category 是在运行时才加载的,此时内存布局已固定,无法再增加新的存储空间。

Extension 为什么可以?

Extension 在编译时就和主类合并,它们属于同一个编译单元,所以可以添加实例变量。

4.2 属性声明的差异

Category Extension
声明属性 ✅ 可以 ✅ 可以
自动生成实例变量 ❌ 不会 ✅ 会生成 _xxx
自动生成 setter/getter ❌ 不会 ✅ 会生成

Category 中的属性:只声明,不实现。需要手动实现或使用关联对象。

objectivec 复制代码
// Person+Test.h
@interface Person (Test)
@property (nonatomic, copy) NSString *nickName;  // 只声明,不会自动生成
@end

// Person+Test.m
#import <objc/runtime.h>

@implementation Person (Test)

- (void)setNickName:(NSString *)nickName {
    objc_setAssociatedObject(self, @selector(nickName), nickName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)nickName {
    return objc_getAssociatedObject(self, _cmd);
}

@end

Extension 中的属性:会像主类属性一样,自动生成实例变量和 setter/getter。

4.3 方法实现的必需性

Category Extension
方法必须实现? ❌ 不必须 ✅ 必须

Category :声明了方法但没有实现,编译可以通过。只有在调用时才会崩溃。

Extension :声明了方法但没有实现,编译直接报错。因为 Extension 是主类的一部分,编译器会强制检查所有声明的方法是否都被实现。

4.4 加载时机

Category Extension
加载时机 运行时 编译时
是否动态 ✅ 动态加载 ❌ 静态合并

Category 的加载过程:

  1. 编译阶段:Category 被编译成 category_t 结构体
  2. Runtime 初始化:_objc_init() 读取所有 Category
  3. _read_images() 函数:将 Category 的方法合并到类的方法列表前面

Extension 的加载过程:

在编译时直接和主类合并,不存在"加载"的概念。


五、底层原理分析

5.1 Category 的 Runtime 结构

Category 在 Runtime 层被定义为 category_t 结构体:

c 复制代码
struct category_t {
    const char *name;                          // 类名
    classref_t cls;                            // 对应的类
    struct method_list_t *instanceMethods;     // 实例方法列表
    struct method_list_t *classMethods;        // 类方法列表
    struct protocol_list_t *protocols;         // 协议列表
    struct property_list_t *instanceProperties; // 属性列表
};

关键发现 :结构体中没有存储实例变量的字段。这从源码层面证明了 Category 不能添加实例变量。

5.2 方法覆盖的机制

当 Category 和原类有同名方法 时,Category 的方法会优先被调用。这是因为 Category 的方法被添加到了类的方法列表前面。

objectivec 复制代码
// 原类
@interface Person : NSObject
- (void)sayHello;
@end

@implementation Person
- (void)sayHello {
    NSLog(@"Person sayHello");
}
@end

// Category
@interface Person (Test)
- (void)sayHello;
@end

@implementation Person (Test)
- (void)sayHello {
    NSLog(@"Category sayHello");
}
@end

// 调用
Person *p = [[Person alloc] init];
[p sayHello];  // 输出:"Category sayHello"

注意:原类的方法并没有被"删除",只是被"覆盖"了。在消息传递过程中,方法列表按顺序查找,第一个匹配的就被返回。


六、使用场景对比

6.1 Category 的典型使用场景

场景 示例
扩展系统类 NSString+JSON、UIView+Frame
分解庞大类 ViewController+TableView、ViewController+Network
为类添加测试方法 在测试 target 中给类添加辅助方法
非正式协议(历史用法) NSObject+MyProtocol(已被正式协议取代)

6.2 Extension 的典型使用场景

场景 示例
声明私有属性 @property (nonatomic, strong) NSMutableArray *cache;
声明私有方法 - (void)privateSetupUI;
重写只读属性为可读写 在 .h 中声明为 readonly,在 Extension 中重新声明为 readwrite
遵循私有协议 @interface MyViewController () <UITableViewDelegate>

6.3 实战场景对比

objectivec 复制代码
// ============================================================
// 场景1:给系统类加方法 → 用 Category
// ============================================================

// UIView+Frame.h
@interface UIView (Frame)
@property (nonatomic, assign) CGFloat x;
@property (nonatomic, assign) CGFloat y;
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
@end

// 使用关联对象实现 Frame 相关的便捷方法


// ============================================================
// 场景2:隐藏类的私有实现 → 用 Extension
// ============================================================

// MyViewController.m
@interface MyViewController ()
@property (nonatomic, strong) NSTimer *refreshTimer;  // 私有属性
- (void)setupRefreshTimer;                     // 私有方法
- (void)handleRefresh;                         // 私有方法
@end

@implementation MyViewController
// 必须实现所有私有方法
- (void)setupRefreshTimer { /* ... */ }
- (void)handleRefresh { /* ... */ }
@end

七、快速决策指南

7.1 决策流程图

复制代码
需要给类添加功能?
        │
        ▼
   我能修改这个类的源码吗?
        │
   ┌────┴────┐
   │         │
  是         否
   │         │
   ▼         ▼
用 Extension    用 Category
(私有实现)    (扩展系统类)

7.2 选择建议

场景 推荐方案 理由
给 NSString、NSArray 添加方法 Category 无法修改系统类源码
给自己的类添加私有属性 Extension 需要实例变量存储数据
给一个已有类添加测试辅助方法 Category 不影响主类,测试专用
将一个大型 ViewController 拆分为多个文件 Category 按功能模块分离代码
将 readonly 属性在内部改为可读写 Extension 重新声明属性权限
声明只在类内部使用的方法 Extension 不暴露给外部调用

八、总结

8.1 核心区别速记表

判断标准 Category Extension
括号里有名字吗? ✅ 有(如 (Reverse)) ❌ 没有(空括号 ())
文件位置 独立 .h/.m 文件 通常在 .m 顶部
能添加实例变量吗? ❌ ✅
加载时机 运行时 编译时
方法必须实现吗? ❌ ✅
一句话记忆 "给类加方法" "隐藏私有实现"

8.2 面试常问考点

  1. Category 能添加属性吗? → 能声明,但不会自动生成 setter/getter。需要手动实现或用关联对象。
  2. Category 能添加实例变量吗? → 不能。原因是运行时类的内存布局已经固定。
  3. Category 和 Extension 的最大区别是什么? → Extension 是编译时的、可以添加实例变量、方法必须实现;Category 是运行时的、不能添加实例变量、方法可以不实现。
  4. Extension 和 Category 哪个优先级高? → Extension 和主类是一体的,没有优先级概念。Category 的方法会覆盖原类方法。
  5. Category 的方法覆盖原类方法后,原类的方法还能调用吗? → 不能通过正常方式调用。但可以通过 Runtime 的 class_copyMethodList 获取并调用。

8.3 一句话总结

Category 是"运行时给类加方法"的扩展工具,适合扩展现有类(尤其是系统类);Extension 是"编译时给类加私有声明"的隐藏机制,适合为自己的类添加私有属性和方法。 😊