iOS底层原理05 - 属性关键字copy&weak&strong底层分析

上一篇: iOS底层原理04 - 类的结构


Student类中添加分别由strongcopyweak修饰的属性:

在通过Clang编译后的.cpp文件中,看到会生成三个对应的setter方法:

唯独在setAge的方法调用中,是通过objc_setProperty方法实现的。

1. copy

我们再objc-818.4源码中搜索,看到了如下几种方法:

objc_setProperty
  • setProperty

objc源码中,搜索不到关于setProperty相关的调用,那么针对不同的修饰符,是如何跳转到对应的setProperty方法的呢?我们打开llvm源码来查找:

在llvm层,其实对copy属性做了编译器优化,之后才会调用objc中的objc_setProperty_xxx方法。我们这里定义的age属性使用了nonatomiccopy修饰,故name = "objc_setProperty_nonatomic_copy".

最后进入reallySetProperty流程,这里可以通过断点调试来看:

reallySetProperty
  • reallySetProperty
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}
  • 如果是copy或者mutableCopy并不会对新值进行objc_retain操作,而是copyWithZone,返回一个不可变的对象
  • 若为atomic原子性的属性,会在setter方法添加spinlock_t类型的自旋锁来保证多线程写入安全。

2. weak

.cpp中看到,使用weak修饰的对象,并不会走上面的objc_setProperty方法,而是走objc源码中的objc_storeWeak来更新弱引用指针的指向

static id 
storeWeak(id *location, objc_object *newObj)
{
    ASSERT(haveOld  ||  haveNew);
    if (!haveNew) ASSERT(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
    
    // 用地址作为唯一标识来获取新值和旧值锁的位置
    // 通过地址来创建索引标志,防止桶的重复
 retry:
    if (haveOld) {
        oldObj = *location;
        // 获得以oldObj为索引存储的SideTable
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        // 获得以newObj为索引存储的SideTable
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    // 加锁操作,防止多线程中竞争冲突 
    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
    // 避免线程冲突重处理
    // location 应该与 oldObj 保持一致,如果不同说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改  
    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
    // 避免弱引用机制和 +initialize 之间的死锁
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // 清除旧值
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }
    // 设置新值
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating ? CrashIfDeallocating : ReturnNilIfDeallocating);
       
        if (!newObj->isTaggedPointerOrNil()) {
            // 弱引用位初始化操作      
            // 引用计数那张散列表的weak引用对象的引用计数中标识为weak引用
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
    callSetWeaklyReferenced((id)newObj);
    return (id)newObj;
}
  • location是指向weak指针的指针,因为要修改weak指针
  • 上面storeWeak的过程,就是在解除与旧对象的关系,并与新对象建立联系

runtime维护了一张weak表,用来存储指向某个对象的所有weak指针,从源码中看到,这张表其实就是SideTables,这是一张hash表,key是对象的地址,value是weak指针的数组。

SideTables 散列表

SideTables结构
  • 使用对象来获取对应的SideTable: &SideTables()[newObj]

  • spinlock_t slock : 自旋锁,用于上锁/解锁 SideTable

  • RefcountMap refcnts :以DisguisedPtr<objc_object>为key的hash表,用来存储OC对象的引用计数(仅在未开启isa优化 或 在isa优化情况下isa_t的引用计数溢出时才会用到)。

// RefcountMap disguises its pointers because we 
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;

RefcountMap是以DenseMap为模板创建的,三个参数分别为hash key类型,value类型,以及是否要在value==0时自动释放掉响应的hash节点,这里是true。

  • weak_table_t weak_table : 存储对象弱引用指针的hash表。是OC weak功能实现的核心数据结构。
struct weak_table_t {
    weak_entry_t *weak_entries; // hash数组 存储弱引用对象的相关信息
    size_t    num_entries;      // hash数组的元素个数
    uintptr_t mask;             // hash数组长度-1,参与hash计算
    uintptr_t max_hash_displacement;    // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误
};

weak_table_t是全局的弱引用表,将对象id存储为键,将weak_entry_t存储为它们的值。

在我们的App中,多个对象会重用同一个SideTable节点,也就是说,weak_table会存储多个对象的弱引用信息。因此在一个SideTable中,又会通过weak_table作为hash表再次分散存储每一个对象的弱引用信息。

  • weak_entry_t
struct weak_entry_t {
    DisguisedPtr<objc_object> referent; // 被弱引用的对象
    // 两种形式的联合体 - 动态数组weak_referrer_t和定长数组weak_referrer_t
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };
    ...
};
  • DisguisedPtr<objc_object> referent:弱引用对象的指针摘要
  • union:联合体,有两种形式 - 动态数组weak_referrer_t *referrers和定长数组weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]。 用来存储弱引用该对象的指针的指针。当弱引用该对象的指针数目小于等于WEAK_INLINE_COUNT 4时,为定长数组;当超过时,会将定长数组中的元素转移到动态数组中。

clearDeallocating

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);  // 从引用计数表中擦除该对象的引用计数
    }
    table.unlock();
}

当一个对象被释放,调用clearDeallocating后会做如下操作:

  • 由当前对象作为key找到SideTable
  • 将当前对象转为objc_object类型并作为key,从SideTableweak_table_t中取出weak_entry_t
  • 遍历weak_referrer_t,将指向该对象的弱引用指针置为nil;
  • referrers数组移除,weak_table中hash数组的元素个数-1
  • table.refcnts.eraser() 从引用计数表中擦除该对象的引用计数

3. strong

objc_storeStrong

若以strong修饰的属性,会调用objc_storeStrong方法,进行新值retain旧值release

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

推荐阅读更多精彩内容