分类诞生的历史

Objective-C 的分类(Category)语法并非随语言初始版本一同出现,而是随着语言发展逐步引入的重要特性。以下是关键时间线和技术背景分析:

Objective-C 由 Brad Cox 和 Tom Love 在 1980 年代初基于 C 语言开发,旨在添加 Smalltalk 风格的面向对象特性。初始版本(1981年)​​ 仅包含核心的面向对象机制(如类、继承、消息传递),​​未包含分类语法​​。此时语言的核心是“软件组件”(Software-IC)概念,聚焦于通过类封装数据和操作。

​​Objective-C 的类别(Category)语法是在 1990 年代初加入到 Objective-C 语言中的,具体时间是随着 NeXTSTEP 操作系统的发布而引入的。NeXTSTEP 是苹果公司前首席执行官史蒂夫·乔布斯创办的 NeXT 公司开发的操作系统,它使用了 Objective-C 作为主要编程语言,并引入了类别这一特性。
​​
​​设计动机​​:解决无法修改原始类(如系统框架类)时扩展功能的需求。例如,为 NSString 添加绘图方法而无需继承或修改原始实现。这是最核心的应用场景。早期 Objective-C 依赖继承和组合实现扩展,分类则提供了更轻量、灵活的替代方案,直接为类添加方法。

核心语法:声明与实现

分类,分类,顾名思义就知道,它是对类的拆分,所以需要一个明确的类,然后是分类的名称。

  • 分类的声明:
    1
    2
    3
    @interface ClassName (CategoryName)
    // 分类需要的属性。。。
    @end
  • 分类的实现:
    1
    2
    3
    @implementation ClassName (CategoryName)
    // 方法的实现
    @end

使用场景

按照实际使用频率排序:

🛠️ 一、扩展系统或第三方框架类

场景:为 NSString、NSArray 等系统类或者第三方框架提供的类添加项目所需的特定方法。
示例:为 NSString 添加邮箱格式验证方法。

1
2
3
4
5
6
7
8
9
10
11
12
// NSString+Validation.h
@interface NSString (Validation)
- (BOOL)isValidEmail;
@end

// NSString+Validation.m
@implementation NSString (Validation)
- (BOOL)isValidEmail {
// 正则验证逻辑
return [emailTest evaluateWithObject:self];
}
@end

优势:

  • 不修改 NSString 源码,避免破坏系统类的封装性;
  • 方法对所有 NSString 实例及子类(如 NSMutableString)生效。

⚙️ 二、面向切面编程(AOP)支持

场景​​:无侵入添加横切关注点(如日志、埋点)。
示例​​:通过分类实现方法交换(Method Swizzling):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface ViewController (log)
@end

@implementation ViewController (log)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(swizzled_viewDidAppear:));
method_exchangeImplementations(original, swizzled);
});
}
- (void)swizzled_viewDidAppear:(BOOL)animated {
// 调用原始方法实现
[self swizzled_viewDidAppear:animated];
// 实现需求
[Analytics logEvent:@"ScreenAppeared" forView:self];
}
@end

​​优势​​:

  • 统一埋点逻辑,避免污染业务代码;
  • 适用于调试、性能监控等全局行为。

🧩 三、代码模块化与可读性提升

场景​​:将大型类的功能拆分到多个分类中,按逻辑分组管理。
示例​​:自定义 UIView 时分离布局、动画、样式代码;或者 NSApplication.h,UIApplication.h 头文件的实现方式

1
2
3
4
5
6
7
8
9
// UIView+Layout.h  (布局相关)
@interface UIView (Layout)
- (void)autoCenterInSuperview;
@end

// UIView+Animation.h (动画相关)
@interface UIView (Animation)
- (void)fadeInWithDuration:(NSTimeInterval)duration;
@end

底层实现原理

分类中的方法都会合并到类对象的方法列表中。具体来说,分类中的实例方法会被合并到类对象的方法列表中,分类中的类方法会被合并到元类对象的方法列表。

那么这个合并的动作发生在什么时刻?是程序启动的时候?还是编译源码的时候?

根据分类的能力来说,分类可以给系统的类和第三方的类添加方法,那么可以推理出一定不是在编译时进行合并的,因为在编译时根本无法获取到系统的类和第三方的类对象啊,只能在运行时加载系统的类和第三方类的时候将分类中的方法添加到类中。其实实际上也确实这样的,我们可以从 Objective-C 的运行时库源码和编译后的产物中得到验证。

