聊聊那些iOS内存管理的关键字

前言

这篇博文可能是在理解了iOS的内存管理机制的童鞋看起来会好懂一点。过后如果有时间,我也会更新写一写我对iOS的ARC以及MRC的理解。在平时的代码里,我们可能会经常看到,类似于:

1
@property (weak, nonatomic) UIButton *button;

我们定义的属性,它所具有的参数,像 weak,strong,nonaomic 这些,各自有什么样的含义呢?请点开全文吧~

从引用计数器开始谈起

我们知道在程序运行过程中要创建大量的对象,和其他高级语言类似,在 ObjC 中对象时存储在堆中的,系统并不会自动释放堆中的内存(注意基本类型是由系统自己管理的,放在栈上)。

如果一个对象创建并使用后没有得到及时释放那么就会占用大量内存。其他高级语言如C#、Java都是通过垃圾回收来(GC)解决这个问题的,但在 OjbC 中并没有类似的垃圾回收机制,因此它的内存管理就需要由开发人员手动维护。

那么ObjC中内存又是如何管理的呢?

其实在ObjC中内存的管理是 依赖对象引用计数器 来进行的:在 ObjC 中每个对象内部都有一个与之对应的 整数retainCount),叫“引用计数器”,当一个对象在创建之后它的引用计数器为1,当调用这个对象的 allocretainnewcopy 方法之后引用计数器自动在原来的基础上加1(ObjC中调用一个对象的方法就是给这个对象发送一个消息),当调用这个对象的 release 方法之后它的引用计数器减1,如果一个对象的引用计数器为0,则系统会自动调用这个对象的 dealloc 方法来销毁这个对象。

在 Apple 官方文档里面有这样的一张介绍 MRC 与 ARC 的区别:

之所以我们平时再写代码是不用花精力去考虑对象的内存管理,是因为
ObjC 中提供了两种内存管理机制 MRC(MannulReference Counting)和 ARC(Automatic Reference Counting),分别提供对内存的手动和自动管理,来满足不同的需求。在Xcode 4.1及其以后版本支持了 ARC,现在我们也是基本都用 ARC 来做内存管理的。

但是,如果想好好理解学习 ObjC 的内存管理,建议还是从 MRC 的知识慢慢慢学。

MRC 模式下的关键字

按照上面的思想,我们在 MRC 的模式下面管理对象,就需要在对象的 setter 方法里去对对象做 release 和 retain 操作了。

保证每次属性赋值的时候对象引用计数器+1,这样一来调用过 getter 方法可以保对象不会被提前释放,其次为了保证上一次的赋值对象能够正常释放,我们在赋新值之前对原有的值进行 release 操作。

这显然是一件比较烦人的事情,ObjC 虽说是一门比较古老的语言,但也不至于不人性化到这种地步,前言提及的 @property 属性就是来解决这一问题的。

1
@property (retain) NSObject *a;

加上 retain 参数,我们不必手动实现car的getter、setter方法程序仍然没有内存泄露。那么,除了 retain ,还有其他什么参数?它们又有什么作用呢?

先进行一下总结,大致如下表:

PS:表中所提及的默认值,并非 Xcode 为我们预输入的(Xcode 是会根据我们从storyborad 链接过来的控件为我们预输入3个属性)默认值是如果我们将它置空,它的值。置其中一类参数,程序会使用三类中的各个默认参数,默认参数:(atomic,readwrite,assign)

其中,

  • assign,用于基本数据类型
  • retain,通常用于非字符串对象
  • copy,通常用于字符串对象、block、NSArray、NSDictionary

ARC 模式下关键字

ARC 的推出,其实就是为了让我们可以专注于代码的业务逻辑而不用去理会我们使用的对象内存管理,因为 ARC 都已经为我们做好了。

ARCMRC 的基础之上,增加了 strongweakunsafe_unretainedautoreleasing 等属性值。

我在官方文档找到以下的解释:

__strong is the default. An object remains “alive” as long as there is a strong pointer to it.

__weak specifies a reference that does not keep the referenced object alive. A weak reference is set to nil when there are no strong references to the object.

__unsafe_unretained specifies a reference that does not keep the referenced object alive and is not set to nil when there are no strong references to the object. If the object it references is deallocated, the pointer is left dangling.

__autoreleasing is used to denote arguments that are passed by reference (id *) and are autoreleased on return.

我们使用的比较多的显然是 strongweak

strong 相当于 MRC 中的 retain,

1
2
 // The following declaration is a synonym for: @property(retain) MyClass *myObject;
@property(strong) MyClass *myObject;

weak 相当于 MRC 中的 assign

1
2
// The following declaration is similar to "@property(assign) MyClass *myObject;"
@property(weak) MyClass *myObject;

