Objective-C 之属性

1,350 阅读13分钟

什么是属性

  • 属性指的是一个对象的属性或特性。
  • 苹果公司在 Objective-C 2.0 中引入了属性(property),它组合了新的预编译指令和新的属性访问器语法。

自动合成

  • @ 符号标志着“这是 Objective-C 语法”。

@property

  • @property 是一种新的编译器功能,它意味着声明了一个新对象的属性。
  • @property 预编译指令的作用是自动声明属性的 setter 和 getter 方法。

用代码表示为:

@property UIView *view;

等同于:

- (UIView *)view;
- (void)setView:(UIView *)view;

@synthesize

  • @synthesize 也是一种新的编译器功能,它表示“创建了该属性的访问代码”。
  • 所有的属性都是基于变量的,所以在你合成(synthesize)getter 和 setter 方法的时候,编译器会自动创建与属性名称相同的实例变量。

用代码表示为:

@synthesize view;

等同于:

- (UIView *)view {
    return view;
}
- (void)setView:(UIView *)view {
    view = view;
}

@dynamic

  • @dynamic 关键字告诉编译器自动合成无效,不要生成任何代码或创建相应的实例变量,用户会自己生成属性的 getter 和 setter 方法。
  • @dynamic 通常用于子类中告诉编译器使用父类的属性实现。

Xcode4.4 以后

  • Xcode4.4 以后定义 @property 时可以省去 @synthesize。也就意味着 @property 可以自动声明和实现属性的 setter 和 getter 方法,并且自动创建名称为属性名前带有下划线(_)的实例变量。

用一个公式表示为:

@property = ivar + setter + getter

用代码表示为:

@property UIView *view;

等同于:

- (UIView *)view;
- (void)setView:(UIView *)view;
- (UIView *)view {
    return _view;
}
- (void)setView:(UIView *)view {
    _view = view;
}

小结

  • @property 是一种编译器指令,它意味着声明了一个新对象的属性。属性声明等同于声明了 setter 和 getter 方法。
  • @synthesize 也是一种编译器指令,会让编译器为类的实例变量自动生成访问方法。所有的属性都是基于变量的,所以在你合成(synthesize)getter 和 setter 方法的时候,编译器会自动创建与属性名称相同的实例变量。
  • @dynamic 关键字告诉编译器自动合成无效,不要生成任何代码或创建相应的实例变量,用户会自己生成属性的 getter 和 setter 方法。
  • Xcode4.4 以后定义 @property 时可以省去 @synthesize。如果你不使用 @synthesize,那么编译器生成的实例变量会以下划线(_)字符作为其名称的第一个字符。

关键字

  • 关键字也叫属性可选项,也叫属性特性。

读写性

关键字 说明
readwrite 读写
readonly 只读

方法名

关键字 说明
getter 指定 getter 方法的名字
setter 指定 setter 方法的名字

原子性

关键字 说明
atomic 原子性
nonatomic 非原子性
  • 原子性是多线程中的一个概念,如果说访问方法是原子的,那就意味着多线程环境下访问属性是安全的,在执行的过程中不可被打断。
  • 在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与后的结构,那么该操作就是“原子的”,或者说,该操作具备“原子性”。

内存管理

关键字 基本数据类型 MRC ARC
assign 直接赋值 直接赋值 直接赋值
unsafe_unretained 直接赋值 直接赋值 直接赋值
retain 出错 赋值并对新值进行 retain 操作 赋值并对新值进行 retain 操作
strong 出错 赋值并对新值进行 retain 操作 赋值并对新值进行 retain 操作
weak 出错 直接赋值 弱引用
copy 出错 赋值时建立传入值的一份副本 赋值时建立传入值的一份副本
  • assign “设置方法”只会执行针对“纯量类型”的简单赋值操作。
  • unsafe_unretained 此特质的语义和 assign 相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”,当目标对象遭到摧毁时,属性值不会自动清空,这一点与 weak 有区别。
  • retain 此特质表明该属性定义了一种“拥有关系”。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  • strong 此特质与 retain 等价。但 strong 是随着 ARC 时加入的新特性,所以在 MRC 环境下只能用 retain 来修饰。在 ARC 环境下,为了与 MRC 进行区分,也为了和 weak 对应,最好使用 strong。
  • weak 此特质表明该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似,然而在属性所指的对象遭到摧毁时,属性值也会清空。
  • copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝”。

小结

  • assign/unsafe_unretained 这两个关键字在实际使用时完全等价,表示一种“非拥有关系”,虽然允许修饰“对象类型”,但在使用时要用来修改“基本数据类型”。
  • strong/retain 这两个关键字在实际使用时完全等价,表示一种“拥有关系”,不能用来修饰“基本数据类型”。
  • weak/assign 这两个关键字都表示一种“非拥有关系”,但 weak 在所指的对象摧毁时,会清空属性值。
  • copy/strong 这两个关键字都表示一种“拥有关系”,但 copy 不保留新值,而是拷贝新值。

可空性

关键字 说明
nullable 可以为空
nonnull 不能为空
null_resettable getter 不能返回空,setter 可以传空
null_unspecified 不确定是否为空
  • 可空性关键字只能用来修饰“对象类型”的属性。
  • 如果使用了 null_resettable 关键字,必须重写 getter 或者 setter,保证 getter 返回值不为空。

类属性

关键字 说明
Class 类属性

类属性需要自己手写变量、getter 和 setter:

@property (nonatomic, strong, class) NSObject *classObject;
static NSObject *_classObject;

