大话Objective-C属性的特性

2,369 阅读11分钟

前言

在日常的iOS开发中,只要是使用Objective-C进行开发,就绕不开属性。而对于属性,其又拥有一系列的特性。据本人的经验以及在工作中的观察发现,iOS研发的新人对如何给一个属性设置合适的特性,缺乏足够的认知。于是,撰写本文,旨在帮助iOS研发的新人学习Objective-C属性的特性。

为了确保本文中文的翻译能一一对应上官方术语,这里将本文出现的术语做成表格,方便读者对照:

中文英文
属性property
特性attribute
存取器accessor

本文环境:

  1. ARC (为了不影响新人对这一块的学习,全文不会出现任何MRC的特性)
  2. Xcode 11.0 +

本文受众:

  1. iOS研发的新人 (需要有一定的Objective-C基础)
  2. 对Objective-C属性的特性缺乏足够认知的同学

(本文讲解的难度较低,仅会蜻蜓点水般讲解一些粗浅的原理)

简单认识属性(property)

@interface FooClass : NSObject

@property id foo;

@end

如上所示,我们知道,当我们在一个类里面,简单声明一个属性时,编译器实际上帮我们干了两件事:

  1. 生成实例变量
  2. 生成getter和setter方法

即,等价于如下定义:

@interface FooClass : NSObject {
@private id _foo;
}

- (id)foo;
- (void)setFoo:(id)foo;

@end

而属性中的特性,就是告诉编译器,如何生成实例变量和getter/setter方法。

通常情况下,我们会使用点方法来读写属性:

FooClass *foo = [FooClass new];
foo.foo = @"foo";
id obj = foo.foo;

实际上,我们可以将其理解为getter/setter方法的“语法糖”,即我们可以理解为编译器在预编译阶段会将点语法进行展开成getter/setter方法:

FooClass *foo = [FooClass new];
[foo setFoo:@"foo"];
id obj = [foo foo];

那什么时候会展开成getter方法,什么时候又会展开成setter方法呢?这里可以记住一个诀窍:属性点方法的调用在赋值符号(等号,=)左边的场景会展开成setter方法,其他场景一律展开成getter方法。

属性中的特性(attribute)

在Objective-C项目中,我们经常能看到以下写法:

@interface FooClass : NSObject

@property (nonatomic, strong) id obj;
@property (nonatomic, weak) id delegate;
@property (nonatomic, assign) BOOL flag;

@end

不难知道,属性标识符后面括号里面的关键字,即为描述属性的特性。那么问题来了,这些特性关键字具体有什么含义?又有哪些特性?先按下不表,这里我们先给出定义属性的语法:

@property (attributes) type name;

然后,我们给特性,划分出不同的类型:

image.png

如上图所示,特性一共有五大类型:

  1. 原子性 Atomicity
  2. 读写性 Writability
  3. 存储语义 Setter Semantics
  4. 存取器方法名 Accessor Method Names
  5. 空值性 Nullability

原子性 Atomicity

描述属性原子性的特性一共有两个关键字:

  1. atomic 表示这个属性的getter/setter方法是原子性的,即在多线程下访问该属性对应的getter/setter方法是原子性的(即确保多线程调用getter/setter方法的安全)
  2. nonatomic 表示这个属性的getter/setter方法是非原子性的

初学者对于这两者的概念可能有点绕,这里我们举个例子:

@interface FooClass : NSObject

@property (atomic) id fooA;
@property (nonatomic) id fooN;

@end

在这里例子中,我们分别定义了一个atomicnonatomic的属性。上面讲解过,特性是用来告诉编译器如何生成实例变量和对应getter和setter方法的。我们这里做一次等价转化:

@interface FooClass : NSObject {
@private id _fooA;
@private id _fooN;
}

- (id)fooA;
- (void)setFooA:(id)fooA;

- (id)fooN;
- (void)setFooN:(id)fooN;

@end

@implementation FooClass

- (id)fooA {
    @synchronized (self) {
        return _fooA;
    }
}
- (void)setFooA:(id)fooA {
    @synchronized (self) {
        return _fooA = fooA;
    }
}

