美文网首页
NSTimer内存问题分析及优雅使用姿势

NSTimer内存问题分析及优雅使用姿势

作者: 长不大的帅小伙 | 来源:发表于2018-11-26 23:45 被阅读16次

    NSTimer特别容易出现内存泄露问题,这篇文章会分析一下为什么会出现内存泄露,以及如何优雅的解决这个问题。

    NSTimer导致内存问题的原因分析

    @interface ViewController ()
    @property (nonatomic, strong) NSTimer *timer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad
    {
        _timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)timerAction {}
    @end
    

    这段代码是常见的NSTimer使用方法,然而这段代码会有一个隐患,self无法正常被释放,除非在selfdealloc执行之前调用[_timer invalidate]self才能正常被释放。此时他们在内存中的引用关系如图

    timer内存引用关系简图
    我这个例子用的strong修饰timer,所以self强引用着timer, 同时timer也会强引用selftimer运行需要添加到当前线程所运行的runloop中,所以runloop也强引用着timer。当self被pop掉的时候,此时self始终被timer强引用着,从而无法在内存中释放,这也是为什么不能在selfdealloc方法中去做[_timer invalidate]的原因。

    最笨的解决方法:在dealloc前手动去释放

    上面这段代码,已经出现了内存泄露,只要app没被杀死,self会一直在内存中,那么有种最笨的方法就是手动去释放,例如可以在- (void)viewWillDisappear:(BOOL)animated方法中去做[_timer invalidate]操作,执行完invalidate后,此时runloop不在持有timer, timer也不在持有self,即图中的2和3箭头断开,此时没有任何对象引用着self,self的引用计数为0,从而能够在内存中被释放,self被释放后,timer此时也没对象引用着,即1箭头断开,从而timer也在内存中被释放。
    当然这种方法也是会受到一定的业务限制的,例如self上继续push一个viewController的时候,runloop不在引用timer, timer不在引用self, 此时selftimer都在内存中,只不过timer不在运行着了。假如业务场景要求push的时候,timer正常工作,只有selfpop的时候才杀掉timer,此时这种处理方式就显得力不从心了。

    那么有什么通用方案能让使用者不在担心内存问题吗

    根据上面的分析,我们知道,真正导致self无法释放的原因是timer强引用着,那么如果说timer不强引用self的话,self就能在pop的时候愉快的释放了,开发者也能够在selfdealloc方法中去做[_timer invalidate]操作杀死timer了。

    于是在这种思路下,我做了如下尝试

    @interface ViewController ()
    @property (nonatomic, strong) NSTimer *timer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad
    {
        __weak typeof(self) weakSelf = self;
        _timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)timerAction {}
    @end
    

    这段代码和上面的代码只有self和weakSelf的差别,此时内存中的引用关系如图

    timer内存引用关系简图
    注意虚线这里表示弱引用,self的引用计数不+1的。当self被pop的时候,由于没有其它对象引用着他,所以能够正常释放,从而能够正常执行selfdealloc方法,此时1箭头断开,在selfdealloc方法中执行[_timer invalidate],此时3和2断开,timer也从内存中被释放。
    看起来没有任何地方违背理论基础,貌似一切大功告成。然后通过实际调试测试发现,虽然我试图让timer持有weakSelf,然而实际并没生效,timer依然会强引用self

    所以这种尝试失败,不过具体更深层次的原因我还不知道,我只知道我想让timer强引用weakSelf,而timer依然强引用了self, 可能苹果出于安全考虑,为了确保执行事件的target始终存在,在内部做了什么特殊操作,因为self一旦被释放,weakSelf将会指向nil, timer的target对象也就为nil了。

    终极解决姿势

    说道终极解决姿势得感谢github开源代码的各路豪杰,我也是在第三方库中看到的解决方案。这里其实就是通过消息转发机制巧妙的解决了这个问题。

    @interface ViewController ()
    @property (nonatomic, strong) NSTimer *timer;
    @end
    
    @implementation ViewController
    - (void)viewDidLoad
    {
        _timer = [NSTimer timerWithTimeInterval:1 target:[JLWeakProxy proxyWithTarget:self] selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    }
    
    - (void)timerAction {}
    @end
    

    此时他们在内存中的引用关系如图


    timer内存引用关系简图

    这里的核心点就在JLWeakProxy上,timer的直接target对象其实是JLWeakProxy, 也就是说,timer其实是通过JLWeakProxy对象去执行selector方法的,只不过在JLWeakProxy内部,弱引用着self, 并且会把消息转发给self去执行selector方法。
    从内存引用简图可以看出,popself的时候,self能够正常释放,从而dealloc方法能够正常被执行,在dealloc中做[_timer invalidate]操作,此时runloop不在引用timer, timer 也不在引用JLWeakProxy,从而内存也就都被释放了。

    关于消息转发这里不做解释说明,感兴趣的可以看看我整理的iOS Runtime: 消息转发,下面贴出JLWeakProxy实现。

    @interface JLWeakProxy : NSProxy
    
    @property (nullable, nonatomic, weak, readonly) id target;
    
    - (instancetype)initWithTarget:(id)target;
    
    + (instancetype)proxyWithTarget:(id)target;
    
    @end
    
    @implementation JLWeakProxy
    
    - (instancetype)initWithTarget:(id)target {
        _target = target;
        return self;
    }
    
    + (instancetype)proxyWithTarget:(id)target {
        return [[JLWeakProxy alloc] initWithTarget:target];
    }
    
    - (id)forwardingTargetForSelector:(SEL)selector {
        return _target;
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation {
        void *null = NULL;
        [invocation setReturnValue:&null];
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
        return [NSObject instanceMethodSignatureForSelector:@selector(init)];
    }
    
    - (BOOL)respondsToSelector:(SEL)aSelector {
        return [_target respondsToSelector:aSelector];
    }
    
    - (BOOL)isEqual:(id)object {
        return [_target isEqual:object];
    }
    
    - (NSUInteger)hash {
        return [_target hash];
    }
    
    - (Class)superclass {
        return [_target superclass];
    }
    
    - (Class)class {
        return [_target class];
    }
    
    - (BOOL)isKindOfClass:(Class)aClass {
        return [_target isKindOfClass:aClass];
    }
    
    - (BOOL)isMemberOfClass:(Class)aClass {
        return [_target isMemberOfClass:aClass];
    }
    
    - (BOOL)conformsToProtocol:(Protocol *)aProtocol {
        return [_target conformsToProtocol:aProtocol];
    }
    
    - (BOOL)isProxy {
        return YES;
    }
    
    - (NSString *)description {
        return [_target description];
    }
    
    - (NSString *)debugDescription {
        return [_target debugDescription];
    }
    
    @end
    

    相关文章

      网友评论

          本文标题:NSTimer内存问题分析及优雅使用姿势

          本文链接:https://www.haomeiwen.com/subject/bepbqqtx.html