简单来说,分类(Category)在设计的初衷是为了给现有类添加方法,而不是为了扩展实例变量。
下面我们从技术层面深入解析为什么不能直接添加变量,以及如何间接实现类似功能。
1. 从底层结构分析:根本原因
要理解为什么,我们需要看一下类的底层定义(在Objective-C运行时中):
(1)类的内存结构
一个类的实例(对象)在内存中,本质上是一个指向结构体的指针。这个结构体的第一个成员是isa指针,之后紧接着的就是类的实例变量(ivars)。
c
// 这是一个简化的模型,用于理解
struct objc_object {
Class isa;
};
struct old_class {
Class isa;
struct old_class *superclass;
const char *name;
long version;
long info;
long instance_size;
struct old_ivar_list *ivars // <-- 关键!实例变量列表
struct old_method_list **methodLists;
...
};
关键点在于:instance_size(实例大小)在类被编译后就已经确定了。 运行时系统会根据这个大小来为这个类的对象分配内存。
(2)分类(Category)的底层结构
分类是在运行时被加载的,它本身的结构并不包含ivars列表。
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;
// 注意:这里没有 ivar_list_t !
};
可以看到,分类可以添加:
-
实例方法
-
类方法
-
协议
-
属性(Property)
(3)核心矛盾
当你为一个类MyClass创建了一个分类MyCategory时:
-
MyClass在编译时,其内存布局(包括所有实例变量的偏移量)就已经固定了。 -
如果在运行时,
MyCategory试图添加一个新的实例变量,系统需要修改MyClass的ivars列表,并增大instance_size。 -
这会导致一个严重问题:所有在分类加载之前创建的
MyClass实例,它们分配的内存大小是旧的instance_size。如果此时改变了类定义,这些已存在的对象的内存布局就会错乱,导致程序崩溃。
为了保证程序的稳定性和安全性,Objective-C的设计者禁止了在运行时向已存在的类添加实例变量这一行为。
2. 属性(Property)与关联对象(Associated Objects)
虽然不能添加实例变量,但你可以在分类中声明@property。
objectivec
// MyClass+MyCategory.h
@interface MyClass (MyCategory)
@property (nonatomic, copy) NSString *categoryProperty;
@end
但此时,如果你只声明属性,编译器会发出警告:
Warning: Property 'categoryProperty' requires method 'categoryProperty' to be defined - use @dynamic or provide a method implementation in this category.
Warning: Property 'categoryProperty' requires method 'setCategoryProperty:' to be defined - use @dynamic or provide a method implementation in this category.
这是因为@property在分类中只会自动生成getter和setter方法的声明,而不会生成方法的实现,更不会生成对应的实例变量。
解决方案:使用关联对象(Associated Objects)
关联对象是Runtime API提供的一种机制,它允许你将一个键值对(Key-Value)动态地关联到一个对象上,从而模拟出"添加实例变量"的效果。
objectivec
// MyClass+MyCategory.m
#import <objc/runtime.h>
@implementation MyClass (MyCategory)
// 定义一个静态的键,通常是指针地址,确保唯一性
static const void *kCategoryPropertyKey = &kCategoryPropertyKey;
- (NSString *)categoryProperty {
// objc_getAssociatedObject 相当于 getter 方法
return objc_getAssociatedObject(self, kCategoryPropertyKey);
}
- (void)setCategoryProperty:(NSString *)categoryProperty {
// objc_setAssociatedObject 相当于 setter 方法
objc_setAssociatedObject(self,
kCategoryPropertyKey,
categoryProperty,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
objc_setAssociatedObject 的内存管理策略:
-
OBJC_ASSOCIATION_ASSIGN: 弱引用,类似于assign。 -
OBJC_ASSOCIATION_RETAIN_NONATOMIC:强引用,非原子,类似于strong, nonatomic。 -
OBJC_ASSOCIATION_COPY_NONATOMIC:拷贝,非原子,类似于copy, nonatomic。 -
OBJC_ASSOCIATION_RETAIN:强引用,原子操作。 -
OBJC_ASSOCIATION_COPY:拷贝,原子操作。
3. 总结与对比
| 特性 | 类扩展(Extension) | 分类(Category) |
|---|---|---|
| 位置 | .m文件中,@interface ClassName () |
独立的.h和.m文件,@interface ClassName (CategoryName) |
| 添加实例变量 | 可以 | 不可以 |
| 添加属性 | 自动合成getter/setter和实例变量 | 只能声明,需手动实现getter/setter(通常用关联对象) |
| 添加方法 | 可以 | 可以 |
| 加载时机 | 编译时 | 运行时 |
| 主要用途 | 隐藏私有信息、封装内部实现 | 扩展现有类的功能,模块化 |
4. Swift中的Extension
值得一提的是,Swift中的Extension比Objective-C的Category更强大,但它同样不能添加存储属性。这是因为Swift的实例内存布局同样需要在编译时确定。
在Swift中,如果你想在Extension中添加"属性",同样需要使用计算属性,而其背后如果需要存储数据,依然要依靠关联对象(虽然不推荐)或其他存储机制。
结论:
iOS分类不能添加变量的根本原因在于类的实例内存布局在编译期就已确定,而运行时无法安全地修改已存在实例的内存大小。关联对象(Associated Objects)是官方提供的、用于在分类中模拟实例变量存储功能的运行时解决方案。
**延伸扩展:**iOS中如何在关联对象中实现类似于weak的效果
在 Objective-C 的关联对象中实现类似于 weak 的效果需要一些技巧,因为关联对象本身不支持弱引用。以下是几种完整的实现方案:
1. 包装器方案(推荐)
WeakObjectWrapper.h
objc
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/**
* 弱引用包装器,用于在关联对象中实现weak效果
*/
@interface WeakObjectWrapper : NSObject
@property (nonatomic, weak, readonly) id weakTarget;
@property (nonatomic, copy, readonly, nullable) NSString *key;
- (instancetype)initWithTarget:(id)target;
- (instancetype)initWithTarget:(id)target key:(nullable NSString *)key;
/// 快速创建包装器
+ (instancetype)wrapperWithTarget:(id)target;
+ (instancetype)wrapperWithTarget:(id)target key:(nullable NSString *)key;
/// 检查目标对象是否还存在
- (BOOL)isTargetAlive;
@end
NS_ASSUME_NONNULL_END
WeakObjectWrapper.m
objc
#import "WeakObjectWrapper.h"
@implementation WeakObjectWrapper {
__weak id _weakTarget;
NSString *_key;
}
- (instancetype)initWithTarget:(id)target {
return [self initWithTarget:target key:nil];
}
- (instancetype)initWithTarget:(id)target key:(nullable NSString *)key {
self = [super init];
if (self) {
_weakTarget = target;
_key = [key copy];
// 监听目标对象的释放
[self setupDeallocObserverForTarget:target];
}
return self;
}
+ (instancetype)wrapperWithTarget:(id)target {
return [[self alloc] initWithTarget:target];
}
+ (instancetype)wrapperWithTarget:(id)target key:(nullable NSString *)key {
return [[self alloc] initWithTarget:target key:key];
}
#pragma mark - 属性访问
- (id)weakTarget {
return _weakTarget;
}
- (NSString *)key {
return _key;
}
- (BOOL)isTargetAlive {
return _weakTarget != nil;
}
#pragma mark - 监听目标释放
- (void)setupDeallocObserverForTarget:(id)target {
if (!target) return;
// 使用KVO监听target的dealloc(实际上是通过关联对象添加观察者)
static char observerKey;
DeallocObserver *observer = [[DeallocObserver alloc] initWithBlock:^{
[self targetDidDealloc];
}];
objc_setAssociatedObject(target, &observerKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)targetDidDealloc {
NSLog(@"💨 WeakObjectWrapper: 目标对象已释放, key: %@", _key ?: @"unknown");
// 可以在这里添加释放后的清理逻辑
if (_key) {
[[NSNotificationCenter defaultCenter]
postNotificationName:@"WeakObjectWrapperTargetDidDealloc"
object:nil
userInfo:@{@"key": _key}];
}
}
#pragma mark - 描述信息
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, target: %@, key: %@>",
NSStringFromClass([self class]),
self,
_weakTarget ?: @"nil",
_key ?: @"none"];
}
@end
#pragma mark - 释放观察器
@interface DeallocObserver : NSObject
@property (nonatomic, copy) void (^deallocBlock)(void);
- (instancetype)initWithBlock:(void(^)(void))block;
@end
@implementation DeallocObserver
- (instancetype)initWithBlock:(void(^)(void))block {
self = [super init];
if (self) {
_deallocBlock = [block copy];
}
return self;
}
- (void)dealloc {
if (_deallocBlock) {
_deallocBlock();
}
}
@end
2. Block 方案
WeakReferenceBlock.h
objc
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef id _Nullable (^WeakReferenceBlock)(void);
typedef void (^WeakReferenceSetterBlock)(id _Nullable target);
/**
* 弱引用Block工具类
*/
@interface WeakReferenceBlock : NSObject
/// 创建弱引用getter block
+ (WeakReferenceBlock)weakReferenceGetterForTarget:(id)target;
/// 创建弱引用setter block
+ (WeakReferenceSetterBlock)weakReferenceSetterForTarget:(__autoreleasing id *)targetPtr;
/// 安全获取弱引用对象(如果对象已释放返回nil)
+ (id)safeGetFromWeakReference:(WeakReferenceBlock)weakRef;
@end
NS_ASSUME_NONNULL_END
WeakReferenceBlock.m
objc
#import "WeakReferenceBlock.h"
#import <objc/runtime.h>
@implementation WeakReferenceBlock
+ (WeakReferenceBlock)weakReferenceGetterForTarget:(id)target {
__weak id weakTarget = target;
return ^id {
return weakTarget;
};
}
+ (WeakReferenceSetterBlock)weakReferenceSetterForTarget:(__autoreleasing id *)targetPtr {
return ^(id newTarget) {
*targetPtr = newTarget;
};
}
+ (id)safeGetFromWeakReference:(WeakReferenceBlock)weakRef {
if (!weakRef) return nil;
@try {
id target = weakRef();
return target;
} @catch (NSException *exception) {
NSLog(@"⚠️ 获取弱引用对象时发生异常: %@", exception);
return nil;
}
}
@end
3. 完整的使用示例
UIView+WeakAssociatedObject.h
objc
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIView (WeakAssociatedObject)
// 使用包装器方案的弱引用属性
@property (nonatomic, weak, nullable) id weakDelegate;
@property (nonatomic, weak, nullable) UIView *weakRelatedView;
// 使用Block方案的弱引用属性
@property (nonatomic, weak, nullable) id weakDataModel;
// 弱引用对象字典(用于多个弱引用对象)
@property (nonatomic, strong, nullable) NSDictionary<NSString *, WeakObjectWrapper *> *weakObjectsDict;
// 管理方法
- (void)setWeakObject:(id)object forKey:(NSString *)key;
- (nullable id)weakObjectForKey:(NSString *)key;
- (void)removeWeakObjectForKey:(NSString *)key;
- (BOOL)hasAliveWeakObjectForKey:(NSString *)key;
- (NSArray<NSString *> *)allAliveWeakObjectKeys;
@end
NS_ASSUME_NONNULL_END
UIView+WeakAssociatedObject.m
objc
#import "UIView+WeakAssociatedObject.h"
#import "WeakObjectWrapper.h"
#import "WeakReferenceBlock.h"
#import <objc/runtime.h>
@implementation UIView (WeakAssociatedObject)
static char kWeakDelegateKey;
static char kWeakRelatedViewKey;
static char kWeakDataModelBlockKey;
static char kWeakObjectsDictKey;
#pragma mark - 包装器方案实现
- (void)setWeakDelegate:(id)weakDelegate {
if (weakDelegate) {
WeakObjectWrapper *wrapper = [WeakObjectWrapper wrapperWithTarget:weakDelegate key:@"weakDelegate"];
objc_setAssociatedObject(self, &kWeakDelegateKey, wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} else {
objc_setAssociatedObject(self, &kWeakDelegateKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
- (id)weakDelegate {
WeakObjectWrapper *wrapper = objc_getAssociatedObject(self, &kWeakDelegateKey);
return wrapper.weakTarget;
}
- (void)setWeakRelatedView:(UIView *)weakRelatedView {
if (weakRelatedView) {
WeakObjectWrapper *wrapper = [WeakObjectWrapper wrapperWithTarget:weakRelatedView key:@"weakRelatedView"];
objc_setAssociatedObject(self, &kWeakRelatedViewKey, wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} else {
objc_setAssociatedObject(self, &kWeakRelatedViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
- (UIView *)weakRelatedView {
WeakObjectWrapper *wrapper = objc_getAssociatedObject(self, &kWeakRelatedViewKey);
id target = wrapper.weakTarget;
// 类型安全检查
if ([target isKindOfClass:[UIView class]]) {
return target;
}
return nil;
}
#pragma mark - Block方案实现
- (void)setWeakDataModel:(id)weakDataModel {
WeakReferenceBlock getterBlock = [WeakReferenceBlock weakReferenceGetterForTarget:weakDataModel];
objc_setAssociatedObject(self, &kWeakDataModelBlockKey, getterBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (id)weakDataModel {
WeakReferenceBlock block = objc_getAssociatedObject(self, &kWeakDataModelBlockKey);
return [WeakReferenceBlock safeGetFromWeakReference:block];
}
#pragma mark - 弱引用字典管理
- (void)setWeakObjectsDict:(NSDictionary<NSString *,WeakObjectWrapper *> *)weakObjectsDict {
objc_setAssociatedObject(self, &kWeakObjectsDictKey, weakObjectsDict, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSDictionary<NSString *,WeakObjectWrapper *> *)weakObjectsDict {
return objc_getAssociatedObject(self, &kWeakObjectsDictKey);
}
- (void)setWeakObject:(id)object forKey:(NSString *)key {
if (!key) return;
NSMutableDictionary *dict = [self.weakObjectsDict mutableCopy] ?: [NSMutableDictionary new];
if (object) {
WeakObjectWrapper *wrapper = [WeakObjectWrapper wrapperWithTarget:object key:key];
dict[key] = wrapper;
} else {
[dict removeObjectForKey:key];
}
self.weakObjectsDict = [dict copy];
}
- (id)weakObjectForKey:(NSString *)key {
if (!key) return nil;
WeakObjectWrapper *wrapper = self.weakObjectsDict[key];
return wrapper.weakTarget;
}
- (void)removeWeakObjectForKey:(NSString *)key {
[self setWeakObject:nil forKey:key];
}
- (BOOL)hasAliveWeakObjectForKey:(NSString *)key {
if (!key) return NO;
WeakObjectWrapper *wrapper = self.weakObjectsDict[key];
return [wrapper isTargetAlive];
}
- (NSArray<NSString *> *)allAliveWeakObjectKeys {
NSMutableArray *aliveKeys = [NSMutableArray new];
[self.weakObjectsDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, WeakObjectWrapper *wrapper, BOOL *stop) {
if ([wrapper isTargetAlive]) {
[aliveKeys addObject:key];
}
}];
return [aliveKeys copy];
}
#pragma mark - 清理方法
- (void)cleanupAllWeakReferences {
// 清理所有弱引用
self.weakDelegate = nil;
self.weakRelatedView = nil;
self.weakDataModel = nil;
self.weakObjectsDict = nil;
}
@end
4. 使用示例和测试
ViewController 测试代码
objc
#import "ViewController.h"
#import "UIView+WeakAssociatedObject.h"
@interface TestModel : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation TestModel
- (void)dealloc {
NSLog(@"🧹 TestModel dealloc: %@", self.name);
}
@end
@interface ViewController ()
@property (nonatomic, strong) UIView *testView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self testWeakAssociatedObject];
}
- (void)testWeakAssociatedObject {
// 创建测试视图
self.testView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
self.testView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.testView];
// 测试1: 包装器方案
[self testWrapperApproach];
// 测试2: Block方案
[self testBlockApproach];
// 测试3: 字典管理方案
[self testDictionaryApproach];
}
- (void)testWrapperApproach {
NSLog(@"=== 测试包装器方案 ===");
@autoreleasepool {
TestModel *model = [[TestModel alloc] init];
model.name = @"WrapperModel";
// 设置弱引用
self.testView.weakDelegate = model;
NSLog(@"设置后: %@", self.testView.weakDelegate);
}
// model 已释放,这里应该为 nil
NSLog(@"释放后: %@", self.testView.weakDelegate);
}
- (void)testBlockApproach {
NSLog(@"=== 测试Block方案 ===");
@autoreleasepool {
TestModel *model = [[TestModel alloc] init];
model.name = @"BlockModel";
// 设置弱引用
self.testView.weakDataModel = model;
NSLog(@"设置后: %@", self.testView.weakDataModel);
}
// model 已释放,这里应该为 nil
NSLog(@"释放后: %@", self.testView.weakDataModel);
}
- (void)testDictionaryApproach {
NSLog(@"=== 测试字典管理方案 ===");
@autoreleasepool {
TestModel *model1 = [[TestModel alloc] init];
model1.name = @"DictModel1";
TestModel *model2 = [[TestModel alloc] init];
model2.name = @"DictModel2";
// 设置多个弱引用
[self.testView setWeakObject:model1 forKey:@"model1"];
[self.testView setWeakObject:model2 forKey:@"model2"];
NSLog(@"设置后 - model1: %@", [self.testView weakObjectForKey:@"model1"]);
NSLog(@"设置后 - model2: %@", [self.testView weakObjectForKey:@"model2"]);
NSLog(@"存活key: %@", [self.testView allAliveWeakObjectKeys]);
}
// models 已释放,这里应该为 nil
NSLog(@"释放后 - model1: %@", [self.testView weakObjectForKey:@"model1"]);
NSLog(@"释放后 - model2: %@", [self.testView weakObjectForKey:@"model2"]);
NSLog(@"存活key: %@", [self.testView allAliveWeakObjectKeys]);
}
@end
5. 方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 包装器方案 | 功能完整,支持释放回调,类型安全 | 代码量稍大,需要创建包装对象 | 需要完整弱引用功能的场景 |
| Block方案 | 简洁,无需额外类 | 功能相对简单,不支持释放回调 | 简单的弱引用需求 |
| 字典方案 | 支持多个弱引用对象管理 | 使用稍复杂,性能开销稍大 | 需要管理多个弱引用对象的场景 |
6. 注意事项
-
线程安全:这些实现不是线程安全的,如果需要在多线程环境下使用,需要添加适当的同步机制
-
性能考虑:对于性能敏感的场景,建议使用 Block 方案
-
内存管理:确保不会因为包装器本身导致内存泄漏
-
循环引用:虽然解决了目标对象的循环引用,但要注意包装器本身的引用关系
推荐使用包装器方案,因为它功能最完整,且提供了目标对象释放的回调机制,在实际项目中更加实用。