- (id)fooN {
    return _fooN;
}
- (void)setFooN:(id)fooN {
    _fooN = fooN;
}

@end

不难看出,atomic属性就是在nonatomic属性的getter/setter方法的基础上,做了个加锁(递归锁)的操作。这样能确保,同一时刻只有一条线程能调用属性的getter/setter方法。那么问题来了,atomic属性就是线程安全的吗?为什么在工程实践中,极少有属性的原子性特性为atomic?

首先,atomic属性并不是线程安全的,或者说它只保证了getter/setter方法的“线程安全”。还是举个例子:

@interface FooClass : NSObject

@property (atomic) NSMutableArray *array;

@end


// foo thread
FooClass *foo = [FooClass new];
foo.array = [NSMutableArray array];

// a thread
[foo.array addObject:@"a"];

// b thread
[foo.array addObject:@"b"];

// multi-thread
[foo.array addObject:@"..."];

我们在多线程场景中,往foo对象的array添加对象,此时大概率会发生crash。虽然,array属性是atomic的,而且在上面这个例子中,我们在调用array属性的getter方法时,也保证了原子性,但是我们调用添加对象的方法addObject:时,addObject:方法并不是线程安全的,所以只要同时有两条线程访问了addObject:方法,就大概率会发生crash。在我们的工程实践中,我们往往是想确保一系列的方法调用是线程安全的,而不是仅仅确保某个getter和setter方法是线程安全的

其次,atomic属性的getter/setter方法因为有加锁的操作,故效率远远比不上nonatomic属性的getter/setter方法。所以在工程实践中,我们往往倾向于去声明一个nonatomic属性。

读写性 Writability

描述属性读写性的特性一共有两个关键字:

  • readwrite 可读写
  • readonly 只读

同样的思路,编译器想实现属性是readonly的,它只需要生成getter方法而不生成setter方法即可:

// Normal Statement
@interface FooClass : NSObject

@property (readwrite) id fooRW;
@property (readonly) id fooRO;

@end





// Equivalence Statement
@interface FooClass : NSObject {
@private id _fooRW;
@private id _fooRO;
}

- (id)fooRW;
- (void)setFooRW:(id)fooRW;

- (id)fooRO;

@end

存储语义 Setter Semantics

描述属性存储语义的特性一共有四个关键字:

  • strong 强引用
  • weak 弱引用
  • assign 仅赋值
  • copy 拷贝

我们知道,在ARC下,Objective-C对象指针存在强指针和弱指针,即:

__strong id strongPtr;
__weak id weakPtr;

而与此对应的,strongweak特性,指的就是当声明一个Objective-C对象属性时,实例变量指针是强指针还是弱指针

// Normal Statement
@interface FooClass : NSObject

@property (strong) id fooStrong;
@property (weak) id fooWeak;

@end





// Equivalence Statement
@interface FooClass : NSObject {
@private __strong id _fooStrong;
@private __weak id _fooWeak;
}

- (id)fooStrong;
- (void)setFooStrong:(id)fooStrong;

- (id)fooWeak;
- (void)setFooWeak:(id)fooWeak;

@end

不难看出,strongweak特性也只能用来声明Objective-C对象属性,否则编译器将报错无法通过编译。

而除了Objective-C对象属性之外,我们还能声明基础数据类型属性,此时assign特性就派上用场了:

// Normal Statement
@interface FooClass : NSObject

@property (assign) NSInteger fooInt;

@end





// Equivalence Statement
@interface FooClass : NSObject {
@private NSInteger _fooInt;
}

- (id)fooInt;
- (void)setFooInt:(NSInteger)fooInt;

@end

换而言之,assign特性对引用计数不会有任何影响,它生成的实例变量类型的等同于基础数据类型。所以,assign特性其实也能用来声明Objective-C对象属性,但是由于调用其对应的setter方法时,仅仅是对指针进行一次赋值,故极有可能原对象释放时,该属性对应的实例变量指针成为野指针,而当再次访问指针的内容时,极易发生crash。(这一块涉及ARC与MRC的知识,如若无法理解可以视为assign特性就是用来描述基础数据类型的属性)