+ (NSObject *)classObject {
    return _classObject;
}

+ (void)setClassObject:(NSObject *)classObject {
    _classObject = classObject;
}

默认关键字

  • 基本数据类型: atomic,readwrite,assign
  • 对象类型: atomic,readwrite,strong

Protocol 和 Category 中的属性

Protocol 中的属性

  • 在 Protocol 中使用 @property 只会自动生成 setter 和 getter 方法声明,不会自动生成其实现。
  • 我们在 Protocol 中使用属性的目的,是希望遵守我协议的对象能实现该属性。

Category 中的属性

  • 在 Category 使用 @property 也是只会自动生成 setter 和 getter 方法的声明,不会自动生成其实现。
  • 如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:objc_setAssociatedObjectobjc_getAssociatedObject

getter 和 setter 的源码实现

getter

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // 如果传入的偏移量为 0 就把属性所属的类的实例对象返回
    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);
}

setter

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // 如果传入的偏移量为 0 就把值设置给属性所属的类的实例对象
    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);
}

MRC 下手动实现 getter 和 setter

getter

-(NSString *)object {
    return [[_object retain] autorelease];
}

setter

-(void)setObject:(NSObject *)object {
    if (_object != object) {
        [_object release];
        _object = [object retain];
    }
}

常见问题

什么情况下使用 weak 关键字,相比 assign 有什么不同?

  • 在 ARC 中,有可能出现循环引用的时候,往往要通过让其中一端的属性使用 weak 来解决,比如: delegate 代理属性

  • 自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;比如:xid 和 storyboard 中控件

  • weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。 而 assign 的“设置方法”只会执行针对“纯量类型” (scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。

  • assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。

怎么用 copy 关键字?

  • NSString、NSArray、NSDictionary 等等经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
  • block 也经常使用 copy 关键字,block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区,在 ARC 中写不写都行。

这个写法会出什么问题: @property (copy) NSMutableArray *array;

  • 两个问题:
  1. 添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃,因为 copy 就是复制一个不可变 NSArray 的对象;
  2. 使用了 atomic 属性会严重影响性能,并且无法保证多线程下的线程安全;

如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

  • 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
  • 实现 copyWithZone: 方法

@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

  • @property = ivar + getter + setter;
  • “属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。
  • 完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。这个过程由编译器在编译期执行
  • 编译后大致生成了五个东西:
    • OBJC_IVAR_类名属性名称 :该属性的“偏移量” (offset),这个偏移量是“硬编码” (hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。
    • setter 与 getter 方法对应的实现函数
    • ivar_list :成员变量列表
    • method_list :方法列表
    • prop_list :属性列表
  • 也就是说我们每次在增加一个属性,系统都会在 ivar_list 中添加一个成员变量的描述,在 method_list 中增加 setter 与 getter 方法的描述,在属性列表中增加一个属性的描述,然后计算该属性在对象中的偏移量,然后给出 setter 与 getter 方法对应的实现,在 setter 方法中从偏移量的位置开始赋值,在 getter 方法中从偏移量开始取值,为了能够读取正确字节数,系统对象偏移量的指针类型进行了类型强转.

runtime 如何实现 weak 属性

  • 对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为 0 的时候会 dealloc,假如 weak 指向的对象内存地址是 a,那么就会以 a 为键, 在这个 weak 表中搜索,找到所有以a为键的 weak 对象,从而设置为 nil。

weak 属性需要在 dealloc 中置 nil 么?

  • 不需要。在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理。

@synthesize和@dynamic分别有什么作用?

  • @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize 和 @dynamic 都没写,那么默认的就是 @syntheszie var = _var;
  • @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
  • @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter 方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

用 @property 声明的 NSString(或 NSArray,NSDictionary)经常使用 copy 关键字,为什么?如果改用 strong 关键字,可能造成什么问题?

  • 使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。
  • 如果我们使用是 strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.
  • 说白了,就是如果对有可变类型的属性设置了 strong ,再赋值给它一个可变类型的对象,修改该对象时也会修改属性保存的值。

@synthesize 合成实例变量的规则是什么?假如 property 名为 foo,存在一个名为 _foo 的实例变量,那么还会自动合成新变量么?

  • 如果指定了成员变量的名称,会生成一个指定的名称的成员变量。
  • 如果这个成员已经存在了就不再生成了。
  • 如果是 @synthesize foo; 还会生成一个名称为 foo 的成员变量,也就是说:如果没有指定成员变量的名称会自动生成一个属性同名的成员变量。
  • 不会合成。

在有了自动合成属性实例变量之后,@synthesize 还有哪些使用场景?

  • 当你在子类中重载了父类中的属性,你必须 使用 @synthesize 来手动合成 ivar。
  • 可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字。
  • 同时重写了 setter 和 getter 时,系统就不会生成 ivar,这时候有两种选择:
    • 手动创建 ivar
    • 使用@synthesize foo = _foo; ,关联 @property 与 ivar

使用atomic一定是线程安全的吗?

  • 对于 atomic 的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象,不会有脏数据。
  • 但是 atomic 可并不能保证线程安全。如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。
  • 给属性加上 atomic 修饰,可以保证属性的 setter 和 getter 都是原子性操作,也就是保证 setter 和 getter 内部是线程同步的
  • 它并不能保证使用属性的过程是线程安全的,比如 self.intA 是原子操作,但是 self.intA = self.intA + 1 这个表达式并不是原子操作。又比如对可变数组或字典的操作。