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

之前写过一篇文章 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

通过以上命令得到:

Objective-C对象成员变量是如何存取的_第1张图片

可见,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
        (location, (objc_object *)newObj);
}

enum HaveOld { DontHaveOld = false, DoHaveOld = true };
enum HaveNew { DontHaveNew = false, DoHaveNew = true };
enum CrashIfDeallocating {
    DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
};

所以可以简化为:

storeWeak(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 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 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 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(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
    
    ...
}static StripedMap& SideTables() {
    return *reinterpret_cast*>(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对象成员变量是如何存取的)