内存管理:实际开发需注意

一、copymutableCopy
二、定时器的循环引用
三、创建大量autorelease对象时,最好自己创建一个@autoreleasepool {}


一、copymutableCopy


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修饰,因为用strongretain修饰的话,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修饰,只能用strongretain修饰,和上面是同样的道理,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进入ViewController1timer跑起来了,1秒钟打印一次“11”。此时我们点击返回按钮,返回ViewController,按理说ViewController1应该销毁,走dealloc方法,timer也跟着失效从而销毁,但实际上ViewController1没有销毁,没走dealloc方法,timer也还一直跑着,这是因为timerViewController1形成了循环引用导致的内存泄漏。

查看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强引用的唯一方式就是调用timerinvalidate方法使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嘛),把这个中间对象作为timertarget参数传进去,让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,而不是NSObjectNSProxy是专门用来做消息转发的,继承自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 {}


只要不是用allocnewcopymutableCopy方法创建的对象,而是用类方法创建的对象,方法内部都调用了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;
}

allocnewcopymutableCopy方法的内部实现大概如下:

- (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);
    }
}
禁止转载,如需转载请通过简信或评论联系作者。