Objective-C对象成员变量是如何存取的

作者: 01_Jack | 来源:发表于2020-01-16 20:25 被阅读0次

    之前写过一篇文章 Objective-C对象内存分布是怎样确定的,作为姊妹篇,两者配合食用口味更佳。


    0x00 API

    runtime.h中可以找到如下接口:

    OBJC_EXPORT id _Nullable
    object_getIvar(id _Nullable obj, Ivar _Nonnull ivar) 
        OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
    OBJC_EXPORT void
    object_setIvar(id _Nullable obj, Ivar _Nonnull ivar, id _Nullable value) 
        OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
    OBJC_EXPORT void
    object_setIvarWithStrongDefault(id _Nullable obj, Ivar _Nonnull ivar,
                                    id _Nullable value) 
        OBJC_AVAILABLE(10.12, 10.0, 10.0, 3.0, 2.0);
    
    OBJC_EXPORT Ivar _Nullable
    object_setInstanceVariable(id _Nullable obj, const char * _Nonnull name,
                               void * _Nullable value)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
        OBJC_ARC_UNAVAILABLE;
    
    OBJC_EXPORT Ivar _Nullable
    object_setInstanceVariableWithStrongDefault(id _Nullable obj,
                                                const char * _Nonnull name,
                                                void * _Nullable value)
        OBJC_AVAILABLE(10.12, 10.0, 10.0, 3.0, 2.0)
        OBJC_ARC_UNAVAILABLE;
    
    OBJC_EXPORT Ivar _Nullable
    object_getInstanceVariable(id _Nullable obj, const char * _Nonnull name,
                               void * _Nullable * _Nullable outValue)
        OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0)
        OBJC_ARC_UNAVAILABLE;
    

    这 6个函数是用来对成员变量进行存取操作的,其中后三个函数在ARC下不可用。从函数形参来看,MRC下的函数只需要传入成员变量的名字char *即可对成员变量进行存取,而前三个函数则要传入Ivar,显然MRC下的接口更易用。

    0x01 set

    void object_setIvar(id obj, Ivar ivar, id value);
    void object_setIvarWithStrongDefault(id obj, Ivar ivar, id value);
    Ivar object_setInstanceVariable(id obj, const char *name, void *value);
    Ivar object_setInstanceVariableWithStrongDefault(id obj, const char *name, void *value);
    

    查看源码可发现,这4个函数在最终都会调用_object_setIvar

    static ALWAYS_INLINE 
    void _object_setIvar(id obj, Ivar ivar, id value, bool assumeStrong)
    {
        if (!obj  ||  !ivar  ||  obj->isTaggedPointer()) return;
    
        ptrdiff_t offset;
        objc_ivar_memory_management_t memoryManagement;
        _class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);
    
        if (memoryManagement == objc_ivar_memoryUnknown) {
            if (assumeStrong) memoryManagement = objc_ivar_memoryStrong;
            else memoryManagement = objc_ivar_memoryUnretained;
        }
    
        id *location = (id *)((char *)obj + offset);
    
        switch (memoryManagement) {
        case objc_ivar_memoryWeak:       objc_storeWeak(location, value); break;
        case objc_ivar_memoryStrong:     objc_storeStrong(location, value); break;
        case objc_ivar_memoryUnretained: *location = value; break;
        case objc_ivar_memoryUnknown:    _objc_fatal("impossible");
        }
    }
    
    1. 如果obj或ivar为空,或obj不为空但是个TaggedPointer,则直接返回。关于TaggedPointer可以查看 TaggedPointer的推理与验证

    2. 通过_class_lookUpIvar获取当前成员变量在obj中的偏移量offset,与当前成员变量的所有权memoryManagement

    3. 如果所有权为unknown,则通过参数assumeStrong来对所有权赋值。assumeStrong为true,赋予__strong所有权,assumeStrong为false,赋予__unsafe_unretained所有权。

    值得一提的是:object_setIvarWithStrongDefaultobject_setInstanceVariableWithStrongDefault内部调用这个函数时,给assumeStrong 传递的参数都是true,这也是为什么我们在写诸如@property (nonatomic) id name之类的代码,他的默认修饰符是strong的原因。

    可以就这个点简单的验证一下:

    @interface Test : NSObject
    @property (nonatomic, strong) id a;
    @property (nonatomic, copy) id b;
    @property (nonatomic) id c;
    @property (nonatomic, weak) id d;
    @property (nonatomic, assign) id e;
    @property (nonatomic, unsafe_unretained) id f;
    @end
    
    @implementation Test
    @end
    
    xcrun -sdk macosx clang -arch x86_64 -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.15.1 -Wno-deprecated-declarations Test.m
    

    通过以上命令得到:

    可见,strong、copy或者默认修饰符对应着__strong,weak对应__weak,assign与unsafe_unretained对应着__unsafe_unretained

    1. id *location = (id *)((char *)obj + offset)这句代码是整个函数的核心所在,先获取obj地址,通过offset偏移量获得成员变量存储地址。location是指向成员变量地址的指针,当所有权是__weak与__strong时,分别通过objc_storeWeakobjc_storeStrong进行后续操作,当所有权是__unsafe_unretained时,直接向location(地址)写数据。

    0x02 objc_storeStrong

    void objc_storeStrong(id *location, id obj)
    {
        id prev = *location;
        if (obj == prev) {
            return;
        }
        objc_retain(obj);
        *location = obj;
        objc_release(prev);
    }
    

    obj所有权为__strong时会调用这个函数,函数本身没啥好说的,通过*location取值,如果取到的值与要存的值相等则return,否则,先将要obj引用计数器加1,然后将向location(地址)中写入obj,再对开始通过*location取出的prev执行release操作。

    由于这里对obj执行了retain,所以obj不会释放,从而确保通过*location取出的值就是obj。

    0x03 objc_storeWeak

    id objc_storeWeak(id *location, id newObj)
    {
        return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
            (location, (objc_object *)newObj);
    }
    
    enum HaveOld { DontHaveOld = false, DoHaveOld = true };
    enum HaveNew { DontHaveNew = false, DoHaveNew = true };
    enum CrashIfDeallocating {
        DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
    };
    

    所以可以简化为:

    storeWeak<true, true, false>(location, (objc_object *)newObj);
    

    接着来看storeWeak,这里的代码较多,但核心点就三个函数:

    void weak_unregister_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id);
    
    id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating);
    
    inline void objc_object::setWeaklyReferenced_nolock();
    

    分别对应着:

    1. 将旧对象与location解绑
    2. 将新对象与location绑定
    3. 设置对象isa的weakly_referenced字段设置为true,用于标识有弱引用引用该对象

    0x04 weak_table_t

    在unregister于register函数中,可以看到weak_table形参的类型是weak_table_t *,接着看weak_table_t 是什么:

    /**
     * The global weak references table. Stores object ids as keys,
     * and weak_entry_t structs as their values.
     */
    struct weak_table_t {
        weak_entry_t *weak_entries;
        size_t    num_entries;
        uintptr_t mask;
        uintptr_t max_hash_displacement;
    };
    

    weak_table_t是个典型的hash结构,具体成员含义如下:

    • weak_entries
      弱引用对象的相关信息会被整合到weak_entry_t类型的数据结构中,而weak_entries是个动态数组,用于存储这些weak_entry_t结构信息

    • num_entries
      weak_entries动态数组中的元素个数

    • mask
      hash掩码

    • max_hash_displacement
      出现hash碰撞的最大可能次数

    weak_table_t的取值操作如下:

    static weak_entry_t * weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
    {
        assert(referent);
    
        weak_entry_t *weak_entries = weak_table->weak_entries;
    
        if (!weak_entries) return nil;
    
        size_t begin = hash_pointer(referent) & weak_table->mask;
        size_t index = begin;
        size_t hash_displacement = 0;
        while (weak_table->weak_entries[index].referent != referent) {
            index = (index+1) & weak_table->mask;
            if (index == begin) bad_weak_table(weak_table->weak_entries);
            hash_displacement++;
            if (hash_displacement > weak_table->max_hash_displacement) {
                return nil;
            }
        }
        
        return &weak_table->weak_entries[index];
    }
    

    以要referent 为key,通过hash_pointer(referent) & weak_table->mask计算得出索引值,如果从动态数组对应索引取出的weak_entry_t的成员referent与参数referent 不同(哈希碰撞),则index = (index+1) & weak_table->mask如此循环直到两者相同。如果哈希碰撞次数超过最大可能次数,则通过bad_weak_table报错。最后,将通过最终索引取到的weak_entry_t的地址返回。

    可见,weak_table_t是以要存储对象为key,来存储weak_entry_t的,而要存储对象weak指针的信息存储在weak_entry_t中。

    0x05 weak_entry_t

    struct weak_entry_t {
        DisguisedPtr<objc_object> referent;
        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];
            };
        };
    
        bool out_of_line() {
            return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
        }
    
        weak_entry_t& operator=(const weak_entry_t& other) {
            memcpy(this, &other, sizeof(other));
            return *this;
        }
    
        weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
            : referent(newReferent)
        {
            inline_referrers[0] = newReferrer;
            for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
                inline_referrers[i] = nil;
            }
        }
    };
    
    typedef DisguisedPtr<objc_object *> weak_referrer_t;
    
    #if __LP64__
    #define PTR_MINUS_2 62
    #else
    #define PTR_MINUS_2 30
    #endif
    
    #define WEAK_INLINE_COUNT 4
    
    #define REFERRERS_OUT_OF_LINE 2
    
    • referent
      要存储的对象

    • union
      这个union分为两个struct,上半部分很明显又是个hash结构,用于动态存储弱引用指针的地址,下半部分是个静态数组,长度为2,用于静态存储弱引用指针的地址。当弱引用个数大于2时,会从静态存储转成动态存储。

    OK,现在来重新捋一遍思路。弱引用的存储会调用objc_storeWeak,而这个函数内部出现了weak_table_t数据结构,weak_table_t 内部又有weak_entry_t数据结构。现在回到objc_storeWeak本身,weak_table_t类型的数据又是从哪来的?

    0x06 SideTable与StripedMap

    在objc_storeWeak 中有诸如&oldTable->weak_table&newTable->weak_table的取值方式,而oldTable与newTable都是SideTable类型

    struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;
        weak_table_t weak_table;
    
        ...
    };
    
    template<typename T> class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
        enum { StripeCount = 8 };
    #else
        enum { StripeCount = 64 };
    #endif
    
        struct PaddedT {
            T value alignas(CacheLineSize);
        };
    
        PaddedT array[StripeCount];
    
        static unsigned int indexForPointer(const void *p) {
            uintptr_t addr = reinterpret_cast<uintptr_t>(p);
            return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
        }
        
        ...
    }static StripedMap<SideTable>& SideTables() {
        return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
    }
    
    enum { CacheLineSize = 64 };
    
    

    因为CacheLineSize等于64,显然SideTable占64个字节。iPhone真机下,StripedMap中的StripeCount等于8,否则等于64。这也就意味着,在真机下最多只能存在8种不同对象的弱引用

    最终可以总结如下:

    1. StripedMap中存储着多个SideTable(iPhone真机最多为8个,否则最多为64个),每个SideTable代表一种对象的弱引用
    2. SideTable中存储着weak_table_t,每个weak_table_t存储着weak_entry_t类型动态数组,动态数组的个数代表当前对象含有的弱引用的个数
    3. weak_entry_t用来存储具体弱引用指针的地址

    到这里set部分就结束了,接着来看get部分

    0x07 get

    id object_getIvar(id obj, Ivar ivar);
    Ivar object_getInstanceVariable(id obj, const char *name, void **value);
    

    object_getInstanceVariable中会调用object_getInstanceVariable,因此直接来看这个函数:

    id object_getIvar(id obj, Ivar ivar)
    {
        if (!obj  ||  !ivar  ||  obj->isTaggedPointer()) return nil;
    
        ptrdiff_t offset;
        objc_ivar_memory_management_t memoryManagement;
        _class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);
    
        id *location = (id *)((char *)obj + offset);
    
        if (memoryManagement == objc_ivar_memoryWeak) {
            return objc_loadWeak(location);
        } else {
            return *location;
        }
    }
    

    这个逻辑很简单,如果对象不是TaggedPointer,则通过_class_lookUpIvar取出偏移量与所有权,通过对象地址加上偏移量得到偏移量的地址,如果所有权是__weak,则通过objc_loadWeak取值,否者直接通过*location取值。

    通过*location取值又分两种情况,所有权为__strong与__unsafe_unretained:

    1. 所有权为__strong时,由于在set阶段已经对obj执行了retain 操作,所以通过*location总是可以取到正确的值
    2. 所有权为__unsafe_unretained,由于在set阶段直接*location = value赋值,value有可能已被释放,当通过*location取值时,可能出现野指针导致crash

    0x08 objc_loadWeak

    id objc_loadWeak(id *location)
    {
        if (!*location) return nil;
        return objc_autorelease(objc_loadWeakRetained(location));
    }
    

    objc_loadWeak会调用objc_loadWeakRetained
    关于objc_autorelease 与objc_loadWeakRetained,可以看我的另两篇文章 :

    一段weak代码引发的探索
    一文吃透autorelease


    Have fun!

    相关文章

      网友评论

        本文标题:Objective-C对象成员变量是如何存取的

        本文链接:https://www.haomeiwen.com/subject/ifcfzctx.html