iOS底层原理 - 关联对象使用以及源码剖析

字数 1184阅读 112

开篇还是放上几道面试题

Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果

  • 首先大家应该都知道分类可以添加属性,但是不可以添加成员变量,同时自动声明了set方法,get方法,此时获取方法列表是不存在set,fet方法的,必须实现才会在方法列表中出现,大家可能会想那我就手动实现set方法,如下图

分类添加了个属性 @property (nonatomic,copy)NSString *categoryName1;

屏幕快照 2018-11-21 下午2.29.11.png
  • 发现分类中根本没有这个成员变量,其实如果看了分类的底层结构category_t应该就知道那里压根就没有存储成员变量的地方,所以想通过正常的方式直接添加成员变量是不可以的,但是可以通过关联对象,来间接添加成员变量

关联对象提供了以下API

添加关联对象
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)

关联对象基本用法

1.以全局变量里面存放着自己的地址为key
static void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
2.以全局变量的地址为key
static char MyKey;
pobjc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
pobjc_getAssociatedObject(obj, &MyKey)
3.使用属性名作为key
objc_setAssociatedObject(obj, @"name", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
pobjc_getAssociatedObject(obj, @"name");
4.使用get方法的@selecor作为key
objc_setAssociatedObject(obj, @selector(setName), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
//get方法里面,下面这两种写法都可以
objc_getAssociatedObject(obj, @selector(name))
objc_getAssociatedObject(obj, _cmd)

objc_AssociationPolicy policy的用法

屏幕快照 2018-11-21 下午2.50.26.png

开始分析源码 源码地址

  • 搜索objc_setAssociatedObject,找到如下源码
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
  • 看到 AssociationsManagerAssociationsHashMap,ObjectAssociationMap,ObjcAssociation这四个类,接下来我们一一在仔细查看对应的类结构
  • AssociationsManager结构如下
class  AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};
  • 由上面代码看到此类只有一个成员变量,也就是说这个manager管理着这个map,所有的信息都存在这个map里

static AssociationsHashMap *_map;

  • 接下来查看这个AssociationsHashMap
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };
  • 由上面代码可以看出这个map的key和value类型结构为:

disguised_ptr_t ObjectAssociationMap
disguised_ptr_t ObjectAssociationMap
disguised_ptr_t ObjectAssociationMap

  • 那么在接着看ObjectAssociationMap
  class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };
  • 可以看到key value的结构如下:

void * ObjcAssociation
void * ObjcAssociation
void * ObjcAssociation

  • 接着再看ObjcAssociation的结构
class ObjcAssociation {
        uintptr_t _policy;
        id _value;
    public:
        ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
        ObjcAssociation() : _policy(0), _value(nil) {}

        uintptr_t policy() const { return _policy; }
        id value() const { return _value; }
        
        bool hasValue() { return _value != nil; }
    };
  • 从上面代码可以看到ObjcAssociation类里有两个成员变量,一个是_policy策略,一个是_value值
  • 再具体点分析如下图所示:


    关联对象分析图.jpg
  • 最终得到结论:


    屏幕快照 2018-11-21 下午6.56.32.png
屏幕快照 2018-11-29 下午8.49.45.png

好了看完了底层结构,在讲讲项目中怎么使用

  • 大家可能注意到,policy没有weak'类型,那得怎么实现weak关联对象呢?

我们可以关联个retain对象,让这个对象持有他
恰好系统有提供这个类NSMapTable,这个map可以设置weak引用里面的值,和我们常用的字典类似

@interface NSObject()
@property (nonatomic,strong) NSMapTable *keyMapTable;
@end
@implementation NSObject (Association)
- (void)setWeakAssociatedObject:(id)object forKey:(NSString *)key
{
    [self.keyMapTable setObject:object forKey:key];
}
-(id)weakAssociatedObjectForKey:(NSString *)key
{
   return  [self.keyMapTable objectForKey:key];
}

