个人技术生活分享

文明其精神,野蛮其体魄

0%

内存管理二

一、定时器的循环引入

二、copy、mutableCopy

三、创建大量autorelease对象时,最好自己创建一个@autoreleasepool {}

四、其他注意事项

一、定时器的循环引入

我们以NSTimer举例,CADisplayLink遇到同样的问题,解决方案也一样。

1.NSTimer的循环引入

使用NSTimer,写法通常如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "ViewController.h"
#import "ViewController1.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

ViewController1 *vc = [[ViewController1 alloc] init];
[self.navigationController pushViewController:vc animated:YES];
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#import "ViewController1.h"

@interface ViewController1 ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController1

- (void)viewDidLoad {
[super viewDidLoad];

// 创建timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
}

- (void)test {

NSLog(@"11");
}

- (void)dealloc {

NSLog(@"%s", __func__);

// 退出界面时,使timer失效从而销毁
[self.timer invalidate];
}

@end

运行代码,点击ViewController进入ViewController1,此时timer跑起来了,每隔1秒打印一次“11”。此时点击返回按钮,返回ViewController,正常情况下ViewController1应该会销毁,并触发dealloc方法,timer也跟着失效并且销毁。但实际情况却是ViewController1没有销毁,也没有触发dealloc方法,timer还一直跑着,这是因为timerViewController1形成了循环引用,导致内存泄漏。

查看timer的创建方法,可以知道:**timer会强引用target,**也就是说timer确实强引用着ViewController1

creatTimer

ViewController又强引用着timer

img

那怎么打破NSTimer的循环引用呢?我们知道__weak是专门用来打破循环引用的,那它是不是也能打破NSTimer的循环引用?

1
2
3
4
5
6
7
- (void)viewDidLoad {
[super viewDidLoad];

// 尝试用__weak打破NSTimer的循环引用
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(test) userInfo:nil repeats:YES];
}

运行,发现没有效果,那为什么__weak不能打破NSTimer的循环引用?毫无疑问__weak的确是把self搞成了弱指针,但因为NSTimer内部有一个强指针类型的target变量

1
@property (nonatomic, strong) id target;

来接收这个传进来的地址值,所以无论你外界是传进来强指针还是弱指针,它内部都是一个强指针接收,就总是会强引用target,所以用__weak不能打破NSTimer的循环引用。

那再试试另一条引用线吧,让ViewController1弱引用timer

1
2
3
4
5
6
@interface ViewController1 ()

// 尝试用weak修饰timer来打破NSTimer的循环引用
@property (nonatomic, weak) NSTimer *timer;

@end

运行,发现没有效果,奇了怪了,怎么回事呢?查看官方对NSTimer的说明,可以知道:timer添加到RunLoop之后,RunLoop会强引用timer,并且建议我们不必自己强引用timer,而解除RunLoop对timer强引用的唯一方式就是调用timerinvalidate方法使timer失效从而销毁。

img

img

也就是说,实际的引用关系如下:

img

所以我们使用weak修饰timer是正确的,但这还是不能打破NSTimer的循环引用——更准确地说,这可以解决NSTimer的循环引用,但还是没有解决NSTimer内存泄漏的问题。因为[self.timer invalidate]的调用——即timer的销毁——最好就是发生在ViewController1销毁时,而ViewController1要想销毁就必须得timer先销毁,还是内存泄漏。

倒腾来倒腾去,还是得timer强引用target这条引用线下手,把它搞成弱引用,__weak不起作用,那我们想想别的方案呗。

2、打破NSTimer的循环引用

  • 方案一:使用block的方式创建timer
1
2
3
4
5
6
7
8
9
- (void)viewDidLoad {
[super viewDidLoad];

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {

[weakSelf test];
}];
}

为什么能解决呢?因为此时timer是强引用block的,而__weak可以打破block的循环引用,所以block是弱引用self的,所以最终的效果就类似于timer弱引用self。解决是能解决,但用这种方式创建timer要iOS10.0以后才能使用。

  • 方案二:创建一个中间对象——代理

    我们可以把方案一的思路自己实现一下嘛,即创建一个中间对象(方案一的中间对象就是block嘛),把这个中间对象作为timertarget参数传进去,让timer强引用这个中间对象,而让这个中间对象弱引用ViewController1,这样ViewController1就能正常释放,NSTimer就能正常调用失效方法,RunLoop就能正常解除对NSTimer的强引用,NSTimer就能正常解除对中间对象的强引用,内存泄漏就解决了。当然由于中间对象没有target——即ViewController1——的方法,所以我们还要做一步消息转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
-----------INETimerProxy.h-----------

#import <Foundation/Foundation.h>

@interface INETimerProxy : NSObject

+ (instancetype)proxyWithTarget:(id)target;

@end


-----------INETimerProxy.m-----------

#import "INETimerProxy.h"

@interface INETimerProxy ()

/// 弱引用target所指向的对象
@property (nonatomic, weak) id target;

@end

@implementation INETimerProxy

+ (instancetype)proxyWithTarget:(id)target {

INETimerProxy *proxy = [[INETimerProxy alloc] init];
proxy.target = target;
return proxy;
}

// 直接消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector {

return self.target;
}

@end
1
2
3
4
5
6
7
8
9
-----------ViewController1.m-----------

#import "INETimerProxy.h"

- (void)viewDidLoad {
[super viewDidLoad];

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[INETimerProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];
}

为了提高消息转发效率,我们可以让代理直接继承自NSProxy,而不是NSObject。**NSProxy是专门用来做消息转发的,继承自NSObject的类调用方法时会走方法查找 –> 动态方法解析 –> 直接消息转发、完整消息转发这套流程,而继承自NSProxy的类调用方法时只会走方法查找 –> 完整消息转发这两个流程,消息转发效率更高,所以以后但凡要做消息转发就直接继承自NSProxy好了,而不是NSObject。**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-----------INETimerProxy.h-----------

