NSTimer内存泄漏问题排查和解除3

133 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第23天,点击查看活动详情

消息转发 -> 使用中间件NSProxy

同样的,我们可以使用一个中间件,使NSTimer强引用中间件,而中间件弱引用UIViewController,从而打破引用环。

对于中间件,我们选用比NSObject更为轻量级的NSProxy

中间件的实现代码如下:

@interface ORCWeakProxy : NSProxy

@property (nonatomic, readonly, weak) id target;

- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation ORCWeakProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[self.class alloc] initWithTarget:target];
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    return [self.target respondsToSelector:aSelector];
}

// 转发目标选择器
- (id)forwardingTargetForSelector:(SEL)selector {
    return self.target;
}

// 函数执行器
- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}

// 方法签名的选择器
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

@end

使用时的代码如下:

    ORCWeakProxy *proxy = [ORCWeakProxy proxyWithTarget:aTarget];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:ti
                                                  target:proxy
                                                selector:aSelector
                                                userInfo:nil
                                                 repeats:YES];

中间件的加入,使得NSTimertarget成为中间件,而中间件UIViewController是弱引用,所以UIViewController可以被释放。

因为NSTimertarget成为中间件,所以NSTimer在执行定时任务时,会调用中间件selector方法,然而中间件肯定是没有这个selector方法的,所以我们需要在中间件中进行方法的转发。我们使用forwardingTargetForSelector:使得响应selector方法的对象转移为self.target,即UIViewController

既然方法已经被转发了,后续的消息转发接口就不会被执行了,那么我们还有必要重写methodSignatureForSelector:forwardInvocation:吗?

答案是:很有必要! 。因为,当UIViewController被释放之后,就会出现在target上找不到selector,如果不重写,那么恭喜你,崩溃等着你(手动坏笑)!不过这两个方法只要实现,随便写写就行,只要有,其它都不要求。

不过使用这种方式,有以下几点需要注意:

  1. NSProxy是一个虚类,所以我们无法直接使用,而是创建一个该类的子类。
  2. 重要的事情又来了,在UIViewControllerdealloc方法中,记得加上[self.timer invalidate],谢谢(手动微笑)!如果不手动释放NSTimerNSTimer依旧会持续执行定时任务,虽然你看不到,但它就在那里执行。有兴趣的小伙伴可以在中间件forwardInvocation:打个断点试试。

中间件解决NSTimer的循环引用问题的目的已经达到了。但是在每个使用的地方,都需要引入ORCWeakProxy的头文件,这让我用起来有点不乐意啊!所以我们又添加一个NSTimer分类,代码如下:

@interface NSTimer (NoRetainCycleWithProxy)

@end

@implementation NSTimer (NoRetainCycleWithProxy)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self orc_exchangeSelector:@selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)
                        toSelector:@selector(orc_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)];

        [self orc_exchangeSelector:@selector(timerWithTimeInterval:target:selector:userInfo:repeats:)
                        toSelector:@selector(orc_timerWithTimeInterval:target:selector:userInfo:repeats:)];
    });
}

+ (NSTimer *)orc_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    return [self orc_timerWithTimeInterval:ti
                                    target:[ORCWeakProxy proxyWithTarget:aTarget]
                                  selector:aSelector
                                  userInfo:userInfo
                                   repeats:yesOrNo];
}

+ (NSTimer *)orc_scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    return [self orc_scheduledTimerWithTimeInterval:ti
                                             target:[ORCWeakProxy proxyWithTarget:aTarget]
                                           selector:aSelector
                                           userInfo:userInfo
                                            repeats:yesOrNo];
}

@end

我们在分类里交换了创建NSTimer的方法,在新的方法里来创建NSTimer,并将中间件作为target传给这个NSTimer。这样,我们就可以无感知的使用中间件来解决NSTimer的循环引用问题了(nice)!

其实写到这里,关于消息转发 -> 使用中间件NSProxy来处理NSTimer的循环引用问题,就已经写完了。但是说实话,总是被提醒:要我们手动释放NSTimer,否则就会造成内存泄漏!这让我怎么的都不是很爽。秉着如果觉得不爽,那就要让自己爽起来的态度,我又在中间件中加了一些小东西:

  1. ORCWeakProxy添加属性timer
  2. forwardingTargetForSelector:target进行判断,如果targetnil则认为target已经被释放,这个时候就释放timer

代码片段如下:

@interface ORCWeakProxy : NSProxy

@property (nonatomic, readonly, weak) id target; 
@property (nonatomic, weak) id timer; 

- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;

@end
- (id)forwardingTargetForSelector:(SEL)selector {
    // 如果发现target为nil后,就停掉timer
    if (self.target == nil) {
        [self.timer invalidate];
    }
    return self.target;
}
  1. timer属性使用weak修饰,避免出现循环引用问题。
  2. targetnil后,调用invalidate方法释放timer,那么就不需要再在考虑UIViewController被释放,而NSTimer没被释放的问题了(手动开心)。

以上,我们在使用NSTimer时,只需要调用接口创建NSTimer对象,并使用它满足我们的各种需求,而不再需要去关心,它会不会在满足我们的需求之后,对我们造成什么不好的影响了。