iOS开发:关联对象、weak指针的底层实现原理

一、关联对象的实现原理

在分类中使用@property声明属性,只是将该属性添加到类属性列表,并声明了settergetter方法,但是并没有生成对应的成员变量,也没有实现settergetter方法。由于没有实现实现settergetter方法,所以访问的时候会crash,提示... unrecognized selector sent to instance ...

如果在分类中使用@property声明属性后,手动实现settergetter方法,那么就可以正常设置和读取这个属性了,实现的方式可以参考如下:

@interface NXTeacher(Subperson)
@property (nonatomic, copy) NSString *address;
@end

static const char *kAddressKey = "AddressKey";

@implementation NXTeacher(Subperson)
- (void)setAddress:(NSString *)address{
    objc_setAssociatedObject(self, kAddressKey, address, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)address {
    return objc_getAssociatedObject(self, kAddressKey);
}
@end

这里用到了objc_setAssociatedObject(...)objc_getAssociatedObject(...)方法,该方法内部分别调用了_object_set_associative_reference(...)_object_get_associative_reference(...)

先看一下objc_setAssociatedObject(...)方法,第一个参数id object,传我们的对象实例self;第二个参数const void *key,需要我们定义一个跟属性相关的const *类型的变量;第三个参数id value, 传设置的新值;第四个参数objc_AssociationPolicy policy,表示关联策略,他有5个枚举值:

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  
};
  • OBJC_ASSOCIATION_ASSIGN等价于@property (assign) ,一般用于基础类型(如intCGFloat等)或weak类型(如delegate类型,或MRC下的block类型)。
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC等价于@property (nonatomic, strong) (或MRC下@property (nonatomic, retain))。
  • OBJC_ASSOCIATION_COPY_NONATOMIC等价于@property (nonatomic, copy)
  • OBJC_ASSOCIATION_RETAIN等价于@property (strong)(或MRC下@property (retain))。
  • OBJC_ASSOCIATION_COPY等价于@property (copy)
void _object_set_associative_reference(id object, const void *key, id value, uintptr_t policy){
     ...
    //disguised相当于是object的一个唯一key(内部对指针地址按位取反)
    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};
    //acquireValue方法内部会根据关联策略是value对象进行objc_retain(value)或[value copy]操作。
    association.acquireValue();
    bool isFirstAssociation = false;
    {
        //AssociationsManager是一个关联对象管理类,内部_mapStorage是个静态变量。
        //manager在这里是利用构造函数、析构函数特性来自动加锁、解锁,manager.get()从_mapStorage中读出所有的关联表associations。
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            //如果新的关联值不为空
            //根据disguised查找一个对应的ObjectAssociationMap,没有的话会创建一个并插入到associations中。
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                isFirstAssociation = true;
            }

            auto &refs = refs_result.first->second;
            //根据key查找对应的bucket(key和association的封装),替换掉原有的或插入新的association,并设置关联策略
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } 
        else {
            //如果新的关联值为空,查找该对象的对应的ObjectAssociationMap,返回查找的迭代器。
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);//通过key去refs中查找该属性的ObjectAssociation,返回查找的迭代器
                if (it != refs.end()) {
                    //如果存在,进行擦除操作
                    association.swap(it->second);
                    refs.erase(it);
                    
                    //如果refs也没有该对象的其他关联属性的话,直接从associations中移除该对象对应的refs_it。
                    if (refs.size() == 0) {
                        associations.erase(refs_it);
                    }
                }
            }
        }
    }

    //在isa中标记该对象已经有关联对象了
    if (isFirstAssociation) object->setHasAssociatedObjects();
    //上面对value进行了retain操作的,这里进行一次release
    association.releaseHeldValue();
}

知道了这个值怎么设置之后,那么读取这个值的思路就很简单了:

id _object_get_associative_reference(id object, const void *key){
    ObjcAssociation association{};
    {
        //全部的关联对象表associations
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            //refs当前对象的,找到后根据key取当前属性的(通过迭代器遍历获取)
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                //赋值给association
                association = j->second;
                //如果关联策略包含retain, 这里就会retain一下。
                association.retainReturnedValue();
            }
        }
    }
    //处理autorelease
    return association.autoreleaseReturnedValue();
}

