目录
- 前言
- 工作中遇到问题的思考
---- 直接在分类添加成员变量
---- 在分类中增加属性- 如何在分类中添加成员变量?
----【方法一】利用字典给分类添加成员变量
----【方法二】利用关联对象给分类添加成员变量- 关联对象简介
- 关联对象作用
- 基本用法
- key的用法
- 关联对象的实现原理
- 总结
前言
在ARC环境下,在一个类中声明一个属性@property (nonatomic, assign) int age;那系统类似的帮我们生成如下代码:
@interface Person : NSObject
{
int _age;
}
- (void)setAge:(int)age;
- (int)age;
@end
@implementation Person
- (void)setAge:(int)age{
_age = age;
}
- (int)age{
return _age;
}
@end
下划线的成员变量,setter, getter方法的声明和实现。
工作中遇到问题的思考
在平时的工作中经常碰到给类别添加属性的操作,那么实现思路是怎么样的呢?
-
直接在分类添加成员变量
@interface Person (walk) {
int _weight;
}
系统报错:!Instance variables may not be placed in categories(实例变量不能放在类别中)
说明分类中不能直接添加成员变量!!!
-
在分类中增加属性
@interface Person (walk)
@property (nonatomic, assign) int weight;
@end
当调用set或者get方法时,控制台会报错:
-[Person setWeight:]: unrecognized selector sent to instance 0x1007160e0
-[Person weight]: unrecognized selector sent to instance 0x105011920
说明分类可以添加属性,但是添加属性后,只会自动添加set和get方法声明,不会实现set和get方法。
本质原因:通过底层我们可以看到分类的结构体类型_category_t,里面并没有存储成员变量。
struct _category_t {
const char *name; // 类名
struct _class_t *cls;
const struct _method_list_t *instance_methods; //对象方法列表
const struct _method_list_t *class_methods; //类方法列表
const struct _protocol_list_t *protocols; //协议列表
const struct _prop_list_t *properties; //属性列表
};
如何在分类中添加成员变量?
-
【方法一】利用字典给分类添加成员变量
// 声明一个全局字典,用于保存属性
NSMutableDictionary *dic_;
+ (void)load {
dic_ = [[NSMutableDictionary alloc] init];
}
- (void)setWeight:(int)weight {
NSString *key = [NSString stringWithFormat:@"%p",self];
dic_[key] = @(weight);
}
- (int)weight {
NSString *key = [NSString stringWithFormat:@"%p",self];
return [dic_[key] intValue];
}
通过分类中的字典可以保存分类里添加属性的值,但是有以下几个缺点
- 内存泄漏,字典是全局变量,会一直保存在内存中。
- 线程安全,不同的对象在不同的线程同时访问分类中的成员变量,可能会导致读写错误。
- 每次给这个添加一个新的属性时需要重新创建一个新的字典保存。
- 代码量比较大,实现起来比较麻烦。
-
【方法二】利用关联对象给分类添加成员变量
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name{
// 隐式参数
// _cmd == @selector(name)
return objc_getAssociatedObject(self, _cmd);
}
关联对象简介
C是一种动态语言,它的方法,对象的类型都是到运行的时候才能够确定的。所以这就使得OC存在了关联对象这一强大的机制。
所谓关联对象,其实就是我们在运行时对一个已存在的对象上面绑定一个对象,使两个对象变成动态的聚合关系。
关联对象作用
关联对象一般用于动态的扩展一个对象,但是这一般都是在其他方法不行的事后才会使用,因为关联对象很可能会出现难以查找的Bug。
- 为现有的类添加属性,变量。
在Objective-C中可以通过Category给一个现有的类添加属性(如NSObject),但是却不能添加实例变量,然而可以通过Associated Object间接地达到这一目的。- 为KVO创建一个关联的观察者。
基本用法
- 添加关联对象,传nil可以移除相关的关联对象。
objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy);
- 获取关联对象
objc_getAssociatedObject(id object, const void * key);
- 移除所有关联对象,如果打算只移除一部分则不能使用该方法。
objc_removeAssociatedObjects(id _Nonnull object);
- 使用关联对象时会用到
objc_AssociationPolicy。 - 它其实和我们OC中的属性修饰符一一对应,使用什么修饰符,取决于你定义的属性的类型,对应关系如下:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
代码举例:
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @"key", name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, "key");
}
我们看到这里都需要一个关键值:
key,下面就对这个key进行探究。
key的用法
-
使用变量地址作为key
static const void *NameKey = &NameKey;
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, NameKey);
}
缺点:声明一个全局指针变量,占用8个字节,且书写麻烦。
-
使用char类型变量地址作为key
static const char NameKey;
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, &NameKey, name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, &NameKey);
}
优点:char类型只占用1个字节
缺点:书写麻烦
-
使用字符串作为key
#define NameKey @"name"
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, NameKey);
}
缺点:书写麻烦
-
使用方法地址作为key
(推荐)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
- (NSString *)name {
return objc_getAssociatedObject(self, _cmd);
}
优点:可读性较高、如果方法名写错系统会有提示,书写简单。
其中_cmd == @selector(name),OC方法有两个隐式参数,一个是self,一个是_cmd.
比如name函数可能是:
- (NSString *)nameSelf:(id)self cmd:(SEL):_cmd {
}
关联对象的实现原理
查看 【objc源码】
objc-references.mm中的_object_set_associative_reference方法和_object_get_associative_reference方法:
_object_get_associative_reference(id object, const void *key)
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy){}
可以看出关联对象过程中主要用到一下几个数据结构:
AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
class AssociationsManager {
......
AssociationsHashMap &get() {
return _mapStorage.get();
}
......
};
class ObjcAssociation {
uintptr_t _policy;
id _value;
......
}
它们的结构关系如下:
运行时通过map维系一张关联对象与被关联对象之间的关系。
- 所有实例对象的关联对象都由一个
AssociationsManager管理。- 其内部有一个
AssociationsHashMap对象,其内部保存着实例对象(object)和存储着实例对象对应自己关联对象的ObjectAssociationMap。- ObjectAssociationMap内部存储着关联对象的key和对应关联对象的value和策略。
总结
- 1、关联对象并不是存储在被关联对象本身。
- 2、关联对象存储在全局统一的一个AssociationsManager中。
- 3、移除关联对象将关联对象设置nil即可(通过查看源码可知,当value==nil时会调用erase方法)。
- 4、移除所有关联对象,调用objc_removeAssociatedObjects(id _Nonnull object)。
- 5、如果被关联对象销毁,对应的关联属性也都会销毁。












网友评论