程序猿阵线联盟-汇总各类技术干货

解决NSTimer、CADisplaylink的内存泄漏

2018-08-14  本文已影响4人  NeroXie

我们在NSTimer或者CADisplayLink的时候,如果使用不当,可能导致内存泄漏问题,至于内存泄漏的原因,很多人都会觉得是循环引用,什么ViewController强持有NSTimer(以一代二),NSTimer强持有ViewController。真的是够了!!!我们先用一段实例代码测试以上的结论:

@interface WeakTargetViewController ()

@property (nonatomic, strong) NSTimer *strongTimer1;
@property (nonatomic, strong) NSTimer *strongTimer2;
@property (nonatomic, weak) NSTimer *weakTimer1;
@property (nonatomic, weak) NSTimer *weakTimer2;

@end

@implementation WeakTargetViewController

#pragma mark - Lifecycle

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self _test1];
    [self _test2];
    [self _test3];
    [self _test4];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
}

#pragma mark - Private

- (void)_test1 {
    self.strongTimer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}

- (void)_test2 {
    WEAK_SELF;
    self.strongTimer2 = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}

- (void)_test3 {
    self.weakTimer1 = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}

- (void)_test4 {
    WEAK_SELF;
    self.weakTimer2 = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}

#pragma mark - Actions

- (void)timerMethod:(NSTimer *)timer {
    NSLog(@"%s", __func__);
}

运行上面的代码我们会发现,只要不调用invalidate方法,ViewController永远不会被释放,而不是什么循环引用。其实真正导致内存泄漏的原因是NSTimer或者CADisplayLink对target的单向强引用。由于单向强持有,ViewController的dealloc才不会被调用,如果我们在dealloc方式中写了[self.strongTimer1 invalidate]; 是不会被调用的,所以看上去好像是循环引用。但是如果我们在viewWillDisappear:调用[self.strongTimer1 invalidate];这样是有效的。

解决方法

以前在项目中的时候,我是分别针对NSTimerCADisplayLink提供Target-Action类型的分类方法。现在我将之前的代码整合一下,并提供Block的支持,代码如下:

NSObject+WeakTarget.h

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

typedef void(^WeakTargetHandlerBlock)(id objc);

@interface NSObject (WeakTarget)

- (NSTimer *)addTimerWithTimeInterval:(NSTimeInterval)timeInterval
                             selector:(SEL)aSelector
                             userInfo:(id)userInfo
                              repeats:(BOOL)repeat;

- (NSTimer *)addTimerWithWithTimeInterval:(NSTimeInterval)timeInterval
                                 userInfo:(id)userInfo
                                  repeats:(BOOL)repeat
                                    block:(WeakTargetHandlerBlock)block;

- (CADisplayLink *)addDisplayLinkWithSelector:(SEL)sel;
- (CADisplayLink *)addDisplayLinkWithBlock:(WeakTargetHandlerBlock)block;

@end

NSObject+WeakTarget.m

#import "NSObject+WeakTarget.h"
#import <objc/message.h>

#define BLOCK_EXEC(block, ...)      if (block) { block(__VA_ARGS__); };

typedef NS_ENUM(NSInteger, UsedType) {
    UsedTypeNSTimerAction,
    UsedTypeCADisplayLinkAction,
    UsedTypeBlock,
};

@interface WeakTarget: NSObject

@property (nonatomic, weak) id objc;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, copy) WeakTargetHandlerBlock block;
@property (nonatomic, assign) UsedType usedType;

@end

@implementation WeakTarget

- (void)dealloc {
    NSLog(@"%@ dealloc", self.class);
}

- (void)weakTargetAction {
    if (!self.target) {
        void (*objcInvalidate_msgSend)(id, SEL) = (typeof(objcInvalidate_msgSend))objc_msgSend;
        objcInvalidate_msgSend(self.objc, @selector(invalidate));
        
        return;
    }
    
    switch (self.usedType) {
        case UsedTypeNSTimerAction: {
            void (*timerAction_msgSend)(id, SEL, NSTimer *) = (typeof(timerAction_msgSend))objc_msgSend;
            timerAction_msgSend(self.target, self.selector, (NSTimer *)self.objc);
        }
            break;
        case UsedTypeCADisplayLinkAction: {
            void (*linkAction_msgSend)(id, SEL, CADisplayLink *) = (typeof(linkAction_msgSend))objc_msgSend;
            linkAction_msgSend(self.target, self.selector, (CADisplayLink *)self.objc);
        }
            break;
        case UsedTypeBlock: {
            BLOCK_EXEC(self.block, self.objc);
        }
            break;
        default:
            break;
    }
}

