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

开篇还是放上几道面试题

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,715评论 0 9
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,385评论 8 265
  • 面试题 Category能否添加成员变量?如果可以,如何给Category添加成员变量?答:不能直接添加成员变量,...
    xx_cc阅读 8,069评论 20 45
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,101评论 1 32
  • 这周六,《奇葩说》提出了一个有趣的话题-生活的暴击值得感激吗。从导师和选手的选择来看,大部分都选择反方-不应该感激...
    米六66阅读 646评论 0 0