#pragma mark - 私有方法
-(NSMapTable *)keyMapTable
{
    if (objc_getAssociatedObject(self, _cmd)==nil) {
        self.keyMapTable=[NSMapTable weakToWeakObjectsMapTable];
    }
    return objc_getAssociatedObject(self, _cmd);
}
-(void)setKeyMapTable:(NSMapTable *)keyMapTable
{
     objc_setAssociatedObject(self, @selector(keyMapTable), keyMapTable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
  • 再来看一个问题,我们大家可能在添加retain关联对象的时候,经常用getter方法作为key,但是用起来每增加个成员变量都得写一遍很麻烦,所以大家可能就会想到使用属性名作为key,这样写一遍公共的方法就好了,但是我告诉你这样是存在潜在问题的,那么就来看看下面代码有啥问题:
- (void)setAssociatedObject:(id)object forKey:(NSString *)key {
    objc_setAssociatedObject(self, &key, object, OBJC_ASSOCIATION_RETAIN);
}
- (id)associatedObjectForKey:(NSString *)key {
    return objc_getAssociatedObject(self, &key);
}

从上面的代码可以看出就是使用传进来的字符串为地址,每次传进来的字符串一样,大家可能就以为这个地址就一样了,其实不然,当字符串相同地址不一样时,set完在取值就取不到了,值为nil

屏幕快照 2018-11-22 上午9.53.07.png
  • 上图这种情况,虽然字符串的值相同,但是地址值不同,所以导致取出的值为nil,常见场景就是set的时候用的是常量字符串,取值的时候用的是后台返回的数据里的值导致不一样,如果你取值时还是用常量字符串那么就没问题,因为常量字符串地址一样
  • 解决方式如下
@interface NSObject()
@property (nonatomic,strong) NSMutableDictionary *strongKeyBuffer;
@end
@implementation NSObject (Association)
-(NSMutableDictionary *)strongKeyBuffer{
    if (objc_getAssociatedObject(self, _cmd)==nil) {
        self.strongKeyBuffer=[NSMutableDictionary dictionary];
    }
    return objc_getAssociatedObject(self, _cmd);
}
-(void)setStrongKeyBuffer:(NSMutableDictionary *)strongKeyBuffer{
    objc_setAssociatedObject(self, @selector(strongKeyBuffer), strongKeyBuffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/**
 * set 方法,以供以后添加属性时候给这个属性的 set 方法调用
 * @param   object      要关联的对象,也就是要设置的新的属性值
 * @param   key         属性名称,传入新增属性的名称
    解决常亮字符串地址不一致问题 导致取值为nil
 **/
- (void)setAssociatedObject:(id)object forKey:(NSString *)key {
    const char *cKey = [self.strongKeyBuffer[key] pointerValue]; // 先获取key
    if (cKey == NULL) { // 字典中不存在就创建
        cKey = key.UTF8String;
        self.strongKeyBuffer[key] = [NSValue valueWithPointer:cKey];
    }
    objc_setAssociatedObject(self, cKey, object, OBJC_ASSOCIATION_RETAIN);
}
#pragma mark -  get 方法,以供以后添加属性时候给这个属性的 get 方法调用
- (id)associatedObjectForKey:(NSString *)key {
    const char *cKey = [self.strongKeyBuffer[key] pointerValue];
    if (cKey == NULL) {
        return nil;
    } else {
        return objc_getAssociatedObject(self, cKey);
    }
}

用一个可变字典存放添加过哪些key,和真正set时用的地址是啥,这里这么用
[NSValue valueWithPointer:key.UTF8String];
当然你也可以不这么用,主要思想是用个字典存储添加过的key,然后取值的时候通过传进来的key,找到set时用的地址值,然后再用此地址值取值,所以就不会为nil了

下面附上我封装的类.h .m文件

  • NSObject+Association.h
//
//  NSObject+Association.h
//  NSObject+Association
//
//  Created by liusong on 2018/4/3.
//  Copyright © 2018年 liusong. All rights reserved.
//
#import <Foundation/Foundation.h>

@interface NSObject (Association)

/** 所有要增加的属性的 set 方法都可以调用这个方法来实现 */
- (void)setAssociatedObject:(id)object forKey:(NSString *)key;

/** 所有要增加的属性的 get 方法都可以调用这个方法来实现 */
- (id)associatedObjectForKey:(NSString *)key ;


//以下为增加获取 weak 属性
- (void)setWeakAssociatedObject:(id)object forKey:(NSString *)key;
- (id)weakAssociatedObjectForKey:(NSString *)key ;
@end
  • NSObject+Association.m
//
//  NSObject+Association.m
//  NSObject+Association
//
//  Created by liusong on 2018/4/3.
//  Copyright © 2018年 liusong. All rights reserved.
//

#import "NSObject+Association.h"
#import <objc/runtime.h>
@interface NSObject()
@property (nonatomic,strong) NSMutableDictionary *strongKeyBuffer;
@property (nonatomic,strong) NSMapTable *keyMapTable;
@end

@implementation NSObject (Association)

/**
 * set 方法,以供以后添加属性时候给这个属性的 set 方法调用
 * @param   object      要关联的对象,也就是要设置的新的属性值
 * @param   key         属性名称,传入新增属性的名称
    解决同一字符串地址不一致问题 导致取值为nil
 **/
- (void)setAssociatedObject:(id)object forKey:(NSString *)key {
    const char *cKey = [self.strongKeyBuffer[key] pointerValue]; // 先获取key
    if (cKey == NULL) { // 字典中不存在就创建
        cKey = key.UTF8String;
        self.strongKeyBuffer[key] = [NSValue valueWithPointer:cKey];
    }
    objc_setAssociatedObject(self, cKey, object, OBJC_ASSOCIATION_RETAIN);
}
#pragma mark -  get 方法,以供以后添加属性时候给这个属性的 get 方法调用
- (id)associatedObjectForKey:(NSString *)key {
    const char *cKey = [self.strongKeyBuffer[key] pointerValue];
    if (cKey == NULL) {
        return nil;
    } else {
        return objc_getAssociatedObject(self, cKey);
    }
}
- (void)setWeakAssociatedObject:(id)object forKey:(NSString *)key
{
    [self.keyMapTable setObject:object forKey:key];
}

-(id)weakAssociatedObjectForKey:(NSString *)key
{
   return  [self.keyMapTable objectForKey:key];
}

#pragma mark - 私有方法
-(NSMapTable *)keyMapTable
{
    if (objc_getAssociatedObject(self, _cmd)==nil) {
        self.keyMapTable=[NSMapTable weakToWeakObjectsMapTable];
    }
    return objc_getAssociatedObject(self, _cmd);
}
-(void)setKeyMapTable:(NSMapTable *)keyMapTable
{
     objc_setAssociatedObject(self, @selector(keyMapTable), keyMapTable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

推荐阅读更多精彩内容