Objective-C 之 Category 详解:从入门到底层原理
在不修改原有类源码的情况下,为它添加新方法------这就是 Category 的魅力所在。
一、什么是 Category?
Category(分类)是 Objective-C 2.0 添加的语言特性,主要作用是为已经存在的类添加方法。它可以在既不子类化,也不侵入一个类的源码的情况下,为原有类添加新的方法,从而实现扩展一个类的目的。
日常开发中最常见的场景 :给 NSString、NSArray、UIView 等系统类添加自定义方法。比如,你可能会给 NSString 加一个 md5 方法,给 UIView 加一个 shake 动画方法。这就是 Category 最典型的应用。
和继承的对比 :虽然继承也能为已有类增加新方法,但它会增加不必要的代码复杂度。而且继承会创建一个新的类型,而 Category 是在原类本身上添加方法,更加轻量。
二、Category 的核心使用场景
| 场景 | 说明 |
|---|---|
| 扩展现有类 | 为系统类(如 NSString、NSArray)添加自定义方法 |
| 分解庞大类 | 将一个大型类按功能拆分成多个文件,便于团队协作和维护 |
| 声明私有方法 | 将 Framework 的私有方法公开化,方便调用 |
| 非正式协议 | 创建 NSObject 或其子类的 Category,实现类似协议的效果(现已逐渐被正式协议取代) |
三、如何在 Xcode 中创建 Category
3.1 通过 Xcode 模板创建(推荐方式)
步骤一:打开新建文件对话框
在 Xcode 项目导航器中,右键点击项目文件夹或某个 Group,选择 New File... (或使用快捷键 Command + N)。
步骤二:选择 Objective-C 模板
在弹出的对话框中,选择 Objective-C File 模板,然后点击 Next。

步骤三:配置 Category 参数
在配置界面中:
- File Name :输入 Category 的名称(如
Reverse) - File Type :选择 Category
- Class :输入要扩展的目标类名(如
NSString)

