Effective Objective-C 2.0 读书笔记------关联对象
文章目录
- [Effective Objective-C 2.0 读书笔记------关联对象](#Effective Objective-C 2.0 读书笔记——关联对象)
前言
暑假的任务是学习Effective Objective-C 2.0书上的相关内容,今天终于开始着手写读书笔记,希望能够在寒假多发两篇吧
如何给分类添加实例变量?
前面我们学习分类(category)的时候,说到分类其实是不能添加的实例变量的,这一点其实与OC之中特有的动态加载有关。
分类方法存储:在编译阶段,分类的方法并不会直接合并到类的原始方法列表中。分类的方法被存储在一个单独的结构中,与类本身分离。
运行时合并:
- 当程序启动并加载分类时,Objective-C 运行时会将分类的方法动态添加到目标类的方法列表中。
- 这个过程会覆盖同名方法(如果分类中的方法名和原类相同,则分类方法会覆盖原方法)。
分类的本质:它是运行时对类的方法列表、协议列表和属性列表的扩展,而不是修改类的内存布局。
在分类的动态加载过程中,Objective-C 的核心机制是 方法调度。方法列表本质上是一个链表,存取着这些方法的指针,分类动态添加方法时,运行时将这些方法插入链表的头部。这里我重新学习了一下分类和OC方法调度的相关内容,举一个例子供参考:
示例:动态方法列表
objc
@interface MyClass : NSObject
- (void)originalMethod;
@end
@implementation MyClass
- (void)originalMethod {
NSLog(@"Original Method");
}
@end
@interface MyClass (Category)
- (void)originalMethod; // 分类中的同名方法
- (void)newMethod; // 分类添加的新方法
@end
@implementation MyClass (Category)
- (void)originalMethod {
NSLog(@"Category Method");
}
- (void)newMethod {
NSLog(@"New Method");
}
@end
运行时加载行为:
objc
MyClass *obj = [MyClass new];
[obj originalMethod]; // 输出: Category Method (分类覆盖原方法)
[obj newMethod]; // 输出: New Method (分类添加的新方法)
方法分发示意图
- 原类方法列表:
originalMethod -> NULL
- 分类方法加载后:
newMethod -> originalMethod (from category) -> originalMethod (from original class)
我们在调用当前对象的方法时,其实就是直接调用这个链表之中的头节点。
分类只能在运行时添加方法或扩展功能,因为它无法在编译阶段改变类的内存布局。类的实例变量布局是固定的,分类无法参与这个布局,因此无法直接添加实例变量。
关联对象
那么接下来就引出今天要讲的关联对象的内容。如果我们想要给分类添加相关的实例变量,那么我们就会用上关联的对象这个方法。**关联对象(Associated Objects)**是一个运行时动态绑定机制,可以为对象存储额外的数据。
关联对象的核心 API 来自 <objc/runtime.h>
,包括:
objc_setAssociatedObject
:设置关联对象。objc_getAssociatedObject
:获取关联对象。objc_removeAssociatedObjects
:移除所有关联对象。
运行原理
Objective-C 运行时维护一个全局的"关联对象表",类似于一个哈希表。
- Key:目标对象的内存地址。
- Value :另一个字典,存储了每个关联对象的
key-value
。
内存管理策略
在介绍这三个方法之前,我们先来看看内存管理策略的关键词
关联类型 | 作用 |
---|---|
OBJC_ASSOCIATION_ASSIGN : |
弱引用,不保留对象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC : |
强引用,非原子性 |
OBJC_ASSOCIATION_COPY_NONATOMIC : |
拷贝对象,非原子性 |
OBJC_ASSOCIATION_RETAIN : |
强引用,线程安全。 |
OBJC_ASSOCIATION_COPY : |
拷贝对象,线程安全。 |
其实我们不难看到,这些属性,和@property的属性功能相同。
objc_setAssociatedObject
这个函数用于设置关联对象
objc
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
参数说明
object
- 要为其设置关联对象的目标对象(任何 Objective-C 对象)。
- 这个对象是关联对象的持有者。
key
- 唯一标识关联对象的键。通常是一个静态变量的地址(如
static char kKey;
)。 - 它可以区分同一个对象中不同的关联对象。
value
- 要关联的对象。如果设置为
nil
,则相当于移除该关联对象。
policy
- 定义了关联对象的内存管理策略。
objc_getAssociatedObject
这个函数用于获取关联对象
objc
id objc_getAssociatedObject(id object, const void *key);
参数说明
object
- 目标对象,用于存储或检索关联对象。它是你要为其获取关联对象的对象实例。
key
- 用于标识关联对象的键。这个键必须是唯一的,通常是静态变量的地址或者唯一的选择器等。
objc_removeAssociatedObjects
这个函数用于移除所有的关联对象,该函数会 删除对象上的所有关联对象,从而释放它们占用的内存。它不会移除对象本身,只有与对象相关的关联对象会被清除。
函数原型
objc
void objc_removeAssociatedObjects(id object);
参数说明
object
目标对象,表示要从中移除所有关联对象的对象实例。
补充:我们使用的Key一般都是定义静态变量,因为静态变量的地址在整个程序运行期间是固定且唯一的。因此,通过
&kNameKey
就能在全局范围内唯一标识一个关联对象。静态变量的值不重要,重要的是它的 地址唯一性。
通过
&kNameKey
,我们得到一个唯一标识符,供objc_setAssociatedObject
存储和检索关联对象。推荐使用
static char
,因为它简单、高效且清晰。另外:除了使用静态变量,我们其实也可以用方法指针或者使用动态分配内存进行操作
方法指针:
objcobjc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
动态分配内存:
objcstatic void *kNameKey = NULL; kNameKey = malloc(1); // 分配一个唯一的内存地址
关联对象的例子
书中的例子一
书中使用了警告视图的为例子,给控制器起关联一个block,方便我们进行直观的看到代码的运行逻辑
objc
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
// 创建一个block,保存用户点击按钮时的处理逻辑
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
};
// 使用关联对象将block与alert对象关联
objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
// 显示alert
[alert show];
}
// UIAlertViewDelegate协议方法
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
// 获取与alertView关联的block
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);
// 执行block
if (block) {
block(buttonIndex);
}
}
// 取消按钮的处理逻辑
- (void)doCancel {
NSLog(@"User canceled the action.");
}
// 继续按钮的处理逻辑
- (void)doContinue {
NSLog(@"User continued the action.");
}
这个操作确实可以视为将一个 block
以 property
的形式与对象(在这个例子中是 UIAlertView
)关联起来。通过使用 objc_setAssociatedObject
和 objc_getAssociatedObject
,你将 block
动态地绑定到 UIAlertView
对象上,从而为该按钮添加一个与之相关联的回调。
在分类之中添加关联对象
objc
#import <objc/runtime.h>
// 在分类中为UIView添加属性
@interface UIView (NameProperty)
@property (nonatomic, strong) NSString *name;
@end
@implementation UIView (NameProperty)
static char kNameKey;
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, &kNameKey, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &kNameKey);
}
@end
// 使用
UIView *view = [[UIView alloc] init];
view.name = @"My Custom View"; // 设置关联对象
NSLog(@"View name: %@", view.name); // 获取关联对象,输出: My Custom View
总结
其实关联对象一定程度上是等同于在子类之中添加属性的,只是相对来说,使用关联对象会比写一个子类并且调用其中的方法更加节约资源一点,本质上其实就给我们提供了一个动态修改的空间,更加的灵活。