iOS内存管理

iOS的内存管理,很经典的问题就是循环引用,我们通过定时器的循环引用问题来展开内存管理的学习。

定时器

  • 使用CADisplayLink和NSTimer的时候为什么产生循环引用?
    我们通过代码来看一下:
    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
    @interface CLViewController ()
    @property (nonatomic, strong) CADisplayLink *displayLink;
    @end

    @implementation CLLockTestViewController

    - (void)viewDidLoad {
    [super viewDidLoad];
    // __weak typeof(self) weakSelf = self;
    // CADisplayLink
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(run)];
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

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

    - (void)run {
    NSLog(@"%s",__func__);
    }

    - (void)dealloc {
    NSLog(@"%@ dealloc",NSStringFromClass(self.class));
    [self.displayLink invalidate];
    }

以上两种case在ViewController创建完之后会调用run方法,那么当我们退出页面的时候run还会调用吗?通过测试发现当前ViewController是不会调用dealloc方法的,所以会继续调用run方法。为什么呢?原因就是ViewController 强持有了displayLink对象,而displayLink对象在构造的时候也强持有了self(也就是当前ViewController),所以就产生了两个对象互相强持有的问题。

用过block的朋友可能在这种场景下会想到使用弱指针也就是__weak typeof(self) weakSelf = self,那么我们这里把target参数传进去一个weakSelf会不会解决循环引用的问题呢?经过测试,传进去weakSelf 依然会产生循环引用。那么为什么在block里面使用weak指针就可以呢?是因为如果在block内部使用weak指针block会对外部的对象产生一个弱引用,如果是strong指针就会产生一个强引用。而我们 displayLinkWithTarget:self 这里不管self 是不是弱指针,CADisplayLink内部会对self 产生一个强引用所以weakSelf 并不不能解决问题。同理NSTimer。

那么该如何解决这个问题呢?

如果是NSTimer可以通过weak指针加block的方式来解决:

1
2
3
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf run];
}];

这段代码经过测试,在block内部调用run的时候不会产生循环引用理由上文已经介绍过了。

  • Tip: NSTimer 以scheduledTimer开头创建的定时器是会自动启动的,因为系统在创建的时候会把timer添加在当前runloop的defaultmode下,而以timerWithTimeInterval开头创建的定时器是没有这个操作的,需要把这个timer添加到runloop里面才会重复执行,如果不添加到runloop中而是调用 [self.timer fire]只会调用一次run。

那如果不用block的方式或者是CADisplayLink这种情况该如何处理呢?

我们先看下图:

proxy@2x

通过一个中间代理对象来规避循环引用:首先viewcontroller强持有timer,其次让timer 强持有中间对象otherobject,而otherobject呢弱持有viewcontroller,这样就不会产生循环了,那么问题来了怎么调用viewcontroller里面定义的timer的事件方法呢?思考一下我们是否可以有一些骚操作?没错,消息转发!我们可以在otherobject对象里面实现forwardingTargetForSelector方法,然后将其持有的viewcontroller对象返回出去,这样就可以调用到viewcontroller里面定义的timer的事件方法了。

下面我们介绍一下这个中间对象的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface CLProxy : NSObject
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation CLProxy
//生成一个代理对象
+ (CLProxy *)proxyWithTarget:(id)target {
CLProxy *proxy = [[CLProxy alloc]init];
proxy.target = target;
return proxy;
}

// 消息转发流程
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end

那么我们在ViewController里面实现timer的时候这样就可以解决问题了:

1
2
// NSTimer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy proxyWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];

同时需要在ViewController的dealloc 里面调用[self.timer invalidate],就可以解决循环引用的问题了。

  • Tip 有的朋友可能用过NSProxy这个类,这个系统类能够用类似的方式解决上述问题,下面我们介绍一下怎么通过这个类来解决此类循环引用问题。

我们定义一个CLProxy继承自NSProxy代码如下:

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 CLProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation CLProxy

// 生成一个代理对象
+ (CLProxy *)proxyWithTarget:(id)target {
CLProxy *proxy = [CLProxy alloc];
proxy.target = target;
return proxy;
}
// 进入消息转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

// 调用target对象的方法
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

@end

上述方式也可以达到我们想要的效果,那么问题来了,这二者有啥区别呢?而且第二种貌似比第一种复杂,需要实现两个方法。结论是:第二种方法比第一种方法效率要高。因为第一种方法会先从父类查找方法,然后再消息转发,而第二种的话会直接来到消息转发的流程,所以效率较高。所以我们可以这么理解,NSProxy类是专门为了做消息转发而定义的类。

iOS内存布局

从低地址到高地址依次是:保留段、代码段、数据段、堆、栈、内核区;

谢谢您的支持!