除此之外还有一个跟关联对象密切相关的函数是objc_removeAssociatedObjects(...),如果对象有关联对象则内部会调用_object_remove_assocations(...)

void _object_remove_assocations(id object, bool deallocating){
    ObjectAssociationMap refs{};
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            //读取到当前对象所有的关联对象
            refs.swap(i->second);

            bool didReInsert = false;
            if (!deallocating) {
                for (auto &ref: refs) {
                    if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
                        i->second.insert(ref);
                        didReInsert = true;
                    }
                }
            }
            if (!didReInsert)
                associations.erase(i);
        }
    }

    SmallVector<ObjcAssociation *, 4> laterRefs;

    for (auto &i: refs) {
        if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
            if (deallocating)
                laterRefs.append(&i.second);
        } 
        else {
            //对应用的对象发送release消息
            i.second.releaseHeldValue();
        }
    }
    for (auto *later: laterRefs) {
        later->releaseHeldValue();
    }
}

上面所使用的关键概念作一下解释:

  • AssociationsManager是关联对象的管理者,内部维护了一个静态的_mapStorage,存储了所有对象的关联对象AssociationsHashMap。
  • 从AssociationsHashMap通过object取出当前对象的关联对象表ObjectAssociationMap。
  • 从ObjectAssociationMap通过setter和getter指定的key获取该属性的关联信息ObjcAssociation。
    相关类的关系如图所示:


    关联对象类关系如图.png

二、weak指针的实现原理

weak指针主要用于打破循环引用,应用场景较为广泛。那么weak修饰的指针与被指向的对象在底层的运作机制是怎样的呢?
1.weak指针实现原理的源码在static id storeWeak(id *location, objc_object *newObj),跟着源码进入一看究竟(源码已做大量精简和修改,仅留下核心流程):

static id storeWeak(id *location, objc_object *newObj){
    if (haveOld) {
        //通过weak指针之前指向的对象oldObj来找到之前的所存储在的sidetable
        id oldObj = *location;
        SideTable *oldTable = &SideTables()[oldObj];
        //当前weak指针从旧对象的弱引用表oldTable->weak_table中清除
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    if (haveNew) {
        //通过新的对象newObj来找到现在对应的sidetable
        SideTable *newTable = &SideTables()[newObj];
        //写入弱引用表
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location,  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
        
        //标记新对象的isa.weakly_referenced = true;
        if (!newObj->isTaggedPointerOrNil()) { newObj->setWeaklyReferenced_nolock();}

        //赋值新值
        *location = (id)newObj;
    }
  
    return (id)newObj;
}

该函数总体来说做了2件事:

  • 如果该weak指针有指向的旧对象oldObj,则根据oldObj拿到所在的弱引用表oldTable,然后从oldTable->weak_table中清除掉当前weak指针。
  • 如果有设置新对象newObj,则根据newObj拿到所在的弱引用表newTable,然后将当前weak指针写入newTable->weak_table。再标记新对象已经被弱引用,最后赋值,返回新值。

2.weak_unregister_no_lock的流程

void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id,   id *referrer_id){
    objc_object *referent = (objc_object *)referent_id;//被弱引用的对象
    objc_object **referrer = (objc_object **)referrer_id;//weak指针
    if (!referent) return;
    weak_entry_t *entry;
    //weak_entry_for_referent是指从weak_table->weak_entries中通过referent,拿到referent的引用实体
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //从entry->referrers中移除weak指针
        remove_referrer(entry, referrer);
        bool empty = true;
        if (entry->out_of_line()  &&  entry->num_refs != 0) {
            empty = false;
        }
        else {
            for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
                if (entry->inline_referrers[i]) {
                    empty = false; 
                    break;
                }
            }
        }
        //如果entry->referrers中的weak指针全部移除了(空了),那么将当前的entry从weak_table中移除。
        if (empty) {
            weak_entry_remove(weak_table, entry);
        }
    }
}