步骤四:保存文件
点击 Next,选择保存位置,Xcode 会自动生成两个文件:
objectivec
// NSString+Reverse.h - 头文件
// NSString+Reverse.m - 实现文件
步骤五:编写 Category 代码
在生成的 .h 文件中声明新方法,在 .m 文件中实现它们。
3.2 手动创建 Category(进阶方式)
如果 Xcode 模板创建失败或你更喜欢手动创建,可以按以下步骤操作:
第一步:手动创建头文件(.h)
objectivec
// NSString+Reverse.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSString (Reverse)
- (NSString *)reversed;
+ (NSString *)reverseString:(NSString *)strSrc;
@end
NS_ASSUME_NONNULL_END
第二步:手动创建实现文件(.m)
objectivec
// NSString+Reverse.m
#import "NSString+Reverse.h"
@implementation NSString (Reverse)
+ (NSString *)reverseString:(NSString *)strSrc {
NSMutableString *reversed = [NSMutableString string];
for (NSInteger i = strSrc.length - 1; i >= 0; i--) {
[reversed appendString:[strSrc substringWithRange:NSMakeRange(i, 1)]];
}
return reversed;
}
- (NSString *)reversed {
return [NSString reverseString:self];
}
@end
第三步:在需要的地方导入使用
objectivec
#import "NSString+Reverse.h"
// 使用
NSString *str = @"Hello";
NSString *reversed = [str reversed]; // "olleH"
3.3 在 Xcode 中查看和管理 Category
创建完成后,Xcode 项目导航器中会显示:
项目目录/
├── NSString+Reverse.h (头文件)
├── NSString+Reverse.m (实现文件)
NSString+Reverse.h 文件中,@interface NSString (Reverse) 表示这是一个 NSString 的 Category,名称为 Reverse。
一个方便的查看技巧 :在 Xcode 中,按住 Command 键点击类名(如 NSString),可以快速跳转到它的原始定义或查看所有 Category 声明。如果配合 Shift + Command + O 快速打开文件,输入 Category 名称也能迅速定位。
四、Category 的基本用法
4.1 声明和实现
objectivec
// NSString+Reverse.h - 声明文件
#import <Foundation/Foundation.h>
@interface NSString (Reverse)
- (NSString *)reversed;
+ (NSString *)reverseString:(NSString *)strSrc;
@end
objectivec
// NSString+Reverse.m - 实现文件
#import "NSString+Reverse.h"
@implementation NSString (Reverse)
+ (NSString *)reverseString:(NSString *)strSrc {
NSMutableString *reversed = [NSMutableString string];
for (NSInteger i = strSrc.length - 1; i >= 0; i--) {
[reversed appendString:[strSrc substringWithRange:NSMakeRange(i, 1)]];
}
return reversed;
}
- (NSString *)reversed {
return [NSString reverseString:self];
}
@end
4.2 命名约定
Apple 建议 Category 的文件名和 Category 名称都采用 原类名+扩展名 的格式,如 NSString+Reverse.h。
方法命名规范 :为了避免和原始类或其他 Category 的方法冲突,建议给方法名加上前缀 (如 xy_reverseString)。例如:
objectivec
// 推荐:带前缀
- (NSString *)xy_reversedString;
// 不推荐:无前缀,可能冲突
- (NSString *)reversedString;
为什么需要前缀 ?因为 Category 的方法会被直接添加到原类的方法列表中,如果有两个 Category 定义了同名方法,最后编译的那个会生效,这种不确定性很难调试。
五、Category 能添加什么?不能添加什么?
✅ 可以添加的内容
| 内容 | 说明 |
|---|---|
| 实例方法 | 最常用,为类添加新的对象方法 |
| 类方法 | 为类添加新的类方法(+ 方法) |
| 协议 | 让类在 Category 中遵守新协议 |
| 属性声明 | 可以用 @property 声明属性,但不会自动生成实例变量和 setter/getter 实现 |
❌ 不能添加的内容
不能添加实例变量(成员变量)。这是 Category 最核心的限制。
原因 :成员变量的内存布局在编译阶段 就已经确定下来了。如果 Category 可以添加实例变量,就会破坏类的内存结构,导致严重问题。Category 的特性是在运行时动态添加方法,但内存布局在编译期已定,两者之间存在根本性矛盾。
六、如何在 Category 中添加"属性"?
虽然 Category 不能直接添加实例变量,但我们可以用 关联对象(Associated Object) 来间接实现------看起来像有属性,实际上是存储在一个全局映射表里。
6.1 关联对象的 API
objectivec
#import <objc/runtime.h>
// 设置关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 获取关联对象
id objc_getAssociatedObject(id object, const void *key);
// 移除所有关联对象
void objc_removeAssociatedObjects(id object);
6.2 实战:给 Person 添加 name 和 weight 属性
objectivec
// Person+Test.h
#import "Person.h"
@interface Person (Test)
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int weight;
@end
objectivec
// Person+Test.m
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
// _cmd 相当于 @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
- (void)setWeight:(int)weight {
objc_setAssociatedObject(self, @selector(weight), @(weight), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (int)weight {
return [objc_getAssociatedObject(self, _cmd) intValue];
}
@end
6.3 Key 的几种常见写法
| 写法 | 示例 | 说明 |
|---|---|---|
| 静态变量地址 | static void *MyKey = &MyKey; |
最常用,安全 |
| char 类型地址 | static char MyKey; |
省内存 |
| 字符串字面量 | @"name" |
简单,字符串常量地址不变 |
@selector |
@selector(name) |
与 getter 同名,语义清晰 |
_cmd |
objc_getAssociatedObject(self, _cmd) |
在 getter 方法中直接使用 |
6.4 关联策略(objc_AssociationPolicy)
| 策略 | 对应 property 修饰符 |
|---|---|
OBJC_ASSOCIATION_ASSIGN |
assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC |
strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC |
copy, nonatomic |
OBJC_ASSOCIATION_RETAIN |
strong, atomic |
OBJC_ASSOCIATION_COPY |
copy, atomic |
6.5 关联对象的底层原理
关联对象不是存储在被关联对象本身的内存中 ,而是存储在全局统一的 AssociationsManager 中。
底层结构关系如下:
AssociationsManager(全局管理器)
└── AssociationsHashMap(全局哈希表)
└── key = 对象指针(disguised_ptr_t)
└── value = ObjectAssociationMap(该对象的所有关联属性)
└── key = 你传入的 key(如 @selector(name))
└── value = ObjcAssociation(关联信息)
├── policy: 关联策略
└── value: 实际存储的值
核心要点:
- 关联对象不是加在对象本身上,而是存在一张全局表里
- 设置 value 为 nil 等于移除该关联对象
- 对象销毁时,所有关联对象会被自动移除
七、Category vs Extension(类扩展)
很多人会混淆 Category 和 Extension,但它们是完全不同的机制。
| 对比项 | Category(分类) | Extension(扩展/匿名分类) |
|---|---|---|
| 是否有名称 | ✅ 有名称(括号里必须写) | ❌ 匿名(括号为空) |
| 添加时机 | 运行时 | 编译时(和类一起编译) |
| 是否能添加实例变量 | ❌ 不能 | ✅ 可以 |
| 是否能添加属性 | ⚠️ 只声明,不生成实现 | ✅ 可以,会生成实例变量和 setter/getter |
| 必须实现方法 | 不一定(调用时才需要) | 必须实现 |
| 典型的声明位置 | .h 和 .m 文件分开 |
通常在 .m 文件的顶部 |
Extension 示例
objectivec
// Person.m 文件顶部
@interface Person ()
// 声明私有属性(会生成 _age 实例变量和 setter/getter)
@property (nonatomic, assign) NSInteger age;
// 声明私有方法
- (void)privateMethod;
@end
@implementation Person
// 必须实现 privateMethod
- (void)privateMethod {
// ...
}
@end
八、Category 的底层原理
8.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 可以存储实例方法、类方法、协议和属性
- 但没有存储实例变量的字段,印证了 Category 不能添加成员变量
8.2 运行时合并过程
Category 的数据是在运行时合并到类中的,这是它和 Extension 最大的区别。
合并过程大致如下:
- 编译阶段 :Category 被编译成
category_t结构体,存储在 Mach-O 文件的__objc_catlist段中 - Runtime 初始化 :
_objc_init()函数调用map_images()读取所有模块 _read_images()函数 :遍历所有 Category,将category_t中的数据合并到对应的类和元类中- 方法列表处理 :Category 的方法被添加到类的方法列表前面(这就是同名方法会被覆盖的原因)
8.3 方法覆盖的真相
当一个 Category 和原类或另一个 Category 有同名方法 时,最后编译的那个 Category 的方法会生效。
注意 :原类的方法并没有被"删除",只是被"覆盖"了。在消息传递过程中,找到第一个匹配的方法就返回了。可以通过 Runtime 的 class_copyMethodList 打印方法列表来验证------原类的方法其实还在。
九、Category 的使用注意点
1. 方法名冲突
如果两个 Category 或 Category 与原类有同名方法 ,最后编译的那个会生效。建议给方法名加前缀 ,如 xy_xxx,避免冲突。
2. 不要覆盖原类方法
虽然 Category 可以覆盖原类方法,但不推荐这么做 。因为覆盖后你再也无法调用原类的方法,可能导致意想不到的问题。如果确实需要覆盖,考虑使用继承。
3. 分类中可以不用实现所有声明的方法
和普通类不同,在 Category 的实现文件中,你可以不用实现所有声明的方法------只要你不去调用它就不会有问题。
4. +load 方法的特殊行为
- 原类和 Category 的
+load方法都会执行 - 执行顺序:父类 → 子类 → 分类
- 分类的
+load不会覆盖原类的+load
5. 多个同名 Category 的处理
名字相同的 Category 会引起编译报错,所以命名时要注意。
6. 关联对象与 KVO 注意事项
使用关联对象添加的属性,不参与 KVO(Key-Value Observing) ,因为它们不是真正的 @property 生成的实例变量。如果需要监听变化,需要手动在 setter 中添加 willChangeValueForKey: 和 didChangeValueForKey:。
7. Category 和 +load 方法
如果 Category 中实现了 +load 方法,它会和原类的 +load 方法一起被调用 ,不会被覆盖。但 +initialize 方法会被覆盖------只有最后一个加载的 Category 的 +initialize 会生效。
十、总结
| 核心知识点 | 结论 |
|---|---|
| Category 是什么 | 为已有类添加方法的语言特性 |
| 能添加什么 | 实例方法、类方法、协议、属性声明 |
| 不能添加什么 | 实例变量(成员变量) |
| 如何添加"属性" | 使用关联对象(Associated Object) |
| 加载时机 | 运行时(Runtime 阶段合并) |
| 与 Extension 的区别 | Extension 是编译时 ,Category 是运行时 |
| 方法覆盖规则 | 最后编译的 Category 生效 |
| Xcode 创建方式 | New File → Objective-C File → 选择 Category 类型 |
Category 是 Objective-C 非常强大的特性,合理使用可以极大提升代码的可维护性和扩展性。但也要注意它不能添加实例变量和可能引发方法冲突的局限性。关联对象是解决"给 Category 添加属性"需求的标配方案,理解其底层原理有助于更好地掌控内存管理。