内存管理:实际开发需注意
一、
copy、mutableCopy二、定时器的循环引用
三、创建大量
autorelease对象时,最好自己创建一个@autoreleasepool {}
一、copy、mutableCopy
1、深拷贝与浅拷贝
深拷贝,是指内容拷贝,会产生新的对象,新对象的引用计数为1;浅拷贝,是指指针拷贝,不会产生新的对象,旧对象的引用计数加1,浅拷贝其实就是
retain。只有不可变对象的不可变拷贝是浅拷贝,其它的都是深拷贝。
- (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修饰
copy拷贝出来的东西是不可变对象,是不能修改的;mutableCopy拷贝出来的东西是可变对象,是能修改的。
- 不可变属性最好用
copy修饰
不可变属性最好用copy修饰,因为用strong或retain修饰的话,setter方法内部仅仅是retain,那当我们把一个可变对象赋值给这个不可变属性时,修改可变对象的值,不可变属性的值也会跟着变化,这不是我们希望看到的。
@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,那不管你外界传给它一个可变对象还是不可变对象,该属性最终都是copy出一份不可变的,这样外界就无法影响这个属性的值,除非我们主动修改属性的值,符合我们的预期。
@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修饰
可变属性坚决不能用copy修饰,只能用strong或retain修饰,和上面是同样的道理,copy修饰的属性最终在setter方法里copy出来的是一份不可变的,如果你非要用它修饰可变属性,那从外在看来好像可以修改这个属性,结果你一修改就崩溃,因为找不到方法。
@interface ViewController ()
@property (nonatomic, copy) NSMutableString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = [@"张三" mutableCopy];
[self.name appendString:@"丰"]; // 一修改,就崩溃,因为NSString根本没有appendString:方法
}
@end
二、定时器的循环引用
我们只以
NSTimer举例好了,CADisplayLink会遇见同样的问题,解决方案也是一样的。
1、NSTimer的循环引用
使用NSTimer,写法通常如下:
// ViewController.m
#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
// ViewController1.m
#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也还一直跑着,这是因为timer和ViewController1形成了循环引用导致的内存泄漏。
查看timer的创建方法,可以知道:timer会强引用target,也就是说timer确实强引用着ViewController1。
而ViewController1又强引用着timer。
那怎么打破NSTimer的循环引用呢?我们知道__weak是专门用来打破循环引用的,那它是不是也能打破NSTimer的循环引用?
- (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变量
@property (nonatomic, strong) id target;
来接收这个传进来的地址值,所以无论你外界是传进来强指针还是弱指针,它内部都是一个强指针接收,就总是会强引用target,所以用__weak不能打破NSTimer的循环引用。
那再试试另一条引用线吧,让ViewController1弱引用timer。
@interface ViewController1 ()
// 尝试用weak修饰timer来打破NSTimer的循环引用
@property (nonatomic, weak) NSTimer *timer;
@end
运行,发现没有效果,奇了怪了,怎么回事呢?查看官方对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
- (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以后才能使用。
3、打破NSTimer的循环引用,方案二:中间对象——代理
我们可以把方案一的思路自己实现一下嘛,即创建一个中间对象(方案一的中间对象就是block嘛),把这个中间对象作为timer的target参数传进去,让timer强引用这个中间对象,而让这个中间对象弱引用ViewController1,不就解决了嘛。当然由于中间对象没有target——即ViewController1——的方法,所以我们还要做一步消息转发。
// 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
// 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。
// 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
三、创建大量autorelease对象时,最好自己创建一个@autoreleasepool {}
只要不是用alloc、new、copy、mutableCopy方法创建的对象,而是用类方法创建的对象,方法内部都调用了autorelease,都是autorelease对象,例如:
NSString *str = [NSString string];
NSArray *arr = [NSArray array];
NSDictionary *dict = [NSDictionary dictionary];
UIImage *image = [UIImage imageNamed:@"11"];
因为类方法的内部实现大概如下:
- (id)object {
id obj = [[NSObject alloc] init];
[obj autorelease];
return obj;
}
而alloc、new、copy、mutableCopy方法的内部实现大概如下:
- (id)allocObject {
id obj = [[NSObject alloc] init];
return obj;
}
所以在创建大量autorelease对象时,最好自己创建一个@autoreleasepool {}。因为如果主线程RunLoop的某次循环一直忙着处理事情,线程没有休眠或退出,那这些autorelease对象是无法及时释放掉的,内存使用峰值过高就有可能导致一些问题。而自己创建@autoreleasepool {}后,每一次for循环就都会出一次@autoreleasepool {}的作用域而销毁一波autorelease对象,这就可以降低内存使用的峰值。
for (int i = 0; i < 100000; i ++) {
@autoreleasepool {
NSString *string = [NSString stringWithFormat:@"%d", i];
NSLog(@"%@", string);
}
}