NSTimer 不释放问题分析及解决

·  阅读 1493
NSTimer 不释放问题分析及解决

NSTimer 不释放问题

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

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
复制代码

如上代码所示,我们创建一个定时器,并添加到当前 runLoop,也通过 __weak 修饰了 weakSelf,但是当我们运行计时器离开页面的时候发现计时器并没有销毁,依然在执行。这里有个比较疑惑的点,我们用 block 的时候用 __weak 修饰的时候是可以解决循环引用的问题,但是为什么这里不可以呢?

image.png

通过官方文档我们可以看到,我们传入的 target 会被 timer 强持有,这里传入的 target 就是 weakSelf,所以 weakSelf 指向的内存空间引用计数会被加 1。而 block 传入 weakSelf 是被弱引用捕获,不会对引用计数加 1。然后这里就会造成一种现象,[NSRunLoop currentRunLoop] 是常驻的,[NSRunLoop currentRunLoop] 持有 timertimer 强持有 weakSelf,而 weakSelf 指向的内存空间就是 self,引用计数会被加 1,所以会出现不释放的问题。

NSTimer 不释放问题解决方案

针对不释放问题我们这里有几种解决方案如下,以供参考。

使用 block 的形式

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer 执行");
    }];
复制代码

离开页面时对 timer 进行处理

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

类似这种,在离开页面的时候调用 invalidate,并把 timer 设置为 nil,但是这里会出现一个问题,push 到下一个页面的时候 timer 也会被释放。所以推荐下面这种方法,只在 pop 离开页面时进行释放。

- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
    }
}
复制代码

中介者模式

- (void)viewDidLoad {
    [super viewDidLoad];

    self.target = [[NSObject alloc] init];
    class_addMethod([NSObject class], @selector(timerRun), (IMP) timerRunObjc, "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(timerRun) userInfo:nil repeats:YES];
}

void timerRunObjc(id obj){
    NSLog(@"%s -- %@",__func__,obj);
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}
复制代码

类似这种,我们可以使用一个中间者来作为 target,来打破这种不释放问题,这种思维来自于 FBKVO,但是这种虽然能解决问题,但是不够简洁,逻辑代码都写在了控制器中,我们可以对此进行优化。

@interface CXTimerViewController ()
@property (nonatomic, strong) CXTimerWapper *timerWapper;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timerWapper = [[CXTimerWapper alloc] cx_initWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
}
复制代码
@interface CXTimerWapper : NSObject

- (instancetype)cx_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

@end

#import "CXTimerWapper.h"
#import <objc/message.h>

@interface CXTimerWapper()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation CXTimerWapper

- (instancetype)cx_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
    if (self == [super init]) {
        self.target     = aTarget; // vc
        self.aSelector  = aSelector; // 方法 -- vc 释放
        
        if ([self.target respondsToSelector:self.aSelector]) { 
            Method method    = class_getInstanceMethod([self.target class], aSelector);
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
            
            self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

// 一直跑 runloop
void fireHomeWapper(CXTimerWapper *warpper){
    
    if (warpper.target) { // vc - dealloc
        void (*cx_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
         cx_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
    }else{ // warpper.target
        [warpper.timer invalidate];
        warpper.timer = nil;
    }
}

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

@end
复制代码

如上代码,这里采用分层思想,把 timer 跟控制器进行了隔离,timer 的所有逻辑处理都放到了 CXTimerWapper 类中,这里做的比较好的一点就是没有把 selector 进行写死,而是由外界传入的 target 调用 selector,这样就会比较灵活,而且在 fireHomeWapper 函数中做了对 warpper.target 的判断,warpper.target 为空说明外界调用者已经被释放了,所以就会对 timer 进行释放。

虚基类的方式

@interface ViewController ()
@property (nonatomic, strong) CXProxy       *proxy;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.proxy = [CXProxy proxyWithTransformObject:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerRun) userInfo:nil repeats:YES];
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
}
复制代码
@interface CXProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

@interface CXProxy()
@property (nonatomic, weak) id object;
@end

@implementation CXProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    CXProxy *proxy = [CXProxy alloc];
    proxy.object = object;
    return proxy;
}

// 消息快速转发
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

// 消息转发 self.object
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
//
//    if (self.object) {
//    }else{
//        NSLog(@"麻烦收集 stack111");
//    }
//    return [self.object methodSignatureForSelector:sel];
//
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation{
//
//    if (self.object) {
//        [invocation invokeWithTarget:self.object];
//    }else{
//        NSLog(@"麻烦收集 stack");
//    }
//
//}
@end
复制代码

这里我们定义了一个继承于 NSProxy 的类 CXProxy,这里需要先了解一下 NSProxyNSProxy 是一个实现了 NSObject 协议的根类,苹果的官方文档是这样描述它的:NSProxy 是一个抽象基类,它为一些表现的像是其它对象替身或者并不存在的对象定义API。一般的,发送给代理的消息被转发给一个真实的对象或者代理本身引起加载(或者将本身转换成)一个真实的对象。NSProxy 的基类可以被用来透明的转发消息或者耗费巨大的对象的 lazy 初始化。

CXProxy 类中我们实现了 forwardingTargetForSelector 方法,返回 self.object,这里 self.object 就是外部的控制器,所有发送给 CXProxy 的方法都会被转发给 self.object。这里跟上面讲的中介者模式类似,但是这里需要注意一点就是在控制器释放的时候需要在 dealloc 方法中对 timer 进行释放。

趁着中秋放假这几天更新了几篇博客,有不足的地方还请多多指正。

分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改