一、
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
修饰的属性最终都是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 *)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
@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
@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);
}
}