runtime(二) 给对象、分类添加实例变量

Posted by Abin's blog on April 12, 2016

在开发中有时候想给对象实例添加个变量来存储数据,但又无法直接声明,比如说既有类的分类。这个时候我们就可以通过 关联对象 在运行时给对象关联一个 对象 来存储数据。(注意:并不是真实的添加了一个实例变量)

关联对象 可以给某个对象关联其他对象并用key来区分其他对象。需要注意的是,存储对象的时候要指明 存储策略,用来维护对象的内存管理语义。存储策略是 objc_AssociationPolicy 枚举定义,以下是存储策略对应的 @property属性:

存储策略类型 对应的@property属性
OBJC_ASSOCIATION_ASSIGN weak
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong
OBJC_ASSOCIATION_COPY copy

用下面的方法可以管理关联对象:

// 这个方法可以根据指定策略给对象关联对象值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

// 这个方法可以获取对象关联对象值
id objc_getAssociatedObject(id object, const void *key)

// 这个方法可以删除指定对象的全部关联对象值
void objc_removeAssociatedObjects(id object)

对于关联对象这个OC特性,我们可以把对象想象成一个 NSDictionary,关联对象需要一个 key( 类型是 opaque pointer,无类型的指针 ) 来区分,我们可以把要添加的变量名作为 key ,把变量的值作为关联的对象来存储到 ”对象“ 这个 NSDictionary 中。 所以,关联对象的

void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

方法类似于字典的 [dict setObject: forKey:] 方法。

在存储和获取关联对象时需要用一个相等的 key ,因为是给 Class 的实例对象关联对象,所以一般用静态变量来做 key

说的再多,不如上段代码!

比如说,我们给 NSString 实例加上个 NSDate 类型的 date 变量。什么?给字符串加个日期变量是要干袅?我要给字符串过个生日不行吗! 别闹,举个栗子嘛!(捂脸逃跑~~~)

首先,我们先给 NSString 新建个名为 RT 的 category。 在头文件中有个 NSDate 类型的 date 属性:

//  NSString+RT.h
//  runtime
#import <Foundation/Foundation.h>
//
@interface NSString (RT)
//
@property (nonatomic, strong) NSDate *date;
//
@end

在分类中的属性只会生成 getset 方法,并不会生成变量。 所以我们需要重写 getset 方法,关联对象以变相实现添加变量,在现实文件中:

//  NSString+RT.m
//  runtime
#import <objc/runtime.h>
#import "NSString+RT.h"
//
@implementation NSString (RT)
//
static void *runtime_date_key = "date";
- (NSDate *)date{
    return objc_getAssociatedObject(self, runtime_date_key);
}
//
- (void)setDate:(NSDate *)date{
    objc_setAssociatedObject(self, runtime_date_key, date, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

需要注意的是,关联对象用到的 key 是个无类型的指针,一般来说是静态来修饰。 另外,给对象关联的只能是对象,如果是 intfloat 等类型需要 NSNumber 进行包装。 因为 date 是强引用和非原子属性,所以关联策略用 OBJC_ASSOCIATION_RETAIN_NONATOMIC

然后执行代码:

NSString *string = @"runtimeTestString";
string.date = [NSDate date];
NSLog(@"string.date = %@",string.date);

输出结果:

2016-04-12 21:27:31.099 runtime[2837:103727] string.date = 2016-04-12 13:27:31 +0000

注意:

  • 定义关联对象时需要指定内存管理语义,用来模拟对象对变量的拥有关系
  • 尽量避免使用关联对象,因为如果出现bug不易于问题排查