copy 的使用同于 MRC.

所以在 ARC 模式下,默认的属性是:

  • 对于非 NSObject 对象:(atomic,readwrite,assign)
  • 对 NSObject 对象:(atomic,readwrite,strong)

根据官方文档的解释:

strong 用来修饰引用的属性,一块内存(一个对象)当没有 strong 类型的指针指向它时,它就会被释放。

weak 用来修饰引用的属性,当一块内存(一个对象)被释放时,指向它的 weak 类型指针就会被释放并赋值为 nil。

浅显一点解释呢,就是:

如果将我们的对象比喻成一只狗的话,strong 类型的指针就像一条条狗链,只有当最后一条狗链断了,狗才可能走掉(也就是内存的释放)。而 weak 指针这是一个监视狗的监控摄像头,也可以看到狗的动态,但是不能强控着狗,当狗走了的时候,只能“目送”,然后默默关闭(相当于赋值nil),无能为力。

我自己的一些疑惑

copy 与 retain

开始写 ObjC 的时候,就看书或网上的博客会说:retain(strong),通常用于非字符串对象; copy,通常用于字符串对象、block、NSArray、NSDictionary.

一开始也没有多想,把他当成是一个约定俗成的的规则,一直就按着这样来写了,今天整理这个文章的时候,还是思考了一下,看了下书,把想法很大家分享一下吧~

自己写了个小例子:

1
2
3
//  .h文件
@property (retain,nonatomic) NSString *retainStr;
@property (copy, nonatomic) NSString *copyStr;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  .m文件
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableString *str = [[NSMutableString alloc] initWithString:@"abc"];

self.retainStr = str;
self.copyStr = str;

NSLog(@"mStr:%p,%p",str,&str);
NSLog(@"retainStr:%p,%p", _retainStr, &_retainStr);
NSLog(@"copyStr:%p,%p", _copyStr, &_copyStr);

}

输出的结果:

从结果来分析:

str 对象的地址 为0x79e6f050,也就是0x79e6f050是@“abc”的首地址,str变量自身在内存中的地址为0xbff7cf7c;

当把str赋值给 retain 的retainStr时,retainStr对象的地址 为0x79e6f050,retainStr变量自身在内存中的地址 为0x79e6c8dc;retainStr与str指向同样的地址,他们指向的是同一个对象@“abc”,这个对象的地址为0x79e6f050,所以他们的值是一样的。

当把str赋值给 copy 的copyStr时,copyStr对象的地址为0x79e6f0d0,copyStr变量自身在内存中的地址0x79e6c8e0;copyStr与str指向的地址是不一样的,他们指向的是不同的对象,所以copy是深复制,一个新的对象,这个对象的地址为0x22,值为@“abc”。

补充上述的代码:

1
2
3
[str appendString:@"de"];
NSLog(@"retainStr:%@", _retainStr);
NSLog(@"copyStr:%@", _copyStr);

得到一下结果:

那如果换成下面的代码:

1
[self.copyStr appendString:@"de"];

编译器器会报错:

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘Attempt to mutate immutable object with appendString:’

即使我们把原先定义的关于 copyStr 的属性从 NSString 变为 NSMutableString

1
@property (copy, nonatomic)  NSMutableString *copyStr;

还是会出现一样的错误。

MRC,用 retain 属性就没有问题:

1
[self.retainStr appendString:@"de"];

而当我们把 str 的类型替换为 NSString 时:

1
NSString *str = [[NSString alloc] initWithString:@"abc"];

得到结果如下:

当类型为NSString 时,类似于C++, 相同内容的字符串在内存上是有着相同的地址的。copyretain 对它的作用都是浅复制,也就只是单纯地指针复制。

总结一下:

  • retain:始终是浅复制。引用计数每次加一。返回对象是否可变与被复制的对象保持一致。

  • copy:对于可变对象为深复制,引用计数不改变;对于不可变对象是浅复制,引用计数每次加一。始终返回一个不可变对象(上面的报错就是修改了不可变对象)。

PS:如果需要深复制得到一个可变参数,可以使用 mutableCopy

  • mutableCopy 始终是深复制,返回可变对象,引用计数不变。

所以,如果一般情况下,我们都不希望字串的值跟着str变化,所以我们一般用 copy 来设置string的属性。
如果希望字串的值跟着赋值的字串的值变化,可以使用 strong,retain

当然这也只是针对 NSMutableString,因为如果是 NSString 那么 copyretain 的效果是一样的。

最后的话

原来写完已经一点半了,昏昏沉沉的,怕是有挺多的错别字,希望不要介意,如果有什么关于这个问题的,欢迎通过邮件来进行讨论~晚安~