Runtime源代码解读5(属性)

751 阅读8分钟

2019-10-15

属性(property)是为类的成员变量提供公开的访问器。属性与方法有非常紧密的联系,可读写的属性有 getter 和 setter 两个方法与之对应。

一、属性概述

属性(property)大多数情况是作为成员变量的访问器(accessor)使用,为外部访问成员变量提供接口。使用@property声明属性时需要指定属性的特性(attribute),包括:

  • 读写特性(readwrite/readonly);
  • 原子性(atomic/nonatomic);
  • 内存管理特性(assign/strong/weak/copy);
  • 是否可空(nullable/nonnull);

注意:上面括号中的第一个值是属性的默认特性,不过是否可空有其特殊性,可以通过NS_ASSUME_NONNULL_BEGIN/NS_ASSUME_NONNULL_END宏包围属性的声明语句,将属性的默认可空特性置为nonnull

除了上述特性还可以显式指定getter、setter。属性的特性指定了属性作为访问器的行为特征。声明了属性只是意味着声明了访问器,此时的访问器是没有gettersetter的实现的,想要访问器关联特定的成员变量在代码上有两种方式:1、使用@synthesize修饰符合成属性;2、实现属性的 getter 和 setter。但是两者的本质是一样的,就是按属性的特性实现属性的 getter 和 setter。

注意:@dynamic修饰属性,表示不合成属性的 getter 和 setter。此时要么在当前类或子类的实现中实现getter/setter、要么在子类实现中用@synthesize合成属性。

二、数据结构

类的class_rw_t中包含属性列表property_array_t类的properties成员,property_array_tlist_array_tt<property_t, property_list_t>,因此类中的属性列表保存property_list_t的数组,同样是个二维数组,property_list_t继承自entsize_list_tt顺序表容器,元素类型是property_t。相关数据结构如下,大部分在介绍成员变量和方法列表时有提及,因此不再赘述。从属性相关的数据结构可知,类中保存的属性信息只有属性名、特性信息。

属性在类中的保存与方法列表的保存也非常相似。class_ro_t中的property_list_t类型的basePropertyList仅保存类定义时定义的基本属性,这些属性是编译时决议的;class_rw_t中的property_array_t类型的properties保存类的完整属性列表,包括类的基本属性,以及运行时决议的 类的分类中定义的属性以及运行时动态添加的属性。

class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

 public:
    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};

struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};

struct property_t {
    const char *name;
    const char *attributes;
};

三、添加属性的实现原理

添加属性调用class_addProperty(...)函数,注意到在源代码中并没有与方法列表相关的操作,推测属性关联方法列表的操作隐藏在@synthesize@dynamic以及属性的attributes解析的实现中没有公开。属性添加与方法添加也基本一样,是添加到class_rw_t的完整属性列表properties的外层一位数组容器的开头,因此也满足优先级关系:运行时动态添加的属性 > 类的分类定义的属性 > 类定义时定义的基本属性

// 添加属性
BOOL 
class_addProperty(Class cls, const char *name, 
                  const objc_property_attribute_t *attrs, unsigned int n)
{
    return _class_addProperty(cls, name, attrs, n, NO);
}

// 添加属性、替换属性的实现逻辑
static bool 
_class_addProperty(Class cls, const char *name, 
                   const objc_property_attribute_t *attrs, unsigned int count, 
                   bool replace)
{
    if (!cls) return NO;
    if (!name) return NO;

    // 根据名称获取类的属性
    property_t *prop = class_getProperty(cls, name);
    if (prop  &&  !replace) {
        // 已存在且不是指定替换属性
        return NO;
    } 
    else if (prop) {
        // 替换属性
        rwlock_writer_t lock(runtimeLock);
        try_free(prop->attributes);
        prop->attributes = copyPropertyAttributeString(attrs, count);
        return YES;
    }
    else {
        rwlock_writer_t lock(runtimeLock);
        
        assert(cls->isRealized());
        
        property_list_t *proplist = (property_list_t *)
            malloc(sizeof(*proplist));
        proplist->count = 1;
        proplist->entsizeAndFlags = sizeof(proplist->first);
        proplist->first.name = strdup(name);
        proplist->first.attributes = copyPropertyAttributeString(attrs, count);
        
        cls->data()->properties.attachLists(&proplist, 1);
        
        return YES;
    }
}

objc_property_t class_getProperty(Class cls, const char *name)
{
    if (!cls  ||  !name) return nil;

    mutex_locker_t lock(runtimeLock);

    checkIsKnownClass(cls);
    
    assert(cls->isRealized());

    for ( ; cls; cls = cls->superclass) {
        for (auto& prop : cls->data()->properties) {
            if (0 == strcmp(name, prop.name)) {
                return (objc_property_t)&prop;
            }
        }
    }
    
    return nil;
}

四、访问属性的实现原理

存取属性值的代码集中在objc-accessors.mm源文件中。

4.1 获取对象属性值

调用objc_getProperty_gc(...)获取对象的属性值,实际上只是按一定的方式访问了属性对应的成员变量空间。若属性为atomic则会在获取属性值的代码两头添加spinlock_t的加锁解锁代码,这也是atomicnonatomic的区别所在。

