一、定时器的循环引入
二、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 一类的语言相对来说学习起来更简单。