那问题来了copy特性有啥用?其他特性用来描述生成的实例变量的类型,似乎已经覆盖了所有的可能性了。其实,copy特性生成的实例变量的类型与strong特性一致,故其也只能用来描述声明Objective-C对象属性。但是与strong特性不同的的是,两者的setter方法不一样:

// Normal Statement
@interface FooClass : NSObject

@property (nonatomic, strong) id fooStrong;
@property (nonatomic, copy) id fooCopy;

@end





// Equivalence Statement
@interface FooClass : NSObject {
@private __strong id _fooStrong;
@private __strong id _fooCopy;
}

- (id)fooStrong;
- (void)setFooStrong:(id)fooStrong;

- (id)fooCopy;
- (void)setFooCopy:(id)fooCopy;

@end

@implementation FooClass

- (void)setFooStrong:(id)fooStrong {
    _fooStrong = fooStrong;
}

- (void)setFooCopy:(id)fooCopy {
    _fooCopy = [fooCopy copy];
}

@end

可以看到,copy特性的setter方法是调用入参对象的copy方法。这时,我们就能知道,为什么工程中,NSStringNSArrayNSDictionaryNSSet等不可变的容器类属性习惯使用copy特性,就是为了确保当setter方法传入的是一个可变容器类对象时,通过调用copy方法将对象固化成不可变容器类:

@interface FooClass : NSObject

@property (copy) NSString *str;

@end

// some function
NSMutableString *str = [NSMutableString stringWithString:@"str"];
FooClass *foo = [FooClass new];
foo.str = str;
[str appendString:@"str"];
NSLog(@"%@", foo.str);    // Output ``str``

存取器方法名 Accessor Method Names

(存取器可能有点不太好理解😝😝😝,实际上就是getter/setter方法)

描述属性存取器方法名的特性一共有两个关键字:

  • getter getter方法名
  • setter setter方法名

我们知道,当我们简单声明一个属性时,编译器会自动生成getter/setter方法,命名规范为:

// property statement
@property type name;

// getter method
- (type)name;
// setter method
- (void)setName:(type)name;

可是,我们有些时候需要去修改getter/setter方法的名字,这个时候就可以用上getter和setter特性了:

// Normal Statement
@interface FooClass : NSObject

@property (getter=isFlag, setter=changeFlag:) BOOL flag;

@end





// Equivalence Statement
@interface FooClass : NSObject {
@private BOOL _flag;
}

- (BOOL)isFlag;
- (void)changeFlag:(BOOL)flag;

@end

(需要注意的是,setter特性后跟着的setter方法名要带上:)

通常,我们不需要改变存取器方法名,如若需要改变,尽量遵守KVC的命名规范(Key-Value Coding Programming Guide)。

空值性 Nullability

描述属性空值性的特性一共有四个(在兼容Swift上有重要意义):

  • null_unspecified 不确定是否为空 (🤣🤣🤣确实是这个意思)
  • nullable 可空
  • nonnull 不可为空
  • null_resettable getter不为空,setter可以为空

null_unspecified对于非Swift开发者来说难以理解,这里可以直接视为等同nullable。同时需要注意的是,基本数据类型是不具备空值性特性的,但是基本数据类型的指针确实可以拥有空值性特性。不过,Objective-C实例对象我们也是用的指针类型,故其实可以认为只有指针类型才具备空值性特性

照例,我们给个示例:

// Normal Statement
@interface FooClass : NSObject

@property (null_unspecified) id fooUnspecified;
@property (nullable) id fooNull;
@property (nonnull) id fooNonNull;
@property (null_resettable) id fooResettable;

@end





// Equivalence Statement
@interface FooClass : NSObject {
@private id _fooUnspecified;
@private id _fooNull;
@private id _fooNonNull;
@private id _fooResettable;
}