#import <Foundation/Foundation.h>

@interface INETimerProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@end


-----------INETimerProxy.m-----------

#import "INETimerProxy.h"

@interface INETimerProxy ()

/// 弱引用target所指向的对象
@property (nonatomic, weak) id target;

@end

@implementation INETimerProxy

+ (instancetype)proxyWithTarget:(id)target {

// NSProxy类是没有init方法的,alloc后就可以直接使用
INETimerProxy *proxy = [INETimerProxy alloc];
proxy.target = target;
return proxy;
}

// 完整消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {

return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {

[invocation invokeWithTarget:self.target];
}

@end

二、copymutableCopy

1、深拷贝与浅拷贝

  • 深拷贝,是指内容拷贝,会产生新的对象,新对象的引用计数为1;浅拷贝,是指指针拷贝,不会产生新的对象,旧对象的引用计数加1,浅拷贝其实就是retain,深拷贝的话新对象和旧对象互不影响,浅拷贝的话改变一个另一个也跟着变了。
  • 只有不可变对象的不可变拷贝是浅拷贝,其它的都是深拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[super viewDidLoad];

NSString *str1 = @"11";
NSString *str2 = [str1 copy]; // 不可变对象的不可变拷贝 --> 浅拷贝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", str1, str2, str3);

NSMutableString *str4 = [@"11" mutableCopy];
NSString *str5 = [str4 copy]; // 深拷贝
NSMutableString *str6 = [str4 mutableCopy]; // 深拷贝
NSLog(@"%p %p %p", str4, str5, str6);
}


控制台打印:
0x1025260b0 0x1025260b0 0x600003bc0ab0
0x600003b992c0 0xc91f17b5d8b748d0 0x600003b99890

2、不可变属性最好用copy修饰,而可变属性坚决不能用copy修饰、只能用strongretain修饰

copy拷贝出来的东西是不可变对象,是不能修改的;

mutableCopy拷贝出来的东西是可变对象,是能修改的。

  • 不可变对象最好用copy修饰

    不可变对象最好用copy修饰,因为用strongretain修饰的话,setter方法内部仅仅是retain,那当我们把一个可变对象赋值给这个不可变属性时,不可变属性仅仅是指针指向了可变对象,修改可变对象的值,也就是不可变属性指向的对象值发生了改变,这不是我们所希望的结果,我们直观的感觉应该是“不可变属性指向的对象不应该随着别人的改变而改变”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface ViewController ()

@property (nonatomic, strong) NSString *name;
//@property (nonatomic, retain) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 可变对象
NSMutableString *mutableName = [@"张三" mutableCopy];
// 可变对象赋值给这个不可变属性
self.name = mutableName;
NSLog(@"%@", self.name); // 张三

// 修改可变对象的值
[mutableName appendString:@"丰"];
NSLog(@"%@", self.name); // 张三丰,不可变属性的值也会跟着变化,这不是我们希望看到的
}

@end

而用copy修饰的话,setter方法内部就是copy,那不管你外界传给它一个可变还是不可变对象,该属性最终都是深拷贝出一份不可变的,这样外界就无法影响这个属性的值,除非我们主动修改,符合我们的预期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@interface ViewController ()

@property (nonatomic, copy) NSString *name;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 可变对象
NSMutableString *mutableName = [@"张三" mutableCopy];
// 可变对象赋值给这个不可变属性
self.name = mutableName;
NSLog(@"%@", self.name); // 张三

// 修改可变对象的值
[mutableName appendString:@"丰"];
NSLog(@"%@", self.name); // 张三,外界无法影响这个属性的值

self.name = @"张三丰";
NSLog(@"%@", self.name); // 张三丰,我们主动修改属性的值,符合我们的预期
}

@end
  • 而可变属性坚决不能用copy修饰、只能用strongretain修饰

    可变属性坚决不能用copy修饰,只能用strongretain修饰,道理和上面一样,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 {...}

只要不是用allocnewcopymutableCopy方法创建的对象,而是用类方法创建的对象,方法内部都调用了autorelease,都是autorelease对象,例如:

1
2
3
4
NSString *str = [NSString string];
NSArray *arr = [NSArray array];
NSDictionary *dict = [NSDictionary dictionary];
UIImage *image = [UIImage imageNamed:@"11"];

因为类方法的内部实现大概如下:

1
2
3
4
5
6
7
- (id)object {

id obj = [[NSObject alloc] init];
[obj autorelease];

return obj;
}

allocnewcopymutableCopy方法的内部实现大概如下:所以在创建大量autorelease对象时,最好自己创建一个@autoreleasepool {...}

1
2
3
4
5
6
- (id)allocObject {

id obj = [[NSObject alloc] init];

return obj;
}

所以在创建大量autorelease对象时,最好自己创建一个@autoreleasepool {...}。因为如果主线程RunLoop的某次循环一直忙着处理事情,线程没有休眠或者退出,那本次循环的autoreleasepool就迟迟无法销毁,这就会导致这次循环里的autorelease对象迟迟无法释放掉,因此就很有可能会导致内存的使用峰值过高,从而导致内存溢出。而自己创建@autoreleasepool {...}后,每一次for循环都会出一次@autoreleasepool {...}的作用域而销毁一波autorelease对象,这就可以降低内存的峰值。

1
2
3
4
5
6
7
8
for (int i = 0; i < 100000; i ++) {

@autoreleasepool {

NSString *string = [NSString stringWithFormat:@"%d", i];
NSLog(@"%@", string);
}
}

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 一类的语言相对来说学习起来更简单。