@end

@implementation NSObject (WeakTarget)

#pragma mark - Public

- (NSTimer *)addTimerWithTimeInterval:(NSTimeInterval)timeInterval selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeat {
    if (![self respondsToSelector:aSelector]) {
        return nil;
    }
    
    WeakTarget *timerTarget = [self _addWeakTargetWithUsedType:UsedTypeNSTimerAction];
    timerTarget.selector = aSelector;
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
                                                      target:timerTarget
                                                    selector:@selector(weakTargetAction)
                                                    userInfo:userInfo
                                                     repeats:repeat];
    timerTarget.objc = timer;
    
    return timer;
}

- (NSTimer *)addTimerWithWithTimeInterval:(NSTimeInterval)timeInterval
                                 userInfo:(id)userInfo
                                  repeats:(BOOL)repeat
                                    block:(WeakTargetHandlerBlock)block {
    if (!block) {
        return nil;
    }
    
    
    WeakTarget *timerTarget = [self _addWeakTargetWithUsedType:UsedTypeBlock];
    timerTarget.block = block;
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval
                                                      target:timerTarget
                                                    selector:@selector(weakTargetAction)
                                                    userInfo:userInfo
                                                     repeats:repeat];
    timerTarget.objc = timer;
    
    return timer;
}

- (CADisplayLink *)addDisplayLinkWithSelector:(SEL)aSelector {
    if (![self respondsToSelector:aSelector]) {
        return nil;
    }
    
    WeakTarget *linkTarget = [self _addWeakTargetWithUsedType:UsedTypeCADisplayLinkAction];
    linkTarget.selector = aSelector;
    
    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:linkTarget selector:@selector(weakTargetAction)];
    linkTarget.objc = link;
    
    return link;
}

- (CADisplayLink *)addDisplayLinkWithBlock:(WeakTargetHandlerBlock)block {
    if (!block) {
        return nil;
    }
    
    WeakTarget *linkTarget = [self _addWeakTargetWithUsedType:UsedTypeBlock];
    linkTarget.block = block;
    
    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:linkTarget selector:@selector(weakTargetAction)];
    linkTarget.objc = link;
    
    return link;
}

#pragma mark - Private

- (WeakTarget *)_addWeakTargetWithUsedType:(UsedType)usedType {
    WeakTarget *target = [WeakTarget new];
    target.usedType = usedType;
    target.target = self;
    
    return target;
}

@end

调用如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.label = [[UILabel alloc] init];
    [self.label addTimerWithWithTimeInterval:1 userInfo:nil repeats:YES block:^(id objc) {
        NSLog(@"%s", __func__);
    }];

    [self addTimerWithTimeInterval:1 selector:@selector(timerMethod:) userInfo:nil repeats:YES];
    //如果要使用selector形式的方法,self必须实现对应的selector方法

    CADisplayLink *link = [self addDisplayLinkWithSelector:@selector(timerMethod:)];
    [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
    CADisplayLink *link2 = [self addDisplayLinkWithBlock:^(id objc) {
        NSLog(@"%s", __func__);
    }];
    [link2 addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

#pragma mark - Actions

- (void)timerMethod:(NSTimer *)timer {
    NSLog(@"%s", __func__);
}

打印结果:


result.jpg

从上面的代码中可以看到,我们只需要在ViewController中创建对应的NSTimer即可。ViewControllerNSTimer之间通过一个内部类WeakTarget进行关联。在ViewController被释放的时候,WeakTargettarget便不存在,当下一次再调用weakTargetAction函数的时候,便会调用objcInvalidate_msgSend(self.objc, @selector(invalidate));通知NSTimer释放掉持有的对象WeakTarget。这里既然知道了对象和对应方法,那就直接走消息转发就好了。

上一篇 下一篇

猜你喜欢

热点阅读