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.hNSString+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+JSONUIView+Frame
分解庞大类 ViewController+TableViewViewController+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 选择建议

场景 推荐方案 理由
NSStringNSArray 添加方法 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 是"编译时给类加私有声明"的隐藏机制,适合为自己的类添加私有属性和方法。 😊