1. 编译时的处理

分类的底层结构,即编译器是如何处理我们编写的分类的。

1
2
3
4
5
6
7
@interface NSDate (custom)
@end
@implementation NSDate (custom)
- (void)hello {
NSLog(@"%s", __func__);
}
@end

我们在源码中创建了多少个分类,在编译时就会产生多少个分类的结构体变量。如:你给 NSObject 创建了 2 个分类,那么在编译时就生成了 2 个分类结构体变量。分类的名字就是变量名的最后部分。通过查看编译后的分类结构也能验证,并不是在编译时就合并到类中了。编译时仅仅是将分类的信息保存了下来。

2. 运行时的合并

那运行时到底是如何合并分类信息到类中的呢?那就是 objc4 的源码分析了

_objc_init -> map_images -> map_images_nolock() -> _read_images()

_read_images() 函数里面。核心方法 remethodizeClass() 里面有一个 attachCategories() 方法。最后是 class 的 rw 的 attachLists 方法的实现。

首先将方法数组扩容,然后将原始的方法列表移动到数组的末尾,再将所有分类的方法列表复制到前面。这样就导致了分类的方法列表会在原始的方法列表前面,造成了类似覆盖的效果。就是说分类中如果存在和原始类中同名的方法,那么就会导致原始类中的方法不会被调用,而是调用分类中的方法。而如多个分类中存在同名的方法,则会按照编译的顺序优先调用,最后编译的分类中的方法会放在数组前面导致的。

分类涉及的相关知识还是挺深的。。。个人认为,想要掌握 Category 的底层原理,必须先掌握,Objective-C 对象模型,运行时库初始化,等等底层知识。

使用注意点

理解了分类的底层原理之后,自然而然的就会考虑到不恰当使用分类可能会导致的问题:

不能添加实例变量

这一点需要结合分类的设计初衷来讲,虽然从技术上来说,分类可以做到给类添加实例变量,但是会带来巨大的不稳定性和性能开销以及破坏类的封装性,从多方面考虑让分类支持添加实例变量都不是一个合适的选择。所以实际上也并没有让分类支持添加实例变量的能力,而是提供了关联对象技术达到类似实例变量的效果。

命名冲突风险

这是一个非常有可能会遇到的问题,多个分类中如果存在同名的方法,那么运行时到底会执行哪一个方法这是有答案的。取决于最后编译的是哪个分类,从前面的底层原理中得知,最后面的编译的分类会被添加到类的方法列表的前面。但是实际运用分类过程中是极不推荐在分类中使用同名方法的,应该尽量避免,解决办法就是给每个分类的方法添加一个前缀,这样大大降低了同名的可能。

不可覆盖原生方法

覆盖系统方法可能导致框架行为异常。这也是非常有可能出现的问题,而且危险性较高,建议分类仅用来添加新功能,而不是修改原有的逻辑。

分类的核心价值在于 ​​“无侵入式扩展”​​,任何可能破坏类封装性或导致运行时冲突的方案(如方法覆盖),都应优先考虑替代设计。

相关面试题

  • Category 的使用场景是什么
  • Category 的实现原理
  • Category 和 Extension 的区别是什么
  • Category 有 load 方法吗?load 方法在什么时候被调用?load 方法能继承吗?
    • 分类有没有 load 方法?这个问题十分奇怪?分类哪里来的 load 方法?你在分类里写了 load 方法就肯定有,没写就肯定没有啊。。。
    • 这回是 load_images 方法的源码分析了。先调用类的 load 方法,再调用分类的 load 方法。
    • objc 运行时在初始化的时候,会直接调用类的 load 方法,而不是通过消息发送的形式调用 load 方法。
    • 我觉得,你得先搞懂 Objective-C 语言提供 load 方法的目的是什么?
    • load,initialize 方法在 NSObject.h 文件中声明,并没有实现。那么 Objective-C 语言在 NSObject.h 文件中声明 load 和 initialize 又不实现的目的是什么呢?
  • load、initialize 方法的区别是什么?它们在 category 中的时候被调用的顺序是什么?出现继承时它们之间的被调用过程是怎样的?
  • category 能否添加成员变量?如果可以,如何添加,如果不可以,解释为什么不可以?