跳到主要内容

Category 与 Extension

问题

Category 和 Extension 有什么区别?Category 能添加实例变量吗?多个 Category 方法重名怎么办?

答案

Category(分类)

为已有类添加方法,无需修改源代码:

// NSString+Utils.h
@interface NSString (Utils)
- (BOOL)isValidEmail;
- (NSString *)md5Hash;
@end

// NSString+Utils.m
@implementation NSString (Utils)

- (BOOL)isValidEmail {
NSString *pattern = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}";
NSPredicate *pred = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", pattern];
return [pred evaluateWithObject:self];
}

@end

Category 可以做的事

  • ✅ 添加实例方法和类方法
  • ✅ 添加属性声明+关联对象实现
  • ✅ 遵循新的协议
  • 不能添加实例变量(内存布局编译时已确定)

Extension(类扩展/匿名分类)

通常写在 .m 文件中,用于声明私有属性和方法:

// Person.m
@interface Person () // 匿名分类 = Extension
@property (nonatomic, copy) NSString *privateInfo;
- (void)internalSetup;
@end

@implementation Person

- (void)internalSetup {
self.privateInfo = @"secret";
}

@end

Category vs Extension 对比

特性CategoryExtension
有类名(CategoryName)()
添加实例变量
添加属性声明可以,需关联对象实现✅ 完整实现
实现位置可以在任何文件必须在原类 .m 中
编译时机运行时加载编译时确定
可见性公开私有

Category 加载原理

Category 的方法在 Runtime 加载时被合并到类的方法列表

Category 方法"覆盖"

Category 的方法会被插入到方法列表的前面,而非真正替换原方法。objc_msgSend 遍历时先找到 Category 的实现就返回了,看起来像是"覆盖"了原方法。原方法仍然存在,可以通过遍历方法列表找到并调用。

多个 Category 同名方法

// Category_A
@implementation Person (A)
- (void)test { NSLog(@"A"); }
@end

// Category_B
@implementation Person (B)
- (void)test { NSLog(@"B"); }
@end

最终调用哪个取决于编译顺序——后编译的 Category 方法在列表最前面,会被优先找到。Xcode 中可在 Build Phases → Compile Sources 中看到顺序。

Category 添加属性

Category 不能添加实例变量,但可以通过关联对象实现属性:

#import <objc/runtime.h>

@interface UIView (Badge)
@property (nonatomic, copy) NSString *badgeText;
@end

@implementation UIView (Badge)

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

- (NSString *)badgeText {
return objc_getAssociatedObject(self, @selector(badgeText));
}

@end

+load 和 +initialize

Category 对这两个方法的影响不同:

+load+initialize
调用时机类加载时(main 之前)类第一次收到消息时
Category原类和 Category 的 +load 都会调用Category 会"覆盖"原类的 +initialize
调用方式直接通过函数指针调用通过 objc_msgSend 调用
子类不影响父类子类没实现则调父类的

常见面试问题

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

答案

因为 class_ro_t(包含 ivars 列表和 instanceSize)在编译时就确定了,运行时不可修改。Category 在运行时加载合并时只能修改 class_rw_t(方法、属性声明、协议列表),不能修改已编译的只读结构。

如果允许 Category 添加实例变量,已分配的对象内存区域不够放新变量,会导致内存越界。

Q2: Category 中 +load 方法的调用顺序?

答案

  1. 先调用所有类的 +load(按继承层级:父类 → 子类)
  2. 再调用所有 Category 的 +load(按编译顺序)

每个 +load 都会调用,不存在"覆盖"的情况。

Q3: Category 和 Extension 在实际开发中怎么选择?

答案

  • Category:为系统类/第三方库扩展功能(如 UIView+LayoutNSString+Crypto
  • Extension:在自己的类中声明私有属性和方法,保持头文件整洁

相关链接