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

1、CADisplayLink

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

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

  • 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的循环引用问题

2、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后, 定时器中止

  • 此时的内存结构以下

3、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是用来作消息转发的类, 若是本身没有实现目标方法, 那么就会马上进入消息转发

一、使用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同样, 这里就再也不赘述

二、-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中也能够看到实现过程

4、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被调用, 定时器中止

相关文章
相关标签/搜索