Objective-C 之 Category 详解

Objective-C 之 Category 详解:从入门到底层原理

在不修改原有类源码的情况下,为它添加新方法------这就是 Category 的魅力所在。


一、什么是 Category?

Category(分类)是 Objective-C 2.0 添加的语言特性,主要作用是为已经存在的类添加方法。它可以在既不子类化,也不侵入一个类的源码的情况下,为原有类添加新的方法,从而实现扩展一个类的目的。

日常开发中最常见的场景 :给 NSStringNSArrayUIView 等系统类添加自定义方法。比如,你可能会给 NSString 加一个 md5 方法,给 UIView 加一个 shake 动画方法。这就是 Category 最典型的应用。

和继承的对比 :虽然继承也能为已有类增加新方法,但它会增加不必要的代码复杂度。而且继承会创建一个新的类型,而 Category 是在原类本身上添加方法,更加轻量。


二、Category 的核心使用场景

场景 说明
扩展现有类 为系统类(如 NSStringNSArray)添加自定义方法
分解庞大类 将一个大型类按功能拆分成多个文件,便于团队协作和维护
声明私有方法 将 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 最大的区别。

合并过程大致如下:

  1. 编译阶段 :Category 被编译成 category_t 结构体,存储在 Mach-O 文件的 __objc_catlist 段中
  2. Runtime 初始化_objc_init() 函数调用 map_images() 读取所有模块
  3. _read_images() 函数 :遍历所有 Category,将 category_t 中的数据合并到对应的类和元类中
  4. 方法列表处理 :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 FileObjective-C File → 选择 Category 类型

Category 是 Objective-C 非常强大的特性,合理使用可以极大提升代码的可维护性和扩展性。但也要注意它不能添加实例变量和可能引发方法冲突的局限性。关联对象是解决"给 Category 添加属性"需求的标配方案,理解其底层原理有助于更好地掌控内存管理。


相关推荐
他们都不看好你,偏偏你最不争气18 天前
【iOS】Runtime - Part 2 && 消息发送:缓存、查找与转发
macos·ios·objective-c·cocoa
他们都不看好你,偏偏你最不争气18 天前
【iOS】Runtime - Part 1 && 对象与类的本质
macos·ios·objective-c·cocoa
鹤卿12319 天前
(OC)UI学习——网易云仿写
ui·ios·objective-c
秋雨梧桐叶落莳19 天前
iOS——QQ音乐仿写项目总结
学习·macos·ui·ios·mvc·objective-c·xcode
wjm04100620 天前
ios内存管理
ios·objective-c·swift·客户端开发
音视频牛哥20 天前
iOS如何实现RTSP/RTMP低延迟播放?SmartMediaKit播放器集成说明
objective-c·低延迟rtsp播放器·低延迟rtmp播放器·ios rtmp player·ios rtsp player·ios平台rtsp播放器·ios平台rtmp播放器
游戏开发爱好者821 天前
iPhone真机调试有哪些方法?一次定位推送权限问题时整理出来的几种方案
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
zhaocarbon23 天前
OC HTTP SSE客户端
http·ios·objective-c