注意:实际上,第三节 介绍的动态添加属性对应用开发者并没有什么用处(对 runtime 本身当然是有用的),原因是:1、没有指定属性关联成员变量的 runtime API;2、可以通过定义函数关联对象模拟属性,此时动态添加的属性就成了鸡肋,可有可无。

#if SUPPORT_GC
id objc_getProperty_gc(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    return *(id*) ((char*)self + offset);
}
#else
id 
objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) 
{
    return objc_getProperty_non_gc(self, _cmd, offset, atomic);
}
#endif

id objc_getProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

获取属性值对copy类型没有做相关处理,也就是说**copy属性的getter返回的也是属性指向对象本身,copy属性的getter并不包含拷贝操作**。用以下代码可以验证,在标记处打断点运行。查看testObj的内存,第8-16个字节保存的就是testObjarr属性所指向的实际的NSArray地址。会发现打印出来的testObj.arr地址和testObj的内存的第8-16个字节内容是一致的。

@interface TestPropCopy: NSObject

@property(copy, nonatomic) NSArray* arr;  // 不要用NSString类型,调试时不好看地址

@end

@implementation TestPropCopy

+(void)testPropCopy{
    NSArray* arr = [NSArray arrayWithObject:@"app"];
    TestPropCopy* testObj = [[self alloc] init];
    testObj.arr = arr;
    
    NSLog(@"testObj:%@", testObj);
    NSLog(@"arr:%@", arr);
    NSLog(@"testObj.arr:%@", testObj.arr);

    // 此处打断点
}

@end

注意:spinlock_t的本质是os_lock_handoff_s锁,关于这个锁网上找不到什么资料,推测是个互斥锁。注意spinlock_t并不是OSSpinLockOSSpinLock已知存在性能问题,已经被弃用。

4.2 修改对象属性值

调用objc_setProperty(...)设置对象的属性值,同样是是按一定的方式访问属性对应的成员变量空间。同样,若属性为atomic则会在设置属性值的代码两头添加spinlock_t的加锁解锁代码。若属性为copy时,则将传入 setter 的参数指向的对象 拷贝到对应的成员变量空间。

#if SUPPORT_GC
void objc_setProperty_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
    if (shouldCopy) {
        newValue = (shouldCopy == MUTABLE_COPY ? [newValue mutableCopyWithZone:nil] : [newValue copyWithZone:nil]);
    }
    objc_assign_ivar(newValue, self, offset);
}
#else
void 
objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, 
                 BOOL atomic, signed char shouldCopy) 
{
    objc_setProperty_non_gc(self, _cmd, offset, newValue, atomic, shouldCopy);
}
#endif

void objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

// 设置属性值的总入口
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);

    // 若属性为copy,则将newVal参数指向的对象拷贝到对应成员变量空间
    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);
}

注意:存取属性值的实现是直接调用属性的getter、setter的对应的方法的SEL触发的,属性与方法的关联细节则没有公布源代码。

4.3 探讨属性关联成员变量的实现

Objective-C 代码中,属性关联成员变量是通过@synthesize实现的,runtime 并没有公开这块代码。本节探讨@synthesize的实现原理。

类定义属性且不手动定义属性的 getter 和 setter 方法时,类的方法列表会添加对应的prop方法及setProp方法。且object_getProperty(...)的参数列表包含self_cmd,和消息的格式很类似。因此 推测 声明属性@synthesize时,runtime 会根据属性的attributes生成属性的 getter 方法SELIMP和 setter 方法SELIMP,并将其添加到类的方法列表中,getter 和 setter 的IMP伪代码如下,其中#号包围的时编译时可确定的参数;

id propGetter(id self, SEL _cmd) {
    char* ivarName = synthizeName ? : ( '_' + #propertyName#)

    Class selfClass = object_getClass(self)
    Ivar ivar = getIvar(Class cls, ivarName)
    uint_32 ivarOffset = ivar_getOffset(ivar)
    
    objc_getProperty(self, _cmd, 
                     #propertyNameAttr#, 
                     ivar,
                     #propertyAtomicAttr#)
} 

void propSetter(id self, SEL _cmd, id newVal) {
    char* ivarName = #synthesizeName# ? : ( '_' + #propertyName#)

    Class selfClass = object_getClass(self)
    Ivar ivar = getIvar(Class cls, ivarName)
    uint_32 ivarOffset = ivar_getOffset(ivar)

    objc_setProperty(self, _cmd, 
                     #propertyNameAttr#, 
                     newVal
                     ivar,
                     #propertyAtomicAttr#,
                     #propertyShouldCopyAttr#)
}

但是上面的处理是有明显的性能缺陷的,每次访问成员变量是都要调用getIvar(...),而getIvar(...)是遍历类的整个成员变量列表,根据成员变量名查找成员变量,实际实现显然不应如此。因此上述代码只是模拟了属性的实现流程,而具体实现细节将在后续将在独立文章中介绍。

五、总结

  • Runtime 提供的关于属性的动态特性对应用开发的意义不大,runtime 属性关联成员变量是隐藏在@synthesize的实现代码中,而且成员变量不能动态添加,因此即使提供也是意义不大;

  • 动态添加属性时,是不包含添加属性 getter 和 setter 方法的操作的,因此必须手动实现其getter 和 setter 方法,为分类定义属性也是不包含 getter 和 setter 方法实现,开发者可以;

  • 下一篇文章介绍分类。