- (id _Null_unspecified)fooUnspecified;
- (void)setFooUnspecified:(id _Null_unspecified)fooUnspecified;

- (id _Nullable)fooNull;
- (void)setFooNull:(id _Nullable)fooNull;

- (id _Nonnull)fooNonNull;
- (void)setFooNonNull:(id _Nonnull)fooNonNull;

- (id _Nonnull)fooResettable;
- (void)setFooResettable:(id _Nullable)fooResettable;

@end

实际上,不遵守空值性特性,调用setter方法传入不符合预期的对象时,一般也不会导致编译错误,但是可能会收获Clang的警告(往往实际工程项目中会打开warning as error的编译选项,此时如若不遵守空值性特性就会编译不通过)

而我们在项目中,往往能见到.h文件中有这么一对宏:

NS_ASSUME_NONNULL_BEGIN

@interface FooClass : NSObject

// balabala
@property (nonatomic) id foo;
// balabala

@end

NS_ASSUME_NONNULL_END

这里不对此做过多的讲解,只需要知道,在这对宏之间没有显式声明空值性特性的属性,空值性特性都为nonnull

至此,所有Objective-C属性的特性都讲解完了。

默认特性

对于没有显式地写特性的属性,编译器会给他们附上默认的特性:

@interface FooClass : NSObject

@property (
           atomic,
           readwrite,
           strong,
           getter=nsObj, setter=setNsObj:,
           null_unspecified
           ) id nsObj;
           
@property (
           atomic,
           readwrite,
           assign,
           getter=cStruct, setter=setCStruct:,
           ) NSInteger cStruct;

@end

特性间的关系

往往,我们在声明一个属性时,附带的特性就不止一个。而往往并不是什么特性都能附加到属性上的。

共存

在同一次属性声明中,可以声明不同类型的特性。

互斥

  • 同为原子性、读写性、存储语义或空值性类型的特性间互斥,即在一次属性声明中,已经声明了一个类型的特性后,无法再次声明相同类型的特性。
  • 当一个类的属性的读写性特性为readonly时,允许在类扩展(extension)中再次声明属性,并显式将读写性特性声明为readwrite
  • 当一个属性的读写性特性为readonly时,在同一次属性声明中,存取器方法名setter特性会失效,且空值性的null_resettable特性等同于nullable特性。
  • 当一个属性的存储语义特性为weak时,空值性特性不能声明为nonnull

重写存取器

在实际工程中,重写getter/setter方法的现象很常见,但是如何正确的重写getter/setter方法却缺乏足够的认知。因为重写getter/setter方法,往往会破坏属性所遵守的特性,当调用方对具体实现不甚了解时,往往会陷入重写者制造的坑中。这里给出常见的重写属性getter/setter方法的例子:

@interface FooClass : NSObject

@property (atomic) id fooAtomic;
@property (nonatomic, nonnull) id fooNonnull;
@property (nonatomic, copy) id fooCopy;
@property (nonatomic, weak) id fooWeak;
@property (nonatomic, setter=isFlag) BOOL flag;

@end

@implementation FooClass

// fooAtomic getter&setter
- (id)fooAtomic {
    @synchronized (self) {
        return _fooAtomic;
    }
}
- (void)setFooAtomic:(id)fooAtomic {
    @synchronized (self) {
        _fooAtomic = fooAtomic;
    }
}

// fooNonnull getter&setter
- (id)fooNonnull {
    // here the developer need to ensure that _fooNonnull must not be nil
    NSAssert(_fooNonnull, @"fooNonnull must not be nil!");
    return _fooNonnull;
}
- (void)setFooNonnull:(id)fooNonnull {
    NSAssert(fooNonnull, @"fooNonnull must not be nil!");
    _fooNonnull = fooNonnull;
}

// fooCopy setter
- (void)setFooCopy:(id)fooCopy {
    _fooCopy = [fooCopy copy];
}

// fooWeak setter
- (void)setFooWeak:(id)fooWeak {
    _fooWeak = fooWeak;
}

// flag getter
- (BOOL)isFlag {
    return _flag;
}

@end

参考链接