avatar

Catalog
iOS内存管理01-定时器

这一阶段我们主要来讲讲iOS内存管理方面的知识,面试的时候可能大家多多少少都会被问及这方面的问题,那我们就从常见的面试题开讲

  • 使用CADisplayLinkNSTimer有什么注意点?

  • 介绍下内存的几大区域

  • 讲一下你对iOS内存管理的理解

  • ARC 都帮我们做了什么?

  • weak指针 的实现原理

  • autorelease 对象在什么时机会被调用release

  • 方法里有局部对象,出了方法后会立即释放吗?

我们一个一个来,今天我们就先来讲讲第一条使用CADisplayLinkNSTimer有什么注意点?

主要就是CADisplayLinkNSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用

循环引用大家都知道吧,这样就会导致内存泄漏,我们写代码来看看

首先我们看下CADisplayLink,我们新建一个NavigationController,然后点击一个按钮push到我们ViewControllerNavigationController就一个按钮比较简单我就不写了,下面看下我们的ViewController里的代码

Code
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 ViewController ()
@property (strong, nonatomic) CADisplayLink *link;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

}

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

- (void)dealloc
{
NSLog(@"%s", __func__);
[self.link invalidate];
}

可能有些同学没用过CADisplayLink

我们先简单了解下,CADisplayLink其实也是一个定时器,只不过这个定时器不用你来设置时间,它是要保证调用频率屏幕的刷帧频率一致,通常来说大概是60FPS,当然如果你主线程要是做了很多耗时操作的话也可能就不到60了,也就是说我们的linkTest方法大概一秒钟会调用60次的样子, 那我们运行程序看下控制台的输出也证实了这一点

那接下来我们来看问题

我们看到我们displayLinkWithTarget这个类方法会传入一个self,这样,CADisplayLink对象就会强引用self,而self强引用了@property (strong, nonatomic) CADisplayLink *link,所以就产生了循环引用,导致两者都不会被释放,可能我们很多同学也都会在dealloc方法里调用 [self.link invalidate] 其实是没有用的,我们点下返回键就可以看到,dealloc根本不用被调用,那如何解决呢,我们先来看看Timer是不是也有这个问题,给Controller添加一个属性

Code
1
@property (strong, nonatomic) NSTimer *timer;

然后初始化并每隔一秒调用timerTest方法

Code
1
2
3
4
5
6
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];

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

我们运行程序看到,返回的时候timer并没有停止,dealloc也没有调用说明timer也存在这个问题,那接下来我们先来解决timer的问题,然后再解决CADisplayLink的问题

可能很多同学会说我来个弱指针__weak typeof(self) weakSelf = self ,把weakSelf传入target, 不就行了吗,那大家想想管用吗?我们运行下程序发下然并卵返回timer依旧跑着,dealloc也没调用,那为什么之前的都好使这次不好使了呢,之前是因为我们在block里的循环引用可以用weakSelf来解决,我们现在是没有block的,而且,weakSelfself其实都是同一个内存地址,我们只是把它当做参数来传给target这个形参的,所以没用,依旧还是强引用,我们可以这样认为

Code
1
2
3
@interface NSTimer()
@property(strong, nonatomic) id target
@end
Code
内部强引用了```target```跟你外部传入强引用还是弱引用没有半毛钱关系,所以这是不能解决问题的,那怎么办?其实要是```NSTimer```的话有好几种解决方案,我们先来看看第一种方案
1
2

### 1、更换```timer```的初始化方法,用带```block```的方法,这个时候就可以用```weakSelf```了

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

Code
1
2
3
4
5
6
7
8
9
我们再运行下程序,我们看到```dealloc```调用了```timer```也停止了,所以问题也解决了

### 2、那我们再换回第一种方法,是否可以解决呢,答案是肯定的,我们来引入一个OtherObject当target,我们来画个图来理解下

![timer](https://upload-images.jianshu.io/upload_images/112869-8f7b22e60afe0ac9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

这样,```timer```强引用```OtherObject```,```OtherObject```弱引用```Controller```就这样就行了,那具体如何来做呢

我们新建一个```XXProxy```类

@interface XXProxy : NSObject

  • (instancetype)proxyWithTarget:(id)target;
    @property (weak, nonatomic) id target;
    @end

@implementation XXProxy

  • (instancetype)proxyWithTarget:(id)target
    {
    XXProxy *proxy = [[XXProxy alloc] init];
    proxy.target = target;
    return proxy;
    }
    Code
    1
    2

    然后修改我们的```Controller
Code
1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[XXProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];

这样就解决循环引用了,但是我们发现另一个问题,现在target里并没有timerTest这个方法,运行肯定会crash,当然我们也可以直接在XXProxy 类里写一个timerTest

Code
1
2
3
4
- (void)timerTest
{
[self.target timerTest];
}

但是大家想没想过,XXProxy这个可能不止被一个timer用,要是有很多timer总不至于把所有的方法都写上吧,大家想想有没有什么更好的方法呢?其实,这个时候我们就可以用消息转发机制,之后可以给大家详细说说,其实消息转发是有三个阶段forwardInvocationmethodSignatureForSelectorforwardingTargetForSelector,我们现在就直接用第三阶段了直接

Code
1
2
3
4
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}

这样不管外界调用我什么方法我都直接转发给控制器对应的方法,这样是不是就一劳永逸了

现在我们运行下程序,我们可以看到dealloc是有调用的,所以问题解决
那我们现在再看下CADisplayLink是不是可以用相同的办法解决,我们修改下代码

Code
1
2
3
// 保证调用频率和屏幕的刷帧频率一致,60FPS
self.link = [CADisplayLink displayLinkWithTarget:[XXProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

我们运行下程序,看下日志,同样解决问题了

点此进入我的博客也会同步更新

One More Thing

点击查看 2020—课程列表 全网IT各种资源有需求的可以微我,或者你喜欢的课程都可以给我发链接剩下的我来搞定

喜欢的朋友可以扫描关注我的公众号(多多点赞,多多打赏,您的支持是我写作的最大动力)关注有福利可以使用免费梯子自由上网

iOS_DevTips

Author: 木子召
Link: https://lizhaobomb.github.io/2020/03/12/iOS%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%8601-%E5%AE%9A%E6%97%B6%E5%99%A8/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶

Comment