0%

property 中的常用关键字分析

栈与堆

首先,我们需要知道:为什么只有Objective-C对象需要进行内存管理,而其它非对象类型(如基本数据类型)不需要我们来管理呢?

因为: Objective-C 的对象在内存中是以堆的方式分配空间的

  • 堆里面的内存是动态分配的,所以也就需要程序员手动的去添加内存、回收内存
  • OC对象存放于堆里面(堆内存要程序员手动回收,也就是 release )
  • 非OC对象一般放在栈里面(栈内存会被系统自动回收)

例如:

1
2
3
int a = 10;
int b = 20;
Car *c = [[Car alloc] init];

我们知道OC对象一般都是指针形式,大神ibireme在 Objective-C 中的类和对象中提到:

凡是首地址是*isa的struct指针,都可以被认为是objc中的对象。运行时可以通过isa指针,查找到该对象是属于什么类(Class)。

下图可以清楚的表示 OC 中的堆栈分配:

assign 与 weak

基础知识提要

@property 声明的属性,会自动创建 getter,setter 方法。 属性中申明的关键字主要有三大类:

  • readwrite/readonly: 是否生成 setter 方法, 默认 readwrite
  • assign/weak/strong/copy/retain: setter 方法中如何传递所有权等内存管理策略, ARC 环境默认为 strong
  • nonatomic/atomic:是否加线程锁(并不能完全控制线程访问,最好事是自己手动底层加锁,默认 atomic

NSString 的引用计数LLVM是有优化的,并不是简单根据设定的关键字来判断。本文只是为了方便易懂,以这个类作为例子。

assign

如果设置 assign ,在 setter 方法中是这样的:

1
2
3
- (void)setCarName:(NSString *)name {
_carName = name; //默认是有下划线的
}

也就是说成员变量 _name 并没有获得 name 所指的对象的所有权,原来对象 @“Amywushu” 的 retainCount 不会改变。一旦 name 被释放,_carName 也就不再指向一个合法的位置,出现指针悬空,如下图所示,相当于 _carName (图中为 carName )指向的 name,但实际上 _carName 是指向OC对象的,这里只是为了方便理解。

assign/weak 的区别

ARC 中 OC 对象已经不再使用 assign 了,而是使用 weak ,两者的区别在: weak 弱引用所指的对象没有被任何strong指针指向,那么就将被销毁,所有指向这个对象的 weak 指针也将被置为 nil 。而 assign 不会被置为 nil 。但对于非 OC 对象来讲,因为其存储空间在栈上,由系统管理内存,所以一般还是使用 assign。

unsafe_unretained

unsafe_unretained 的语义与 assign 类似,相当于用于 OC 对象类型的 assign 。使用这个关键字主要出于性能考虑,因为 weak 对性能有一些影响,因此对性能要求高的地方可以考虑使用 unsafe_unretained 替换 weak 。比如 YYModel 的实现,为了追求更高的性能,其中大量使用 unsafe_unretained 作为变量标识符。

retain 与 strong

知识点:

MRR内存管理基本原则

  1. 为创建的所有对象设置所有权
  2. 应使用retain方法获取对象(你尚未拥有)的所有权
  3. 当不再使用某个对象时,必须放弃其所有权
  4. 不能放弃不归你所有的对象的所有权
1
2
3
4
5
6
7
8
9
10
11
12
//对象通过 alloc 消息创建后,变量 atom 就拥有了该对象的所有权(原则1)
Atom *atom = [[Atom alloc] init];
//变量 href 获取了这个对象的所有权(原则2),不能写成 Atom *href = atom;
//这样写的话 href 没有获取对象的所有权,一旦 atom 释放了,href 就不再指向一个合法的位置,出现指针悬空。
Atom *href = [atom retain];
// 变量 atom 释放,但 href 依旧拥有该对象的所有权
[atom release];
// 变量 href 释放,对象引用计数变为 0 ,运行时系统可以释放对象了
[href release];

——引用自[精通Objective-C]内存管理

如果设置 retain ,在 setter 方法中是这样的:

1
2
3
4
5
6
- (void)setCarName:(NSString *)name {
if (_carName != name) {
[_carName release];
_carName = [name retain];
}
}

也就是说,retain/strong 会在 setter 方法中,对传入的对象 "Amywushu" 进行引用计数 +1 的操作。简单来说,就是会拥有传入对象 "Amywushu" 的所有权,而不是像 assign/weak 一样,依赖于传入的对象指针 name ,而并非拥有实际所有权。

相当于一个保险柜拥有两把钥匙,变量解除所有权 (release) ,也就相当于归还钥匙。当两把钥匙都被归还之后,这个保险柜(对象)也就会被释放。只要拥有该对象的所有权(至少有一把钥匙没有归还),这个对象就不会被释放。

如图所示,_carNamename 均拥有对 Amywushu 这个字符串对象的所有权,该对象的引用计数变为 1+1=2 。

strong 是在 iOS 引入 ARC 的时候引入的关键字,是retain的一个可选的替代。 strong 跟 retain 的意思相同并产生相同的代码,但是语意上更好更能体现对象的关系。

关于 strong 和 weak 还有什么不清楚的话,可以学习 onevcat 的手把手教你ARC——iOS/Mac开发ARC入门和使用这篇文章,讲得非常清晰易懂。

copy 与 mutableCopy

copy 、mutableCopy 与 strong 的区别在于,深拷贝时,示例变量对于传入对象的副本拥有所有权,而不是对象本身;浅拷贝时,则没有区别。
如果设置 copy ,在 setter 方法中是这样的:

1
2
3
4
5
6
- (void)setCarName:(NSString *)name {
if (_carName != name) {
[_carName release];
_carName = [name copy];
}
}

如上图所示,原来对象的 retainCount 不变,新 copy 出来的对象副本的 retainCount=1 ,原对象引用计数不变,两者 copy 之后互不相关,这是深拷贝。

  • 深拷贝与浅拷贝

深拷贝: 是对内存空间的拷贝,也就是这里的 copy(数组等类型例外)
浅拷贝: 是对内存地址的拷贝,也就是上面的 retain/strong,以及 copy 的某些情况
深拷贝和浅拷贝比较复杂,我的另一篇文章有具体讲解copy 与 mutableCopy(传说中的深浅拷贝,这里只是简要提及一下。

注意! 如果是 copy 的是一个 NSArray 呢? 比如:

1
2
NSArray *array = [NSArray arrayWithObjects:@"hello",@"world",@"baby"];
NSArray *array2 = [array copy];

这个时候,系统的确是为 array2 开辟了一块内存空间,但是我们要知道的是, array2 中的每个元素,,只是 copy 了指向 array 中相对应元素的指针,这是“单层深拷贝”.


一般来说,不要将 copy 用到 NSMutableString ,NSMutableArray ,NSMutableDictionary 等可变对象上,除非有特别的需求。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface ViewController ()
@property (nonatomic, copy) NSMutableArray *mutableArray_copy;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *mutableArray = [NSMutableArray arrayWithObject:@"123"];
self.mutableArray_copy = mutableArray;
[self.mutableArray_copy addObject:@"456"];
}

运行则会报出错误 signal SIGABRT

我们明明在代码里用的 NSMutableArray 这个可变数组,为什么错误里会说是在向 NSArray 这个不可变数组,调用 addObject: 这个方法呢?( __NSArrayI 表示的是 NSArray 类型)。
是因为在 self.mutableArray_copy = mutableArray; 这一句的时候,会调用 mutableArray_copy 的 setter 方法, copy 属性默认 setter 方法是这样写的:

1
2
3
- (void)setMutableArray_copy:(NSMutableArray *)mutableArray_copy {
_mutableArray_copy = [mutableArray_copy copy];
}

setter 方法里调用的是 copy,而不是 mutableCopy ,也就是说拷贝过来的是不可变的 NSArray 类型。那么 NSMutableArray 的添加元素等方法自然就不能使用了。
解决方法为:将 copy 改为 strong ;或者重写 setter 方法,将 copy 改为 mutableCopy 。

copy 现在都比较少用,一般用于 NSString 。因为父类指针可以指向子类对象,NSMutableNSString 是 NSString 的子类,使用 strong 的话虽然 NSString 是不可变对象,但是它传入的值可能会是 NSMutableString 可变对象,如果这个可变对象的内容在其他地方被修改了,那 NSString 指针所指的对象也随之改变了,而其本身可能对此毫不知情。因此一般用 copy 。

nonull nullable null_resettable

这三个属性关键字是 WWDC2015 中介绍的 OC 新特性,与 Swift 中的 ? 和 ! 类似。

  • nonull:该属性不能为 nil ,必须有值。
  • nullable:表示可选的,可以为 nil。
  • null_resettable:表示 setter 方法是 nullable ,可以为 nil;而 getter 方法是nonull ,必须有值。

总结

总的来说,就是内存处理方式不一样,assign/weak 相当于借用传入的指针变量来指向对象(实际上并不是,可以这样理解),retain/strong 相当于不同的指针变量指向同一个对象,copy 则是在内存里复制了一个对象并指向它。读写属性以及原子性比较简单,不再赘述。

关于 atomic 的锁机制,以及 ARC 机制到底如何进行的内存管理,之后会进一步学习。敬请期待。

2016.12.07 补充:

新增博文: ARC 是如何进行内存管理的


Reference

[1] Objective-C 内存管理——你需要知道的一切 https://segmentfault.com/a/1190000004943276
[2] @property属性关键字详解 http://www.wugaojun.com/blog/2015/07/25/at-propertyshu-xing-guan-jian-zi-xiang-jie/
[3] Objective-C 的自动引用计数(ARC) https://hran.me/archives/objective-c-automatic-reference-counting.html
[4] Objective-c 内存管理的历史和参考资料 http://www.pchou.info/ios/2015/06/05/oc-memory-management.html

----------------------END END----------------------