iOS-浅谈OC中的关联对象

2019-05-30  本文已影响0人  晴天ccc

目录

  • 前言
  • 工作中遇到问题的思考
    ---- 直接在分类添加成员变量
    ---- 在分类中增加属性
  • 如何在分类中添加成员变量?
    ----【方法一】利用字典给分类添加成员变量
    ----【方法二】利用关联对象给分类添加成员变量
  • 关联对象简介
  • 关联对象作用
  • 基本用法
  • 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创建一个关联的观察者。

基本用法

objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy);
objc_getAssociatedObject(id object, const void * key);
objc_removeAssociatedObjects(id _Nonnull object);
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的用法

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个字节,且书写麻烦。

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个字节
缺点:书写麻烦

#define NameKey @"name"

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, NameKey, name, OBJC_ASSOCIATION_COPY);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, NameKey);
}

缺点:书写麻烦

- (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、如果被关联对象销毁,对应的关联属性也都会销毁。
上一篇 下一篇

猜你喜欢

热点阅读