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 对比
| 特性 | Category | Extension |
|---|---|---|
| 有类名 | 有 (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 方法的调用顺序?
答案:
- 先调用所有类的
+load(按继承层级:父类 → 子类) - 再调用所有 Category 的
+load(按编译顺序)
每个 +load 都会调用,不存在"覆盖"的情况。
Q3: Category 和 Extension 在实际开发中怎么选择?
答案:
- Category:为系统类/第三方库扩展功能(如
UIView+Layout、NSString+Crypto) - Extension:在自己的类中声明私有属性和方法,保持头文件整洁