该函数有三个核心步骤:

  • weak_entry_t *entry = weak_entry_for_referent(weak_table, referent),从weak_table-> weak_entries中通过referent,拿到referent的引用实体。
  • 从entry->referrers中移除weak指针
  • 如果entry->referrers中的weak指针全部移除了(空了),那么将当前的entry从weak_table中移除。
  1. weak_register_no_lock的流程
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, WeakRegisterDeallocatingOptions deallocatingOptions){
    objc_object *referent = (objc_object *)referent_id;//被弱引用的对象
    objc_object **referrer = (objc_object **)referrer_id;//weak指针
    if (referent->isTaggedPointerOrNil()) return referent_id;
    ...
    weak_entry_t *entry;
    //weak_entry_for_referent是指从weak_table->weak_entries中通过referent,拿到referent的引用实体
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        //将weak指针添加到entry->referrers中
        append_referrer(entry, referrer);
    } 
    else {
        //如果没有新对象的weak_entry_t,则新建一个new_entry(写入referent和referrer)
        weak_entry_t new_entry(referent, referrer);
        //如果weak_table中存储的存储已满3/4,则扩容2倍(首次开辟大小为64),扩容后会将原有的weak指针列表赋值一份到新的weak_entries中。
        weak_grow_maybe(weak_table);
        //将当前new_entry插入weak_table-> weak_entries中
        weak_entry_insert(weak_table, &new_entry);
    }
    return referent_id;
}

该函数有三个核心步骤:

  • weak_entry_t *entry = weak_entry_for_referent(weak_table, referent))从weak_table->weak_entries中通过referent,拿到referent的引用实体
  • 如果entry存在,则直接将weak指针插入到entry->referrers中。
  • 如果entry不存在,则新建一个new_entry,将referent与 referrer关联,插入到weak_table-> weak_entries中。(插入之前会按需扩容)。

无论是插入一个新的weak指针还是移除一个旧的weak指针,都仅仅围绕着weak_table_t和weak_entry_t展开的,看一下数据结构:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;//弱引用表
};

//weak_table_t
struct weak_table_t {
    weak_entry_t *weak_entries;//weak_entry_t列表
    size_t    num_entries;//weak_entries中weak_entry_t的个数
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

//weak_entry_t
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;//被引用的对象
    union {
        struct {
            weak_referrer_t *referrers;//weak指针
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
};

整个查找的思路:从最开始的StripedMap<SideTable>>中通过对象的hash查找对应的SideTable,然后从SideTable->weak_table->weak_entries中根据对象的hash查找出weak_entry_t。再根据weak_entry_t-> referrers进行weak指针的插入和删除。在向weak_table-> weak_entries插入weak_entry_t的时候涉及到扩容(如果需要),当weak_table->num_entries >= (weak_table.mask+1)3/4的时候,会进行2倍扩容(首次容量大小64)。在向weak_entry_t-> referrers插入weak指针的时候也涉及到扩容(如果需要),当weak_entry_t->num_refs >= (weak_entry_t-> mask+1)3/4的时候,会进行2倍扩容(首次容量大小为8)。

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

推荐阅读更多精彩内容

  • iOS的日常开发中KVC与KVO还是使用率蛮高的,整理一下。KVC和KVO都是基于OC的动态特性和Runtime机...
    苹果我咬了一口阅读 568评论 0 0
  • 什么是KVC? KVC(Key-value coding)键值编码,单看这个名字可能不太好理解。其实是指iOS的开...
    祀梦_阅读 915评论 0 7
  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    暮年古稀ZC阅读 2,127评论 2 9
  • KVC定义 KVC(Key-value coding)键值编码,就是指iOS开发中,可以允许开发中通过Key名直接...
    雨林QiQi阅读 370评论 0 1
  • 官方链接. About Key-Value Coding KVC 是一种由对象采用 NSKeyValueCodin...
    Vency_阅读 393评论 0 2