NSTimer的一般使用:

 1 @interface ViewController : UIViewController
 2 @property (nonatomic, strong) NSTimer *timer;
 3 @end
 4 
 5 @implementation ViewController
 6 - (void)viewDidLoad {
 7     [super viewDidLoad];
 8     [self startTimer];
 9 }
10 
11 - (void)startTimer {
12     self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTask) userInfo:nil repeats:YES];
13     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
14 }
15 
16 - (void)timerTask {
17     NSLog(@"do something");
18 }
19 
20 - (void)dealloc {
21     [self.timer invalidate];
22     self.timer = nil;
23 }
24 
25 @end

 

 

主要代码如上, 现在问题出来了, 当我们页面移除之后, 控制台依然一直输出 do something, 说明timer依然存在, 那么viewController还存在, 都没有被销毁, 断点dealloc发现根本不进断点.

网上查了一圈,发现如下解释:

为了保证参数的生命周期,NSTimer会对target对象retain一次,做强引用。以保证即便target销毁了,定时器还能正常调用timeEvent。因为定时器要加到RunLoop中,所以RunLoop强引用着NSTimer,一般情况下你的target就是当前的控制器,如果你想让控制器如你所愿的销毁了,首先得销毁NSTimer。不然NSTimer强引用着self,self就无法销毁,从而导致内存泄漏。
 
timer被schedule的时候,timer会持有target对象,NSRunLoop对象会持有timer。当invalidate被调用时,NSRunLoop对象会释放对timer的持有,timer会释放对target的持有。除此之外,没有途径可以释放timer对target的持有。所以解决内存泄露就必须撤销timer,若不撤销,target对象将永远无法释放。

 

最简单的解决办法:

 

  1. 在viewDidDisappear中调用[self.timer invalidate]来结束timer的任务;
  2. 使用block而不用target,这样肯定不会对viewController做retain;

 

OK, 接下来是要说的重点, 既然有最简单的解决办法, 那就有不那么简单的办法: 做一个中间代理, 用消息转发来找到对应的方法实现, 同时timer和ViewController之间不做相互强引用, 以此来解决内存无法释放的问题, 这样可以实现不在ViewController生命周期中的NSTimer的引用问题.

这个地方我们用一个官方给的中间代理类NSProxy来实现中间代理, 具体代码如下:

@interface TimerWeakProxy : NSProxy

@property (nonatomic, weak) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end

@implementation TimerWeakProxy

+ (instancetype)proxyWithTarget:(id)target {
    TimerWeakProxy *proxy = [TimerWeakProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

@end

 

接下来就是把NSTimer的target换成中间代理:

self.timer = [NSTimer timerWithTimeInterval:1.0 target:[TimerWeakProxy proxyWithTarget:self] selector:@selector(timerTask) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

 

至此, 中间代理改造完毕, 其实也没有那么复杂, 重点就是NSProxy这个类, 官方的文档是这样说的:

Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.

NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation: and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forwardInvocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. methodSignatureForSelector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethodSignature object accordingly. See the NSDistantObject, NSInvocation, and NSMethodSignature class specifications for more information.

简单来说, 就是说这是一个抽象类, 不提供初始化方法, 需要子类来提供初始化方法, 并且需要重写forwardInvocation: 和 methodSignatureForSelector: 方法来做消息转发.

 

参考博文: https://www.jianshu.com/p/ee58de47fa5c

              https://www.jianshu.com/p/d4589134358a

 

posted on 2019-09-06 17:44  Gary_小咖  阅读(467)  评论(0编辑  收藏  举报