property的研究(一):来源

@property属性相关

如果没有特殊标明,下面的所有代码都是在ARC环境下。

官方文档

@property的创造原因

在不使用属性的时候,我们往往会如下创建对象

@implementation ViewController
{
    NSString *aaa;   
}

但是这里有个问题,在于对象布局在编译期已经被固定了。当你访问这个变量的时候,编译器就会将其替换为指针偏移量。这个偏移量是硬编码的,表示变量距离存放对象的内存区域的起始地址有多远。但是假如又加了一个变量,就要重新编译。 这种问题有两种解决方案: 1.把实例变量当做一种存储偏移量的特殊变量交给类对象保管,然后偏移量会被在运行期中查找,如果类定义变了,那么偏移量也就变了; 2.就是属性的方法。不直接访问实例变量,通过存取方法来处理。

原理

编译器在编译期为实例变量添加的setter、getter方法。 在 runtime.h文件中,定义如下

typedef struct objc_property *objc_property_t;

而objc_property是一个结构体,包括name和attributes,定义如下:

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

这里attributes本质是 objc_property_attribute_t,定义了property的一些属性,定义如下:

/// Defines a property attribute
typedef struct {
    const char *name;           /**< The name of the attribute */
    const char *value;          /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;

我们使用 property_getAttributes方法,可以知道包括类型、原子性、内存语义和实例变量等。在下面我们可以看到相关代码。

关键字

默认状况下,关键字是 strong, atomic, readwrite

用@property的时候会自动创建创建实例变量和setter、getter方法。 我们写一个属性:

@property (nonatomic, copy) NSString *Balaeniceps_rex;

然后利用 class_copyPropertyListclass_copyMethodList方法查看属性和方法

    unsigned int propertyCount;
    objc_property_t *propertyList = class_copyPropertyList([self class], &propertyCount);
    for (unsigned int i = 0; i< propertyCount; i++)
    {
        const char *name = property_getName(propertyList[i]);
        NSLog(@"__%@",[NSString stringWithUTF8String:name]);
        objc_property_t property = propertyList[i];
        const char *a = property_getAttributes(property);
        NSLog(@"属性信息__%@",[NSString stringWithUTF8String:a]);
    }

    u_int methodCount;
    NSMutableArray *methodList = [NSMutableArray array];
    Method *methods = class_copyMethodList([self class], &methodCount);
    for (int i = 0; i < methodCount; i++)
    {
        SEL name = method_getName(methods[i]);
        NSString *strName = [NSString stringWithCString:sel_getName(name) encoding:NSUTF8StringEncoding];
        [methodList addObject:strName];
    }
    free(methods);
    
    NSLog(@"方法列表:%@",methodList);

打印出来结果

属性信息__T@"NSString",C,N,V_Balaeniceps_rex
方法列表:(
    "Balaeniceps_rex",
    "setBalaeniceps_rex:",
    ".cxx_destruct",
    viewDidLoad
    )

然后通过官方文档,查阅到T表示类型,C表示copy,N表示nonatomic,V表示实例变量。 这里多出来一个 .cxx_destruct,可以查看sunnyxx的ARC下dealloc过程及.cxx_destruct的探究来理解。 这个方法简单来讲作用如下

1.只有在ARC下这个方法才会出现(试验代码的情况下) 2.只有当前类拥有实例变量时(不论是不是用property)这个方法才会出现,且父类的实例变量不会导致子类拥有这个方法 3.出现这个方法和变量是否被赋值,赋值成什么没有关系

nonatomic&atomic

atomic和nonatomic的主要区别是系统自动生成的getter/setter方法不同。atomic会自动为getter/setter方法加锁;而nonatomic则相反。

系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。当然,这也造成了比nonatomic慢。 但是atomic并不是实际场景中的“安全”。它只保证本身的setter和getter方法安全,但是不保证它会不会被销毁,以及使用过程中添加到其他对象中的修改。 这里我们可以查看源码

id objc_getProperty(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);
}

在执行getter方法的时候,如果不是atomic的话,是不会加锁并进行额外的retain的。

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}
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);
    }
//判断是否atomic
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
    //使用自旋锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

这里的setter实际上是一个内联函数。我们可以发现如果是atomic的话,在内部会实现一个自旋锁,并同样会reatin,只有当方法完成之后才会被释放。 而如果是使用的nonatomic的话,可能会因为线程的频繁切换而造成 EXC_BAD_ACCESS

readwrite&readonly

没啥好说的,系统给打的一个标识。

内存管理相关

assign

在对基本数据类型的简单赋值操作的时候,进行简单的赋值操作。但是虽然它也可以用来修饰对象,但是应该坚决避免在这种情况。 主要是因为,它释放之后,指针不会自动置空。基本数据类型是会被放入栈中,由系统来同一处理。 很多老的代码里,或者从MRC转换过来的代码中,经常会发现这个的滥用。比如说delegate使用assign修饰,是有可能发生崩溃的。

strong

用来修饰强引用的属性,会使对象引用计数+1. 当你需要长时间使用某个属性,并且不希望被自动释放的时候,就应该使用这个。修饰可变数据类型用此。

unsafe_unretained

语义和assign类似,是weak在iOS4.0之前版本的补充。但是问题在于,对象释放之后还是会继续指向对象存在的内存,太危险了。 现在没什么意义了,别用。

copy

一般来讲,对于不可变的对象使用这个修饰。在setter的时候相当于自动调用一次copy(也就是说,在没有执行setter方法的时候,是起不到作用的)。 如果想要令自己的类具备拷贝功能,是需要遵循 NSCopying协议,并实现 copyWithZone方法。而自己的immutable和mutable的类,实现拷贝的时候,是需要实现NSCopying和NSMutableCopying协议的。

  • 深拷贝指的是,在拷贝对象自身的时候,将其底层数据一并复制过去。浅拷贝指的是只拷贝对象本身。 我们查看runtime源码,可以知道,是存在两个copy方法的。
+ (id)copyWithZone:(struct _NSZone *)zone {
    return (id)self;
}
+ (id)mutableCopyWithZone:(struct _NSZone *)zone {
    return (id)self;
}

在使用的时候,就分别是 copymutableCopy 两种方法,也就是 NSCopyingNSMutableCopying 协议。 我们可以认为,在自动实现的setter方法中,实际上是执行了一个 [obj copy];的方法。

weak

weak方法是用来在某些情况下替代strong的属性。它的特点是,不会使对象的引用计数加1,可以避免循环引用问题;并且不会保留传入的对象。如果对象被释放,那么对应的实例变量会被自动设置为nil,不会变成野指针。 有关weak更多的可以查看这篇文章。

方法名

比如属性中的getter=isOn。 这种方法一般在使用BOOL值的时候用来指定方法名。了解的并不是很多。

nullable&nonnull

nullable表示对象可以是NULL或nil,而nonnull表示对象不应该为空。 比如说

@property (nonatomic, weak, nullable) id object;
@property (nonatomic, strong, nonnull) NSString *str;

参考资料

iOS @property探究(二): 深入理解

Runtime源码 —— property和ivar

iOS中Weak的底层实现

atomic性能真的很差,并发queue+barrier性能真的很好吗?

atomic 和 nonatomic 有什么区别?

谈nonatomic非线程安全问题

iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)

weak 弱引用的实现方式

从经典问题来看 Copy 方法