一、定时器的循环引入
二、copy、mutableCopy
三、创建大量autorelease对象时,最好自己创建一个@autoreleasepool {}
四、其他注意事项
一、定时器的循环引入
我们以NSTimer
举例,CADisplayLink
遇到同样的问题,解决方案也一样。
1.NSTimer
的循环引入
使用NSTimer
,写法通常如下:
1 |
|
1 |
|
运行代码,点击ViewController
进入ViewController1
,此时timer
跑起来了,每隔1秒打印一次“11”。此时点击返回按钮,返回ViewController
,正常情况下ViewController1
应该会销毁,并触发dealloc
方法,timer
也跟着失效并且销毁。但实际情况却是ViewController1
没有销毁,也没有触发dealloc
方法,timer
还一直跑着,这是因为timer
和ViewController1
形成了循环引用,导致内存泄漏。
查看timer
的创建方法,可以知道:**timer
会强引用target
,**也就是说timer
确实强引用着ViewController1
。
而ViewController
又强引用着timer
。
那怎么打破NSTimer
的循环引用呢?我们知道__weak
是专门用来打破循环引用的,那它是不是也能打破NSTimer
的循环引用?
1 | - (void)viewDidLoad { |
运行,发现没有效果,那为什么__weak
不能打破NSTimer
的循环引用?毫无疑问__weak
的确是把self
搞成了弱指针,但因为NSTimer
内部有一个强指针类型的target
变量
1 | @property (nonatomic, strong) id target; |
来接收这个传进来的地址值,所以无论你外界是传进来强指针还是弱指针,它内部都是一个强指针接收,就总是会强引用target
,所以用__weak
不能打破NSTimer
的循环引用。
那再试试另一条引用线吧,让ViewController1
弱引用timer
。
1 | @interface ViewController1 () |
运行,发现没有效果,奇了怪了,怎么回事呢?查看官方对NSTimer
的说明,可以知道:把timer
添加到RunLoop之后,RunLoop会强引用timer
,并且建议我们不必自己强引用timer
,而解除RunLoop对timer
强引用的唯一方式就是调用timer
的invalidate
方法使timer
失效从而销毁。
也就是说,实际的引用关系如下:
所以我们使用weak
修饰timer
是正确的,但这还是不能打破NSTimer
的循环引用——更准确地说,这可以解决NSTimer
的循环引用,但还是没有解决NSTimer
内存泄漏的问题。因为[self.timer invalidate]
的调用——即timer
的销毁——最好就是发生在ViewController1
销毁时,而ViewController1
要想销毁就必须得timer
先销毁,还是内存泄漏。
倒腾来倒腾去,还是得从timer
强引用target
这条引用线下手,把它搞成弱引用,__weak
不起作用,那我们想想别的方案呗。
2、打破NSTimer
的循环引用
- 方案一:使用block的方式创建
timer
1 | - (void)viewDidLoad { |
为什么能解决呢?因为此时timer
是强引用block的,而__weak
可以打破block的循环引用,所以block是弱引用self
的,所以最终的效果就类似于timer
弱引用self
。解决是能解决,但用这种方式创建timer
要iOS10.0以后才能使用。
方案二:创建一个中间对象——代理
我们可以把方案一的思路自己实现一下嘛,即创建一个中间对象(方案一的中间对象就是block嘛),把这个中间对象作为
timer
的target
参数传进去,让timer
强引用这个中间对象,而让这个中间对象弱引用ViewController1
,这样ViewController1
就能正常释放,NSTimer
就能正常调用失效方法,RunLoop就能正常解除对NSTimer
的强引用,NSTimer
就能正常解除对中间对象的强引用,内存泄漏就解决了。当然由于中间对象没有target
——即ViewController1
——的方法,所以我们还要做一步消息转发。
1 | -----------INETimerProxy.h----------- |
1 | -----------ViewController1.m----------- |
为了提高消息转发效率,我们可以让代理直接继承自NSProxy
,而不是NSObject
。**NSProxy
是专门用来做消息转发的,继承自NSObject
的类调用方法时会走方法查找 –> 动态方法解析 –> 直接消息转发、完整消息转发这套流程,而继承自NSProxy
的类调用方法时只会走方法查找 –> 完整消息转发这两个流程,消息转发效率更高,所以以后但凡要做消息转发就直接继承自NSProxy
好了,而不是NSObject
。**
1 | -----------INETimerProxy.h----------- |
二、copy
、mutableCopy
1、深拷贝与浅拷贝
- 深拷贝,是指内容拷贝,会产生新的对象,新对象的引用计数为1;浅拷贝,是指指针拷贝,不会产生新的对象,旧对象的引用计数加1,浅拷贝其实就是
retain
,深拷贝的话新对象和旧对象互不影响,浅拷贝的话改变一个另一个也跟着变了。- 只有不可变对象的不可变拷贝是浅拷贝,其它的都是深拷贝。
1 | - (void)viewDidLoad { |
2、不可变属性最好用copy
修饰,而可变属性坚决不能用copy
修饰、只能用strong
、retain
修饰
copy
拷贝出来的东西是不可变对象,是不能修改的;
mutableCopy
拷贝出来的东西是可变对象,是能修改的。
不可变对象最好用
copy
修饰不可变对象最好用
copy
修饰,因为用strong
、retain
修饰的话,setter
方法内部仅仅是retain
,那当我们把一个可变对象赋值给这个不可变属性时,不可变属性仅仅是指针指向了可变对象,修改可变对象的值,也就是不可变属性指向的对象值发生了改变,这不是我们所希望的结果,我们直观的感觉应该是“不可变属性指向的对象不应该随着别人的改变而改变”。
1 | @interface ViewController () |
而用copy
修饰的话,setter
方法内部就是copy
,那不管你外界传给它一个可变还是不可变对象,该属性最终都是深拷贝出一份不可变的,这样外界就无法影响这个属性的值,除非我们主动修改,符合我们的预期。
1 | @interface ViewController () |
而可变属性坚决不能用
copy
修饰、只能用strong
或retain
修饰可变属性坚决不能用
copy
修饰,只能用strong
、retain
修饰,道理和上面一样,copy
修饰的属性最终在setter
方法里copy
出一份不可变的,如果你非要用它来修饰可变属性,那从外在看来好像可以改变这个属性,结果一修改就崩溃了,因为找不到方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@interface ViewController ()
@property (nonatomic, copy) NSMutableString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = [@"张三" mutableCopy];
[self.name appendString:@"丰"]; // 一修改,就崩溃,因为NSString根本没有appendString:方法
}
@end
三、创建大量autorelease
对象时,最好自己创建一个@autoreleasepool {...}
只要不是用alloc
、new
、copy
、mutableCopy
方法创建的对象,而是用类方法创建的对象,方法内部都调用了autorelease
,都是autorelease
对象,例如:
1 | NSString *str = [NSString string]; |
因为类方法的内部实现大概如下:
1 | - (id)object { |
而alloc
、new
、copy
、mutableCopy
方法的内部实现大概如下:所以在创建大量autorelease
对象时,最好自己创建一个@autoreleasepool {...}
。
1 | - (id)allocObject { |
所以在创建大量autorelease
对象时,最好自己创建一个@autoreleasepool {...}
。因为如果主线程RunLoop的某次循环一直忙着处理事情,线程没有休眠或者退出,那本次循环的autoreleasepool
就迟迟无法销毁,这就会导致这次循环里的autorelease
对象迟迟无法释放掉,因此就很有可能会导致内存的使用峰值过高,从而导致内存溢出。而自己创建@autoreleasepool {...}
后,每一次for
循环都会出一次@autoreleasepool {...}
的作用域而销毁一波autorelease
对象,这就可以降低内存的峰值。
1 | for (int i = 0; i < 100000; i ++) { |
autoreleasepool
的实现原理简述:autoreleasepool
其实也是一个对象,它在创建后,内部会有一堆AutoReleasePoolPage
对象,这一堆AutoReleasePoolPage
对象是通过双向链表组织起来的——即AutoReleasePoolPage
对象1的child
属性指向AutoReleasePoolPage
对象2,AutoReleasePoolPage
对象2的child
属性指向AutoReleasePoolPage
对象3,而AutoReleasePoolPage
对象3的parent
属性指向AutoReleasePoolPage
对象2,AutoReleasePoolPage
对象2的parent
属性指向AutoReleasePoolPage
对象1,这样通过child
属性和parent
两个属性关联起来的双向数据结构就是双向链表,而每一个AutoReleasePoolPage
对象内部都有4040个字节用来存放autorelease
对象的内存地址,如果项目里一个AutoReleasePoolPage
对象存不下所有的autorelease
对象的内存地址,那autoreleasepool
在创建的时候就会创建两个AutoReleasePoolPage
对象,依次类推,然后当autoreleasepool
销毁时就会去AutoReleasePoolPage
对象里找到这些对象的地址将它们的引用计数都做一次减1操作。
四、其它一些注意
注意代理不要出现循环引用,block不要出现循环引用,KVO和通知要在dealloc
的时候释放等。
五、知识扩展-GC和引用计数对比
Android 手机通常使用 Java 来开发,而 Java 是使用垃圾回收这种内存管理方式。垃圾回收(Garbage Collection,简称 GC)这种内存管理机制最早由图灵奖获得者 John McCarthy 在 1959 年提出,垃圾回收的理论主要基于一个事实:大部分的对象的生命期都很短。所以,GC 将内存中的对象主要分成两个区域:Young 区和 Old 区。对象先在 Young 区被创建,然后如果经过一段时间还存活着,则被移动到 Old 区。(其实还有一个 Perm 区,但是内存回收算法通常不涉及这个区域)
当 GC 工作时,GC 认为当前的一些对象是有效的,这些对象包括:全局变量,栈里面的变量等,然后 GC 从这些变量出发,去标记这些变量「可达」的其它变量,这个标记是一个递归的过程,最后就像从树根的内存对象开始,把所有的树枝和树叶都记成可达的了。那除了这些「可达」的变量,别的变量就都需要被回收了。
听起来很牛逼对不对?那为什么苹果不用呢?实际上苹果在 OS X 10.5 的时候还真用了,不过在 10.7 的时候把 GC 换成了 ARC。那么,GC 有什么问题让苹果不能忍,这就是:垃圾回收的时候,整个程序需要暂停,英文把这个过程叫做:Stop the World。所以说,你知道 Android 手机有时候为什么会卡吧,GC 就相当于春运的最后一天返城高峰。当所有的对象都需要一起回收时,那种体验肯定是当时还在世的乔布斯忍受不了的。
ARC 相对于 GC 的优点:
1.ARC 工作在编译期,在运行时没有额外开销。
2.ARC 的内存回收是平稳进行的,对象不被使用时会立即被回收。而 GC 的内存回收是一阵一阵的,回收时需要暂停程序,会有一定的卡顿。
ARC 相对于 GC 的缺点:
1.GC 真的是太简单了,基本上完全不用处理内存管理问题,而 ARC 还是需要处理类似循环引用这种内存管理问题。
2.GC 一类的语言相对来说学习起来更简单。