前语

在日常的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;

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

大话Objective-C属性的特性

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

  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

参阅链接

  • developer.apple.com/library/arc…
  • www.jianshu.com/p/035977d1b…