小码哥iOS学习笔记第二十三天: 内存管理-定时器

855 阅读5分钟

一、CADisplayLink

  • CADisplayLink: 使用频率和屏幕的刷新频率保持一致, 60FPS

  • 设置程序的界面结构如下图所示, 其中橙色的界面就是ViewController

  • ViewController中有如下代码, ViewController有一个属性CADisplayLink *displayLink
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTest)];
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

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

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

@end
  • 运行程序, 进入ViewController后可以看到控制台不停的打印

  • 即便退出控制器, 也可以看到-dealloc根本没有执行, 定时器依然在不停的调用
  • 此时, 就形成了ViewController-CADisplayLink的循环引用, 类似下图

解决循环引用

  • 可以使用一个中间对象来解决循环引用问题

  • Proxy中代码如下, 使用便利构造器创建Proxy对象, 同时存储target
  • Proxy不实现任何target调用的方法, 而是使用消息转发的方式, 将消息转发给target, 这样不论定时器调用任何方法, 都能交给target去执行
#import "Proxy.h"

@interface Proxy ()
@property (nonatomic, weak) id target;
@end

@implementation Proxy

+ (instancetype)proxyWithTarget:(id)target
{
    Proxy *proxy = [[Proxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end
  • 此时ViewController中代码如下, CADisplayLink绑定[Proxy proxyWithTarget:self], 调用-displayLinkTest方法
  • 当运行程序时, 因为Proxy没有实现-displayLinkTest方法, 此时Proxy就会通过消息转发, 将displayLinkTest转交给target去执行
#import "ViewController.h"
#import "Proxy.h"

@interface ViewController ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.displayLink = [CADisplayLink displayLinkWithTarget:[Proxy proxyWithTarget:self] selector:@selector(displayLinkTest)];
    [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

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

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

@end
  • 运行程序, 进入ViewController界面后, 可以看到控制台不停的打印, 当点击返回按钮, 退出ViewController后, 就会调用ViewController-dealloc方法, 停止定时器

  • 这样, 就解决了ViewController-CADisplayLink的循环引用问题

二、NSTimer

  • NSTimerCADisplayLink类似, 也会造成循环引用问题
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}

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

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

@end
  • 执行程序, 进入ViewController可以看到每一秒打印一次, 退出ViewController后打印也不会停止

  • 此时的循环结构如下图

解决循环引用问题

  • CADisplayLink一样, 使用中间对象Proxy即可
#import "ViewController.h"
#import "Proxy.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[Proxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

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

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

@end
  • 运行程序, 进入ViewController后控制台持续打印, 退出ViewController后, 定时器停止

  • 此时的内存结构如下

三、NSProxy

  • NSProxy是与NSObject同级别的类, NSProxy的定义是下面这段代码
@interface NSProxy <NSObject> {
    Class	isa;
}

+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)allowsWeakReference NS_UNAVAILABLE;
- (BOOL)retainWeakReference NS_UNAVAILABLE;

// - (id)forwardingTargetForSelector:(SEL)aSelector;

@end
  • NSProxy没有任何的父类, 与NSObject一样遵守<NSObject>协议
  • NSProxy是用来做消息转发的类, 如果自己没有实现目标方法, 那么就会立刻进入消息转发

1、使用NSProxy解决定时器内存管理问题

  • 定义BWProxy继承自NSProxy, 并实现下列方法
@interface BWProxy : NSProxy

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

@end

@implementation BWProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy没有init方法, 只需要调用alloc创建对象即可
    BWProxy *proxy = [BWProxy alloc];
    proxy.target = target;
    return proxy;
}
@end
  • ViewController使用BWProxy替代上面的Proxy
#import "ViewController.h"
#import "BWProxy.h"

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[BWProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

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

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

@end
  • 执行程序, 进入ViewController可以看到, 有下面的报错

  • 可以看到, 报错信息是-[NSProxy methodSignatureForSelector:] called!, 并不是找不到timerTest方法
  • 我们可以在BWProxy中加入-methodSignatureForSelector:-forwardInvocation:两个方法, 实现消息转发来解决崩溃的问题
#import <Foundation/Foundation.h>

@interface BWProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation BWProxy

+ (instancetype)proxyWithTarget:(id)target
{
    // NSProxy没有init方法, 只需要调用alloc创建对象即可
    BWProxy *proxy = [BWProxy alloc];
    proxy.target = target;
    return proxy;
}

- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}

@end
  • 运行程序, 进入ViewController后, 再次退出, 可以看到NSTimer停止, ViewController被释放

  • CADisplayLinkNSTimer一样, 这里就不再赘述

2、-isKindOfClass:

  • 使用上面的ProxyBWProxy, 实现下面的代码
Proxy *proxy1 = [Proxy proxyWithTarget:self];
BWProxy *proxy2 = [BWProxy proxyWithTarget:self];
NSLog(@"%d", [proxy1 isKindOfClass:[ViewController class]]);
NSLog(@"%d", [proxy2 isKindOfClass:[ViewController class]]);
  • 可以看到控制台的打印如下

  • proxy1的基类是NSObject, 所以打印为0
  • proxy2实际上是进行了消息转发, 将isKindOfClass:转发给了target, 也就是ViewController, 所以打印是1
  • GUNStep中也可以看到实现过程

四、GCD定时器

  • NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时
  • GCD定时器不依赖于RunLoop, 会更加的准时
#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 获取主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 创建定时器, 在主线程中调用
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 2秒后执行
    NSTimeInterval start = 2.0;
    // 执行间隔1秒
    NSTimeInterval interval = 1.0;
    // 设置定时器
    dispatch_source_set_timer(timer,
                              dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
                              interval * NSEC_PER_SEC,
                              0);
    // 设置回调
    __weak typeof(self) weakSelf = self;
    dispatch_source_set_event_handler(timer, ^{
        [weakSelf timerTest];
    });
    // 启动定时器
    dispatch_resume(timer);
    self.timer = timer;
}

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

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

@end
  • 运行程序, 进入ViewController, 可以看到定时器的打印, 退出ViewController可以看到-dealloc被调用, 定时器停止