个人第三方库:
UDUserDefaultsModel:以Model代替NSUserDefaults
YIIFMDB:直接操作Model进行增删改查,数学运算等,且sql语句易于管理
在iOS开发当中,无可避免的会涉及到定时任务,比如在发送验证码时的倒计时:
小编相信每个人都遇到过这样的需求,都很熟练的写出代码来了,如下:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFire:) userInfo:nil repeats:YES];
简单是简单,但是,这个样子写出来的代码却有两个很大的缺陷:
1.会导致内存泄漏(在@selector(timerFire:)这个方法里打印一个log,会发现这个log在pop之后还会打印)
2.如果滑动ScrollView的时候,定时器却不会走,只有松开ScrollView之后,定时器才重新走,如此会导致体验不佳
内存泄漏不是个小事,这个样子会导致很多程序上的bug。而至于滑动ScrollView时定时器不走的缺陷可以暂时稍后。
为了解决这个bug,我们先来分析NSTimer内存泄漏的原因:首先在Demo中,NSTimer在初始化的时候是放在对象(其实是一个ViewController的对象)方法中的,而当前对象self又是作为NSTimer对象的一个参数存在的,为此就导致了一个死循环,即:
解决这个bug,必须打破self->timer->self(其中->代表强引用)这种循环引用,其中self->timer这一步没法避免,只能从timer->self这里着手,让其变成timer -- self(其中--代表弱引用)。
为此,我们需要查看NSTimer的官方文档,同时也发现了如下两个方法:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
NSInvocation是什么?用过Target-Action的人一定不陌生。现在我们去翻看官方文档,第一句就已经解释清楚了:
NSInvocation
objects are used to store and forward messages between objects and between applications, primarily byNSTimer
objects and the distributed objects system
简而言之就是:NSInvocation对象会保存并转发一些信息,而且完全可以适用于NSTimer对象。而从其暴露的方法来看,只有"invocationWithMethodSignature:"这一个方法,不解释了,想了解的去看官方文档,这里直接上代码:
NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:@selector(timerFire:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = self;
invocation.selector = @selector(timerFire:);
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
然而经过测试,内存泄漏的bug依旧没有解决,@selector(timerFire:)这里面的log在pop的时候依旧在打印。仔细分析一下会发现NSTimer导致的闭环依旧没有解决,只不过是从self->timer->self演变成了self->timer->invocation->self罢了。
虽然NSInvocation并未解决,但是却提供了一个思路:假设有一个对象objectA,其对self进行一个弱引用,那么就会变成self->timer->objectA--self(其中->代表强持有,而--代表弱持有)就可以了。
在翻看大量资料之后,小编得知iOS提供了这样一个类:NSProxy。对于NSProxy的解释,官方文档是这样解释的:
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.
(自己翻译吧!小编倒是认为"stand-in"是关键词。)
那么,我们根据NSInvocation的思想(主要有两点:1.store messages 2.forward messages)去查看NSProxy的官方文档,发现有两个方法十分类似:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; // 类似store messages
- (void)forwardInvocation:(NSInvocation *)invocation; // 类似forward messages,而且这里也涉及到了NSInvocation
除此之外,还要解决一个对self弱引用的问题,为此只需要给NSProxy进行一个拓展,增加一个对对象的弱引用,继承是最好的办法。
继承自NSProxy声明一个叫NSProxyInprovement的类,并在.h当中声明一个weak修饰的属性,如下面代码:
@interface NSProxyInprovement : NSProxy
@property (nonatomic, weak) id aTarget; // 此对象要从外部传过来
@end
同时在NSProxyInprovement的.m中,实现类似NSInvocation中"store and forward messages"的两个方法,如下面代码:
@implementation NSProxyInprovement
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.aTarget methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.aTarget];
}
@end
使用起来很简单但是却比较繁琐,如下面代码:
self.proxy = [NSProxyInprovement alloc];
self.proxy.aTarget = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy selector:@selector(timerFire:) userInfo:userInfo repeats:YES];
经过测试,当pop的时候,调用了dealloc的方法,为此内存泄漏的bug算是解决了。
接下来解决上面遗留的那个滑动ScrollView的时候,定时器不走的缺陷。为此,我们看看官方文档对于@selector(scheduledTimerWithTimeInterval:target:userInfo:repeats:)的解释:
Creates a timer and schedules it on the current run loop in the default mode.
从上面的解释当中可以看到NSTimer还结合了NSRunloop的知识,并且mode类型是NSDefaultRunLoopMode,这就是问题所在:当滑动ScrollView的时候,NSRunloop的mode并不是NSDefaultRunLoopMode,而是UITrackingRunLoopMode,为此,我们需要设置一个包含既包含NSDefaultRunLoopMode又包含UITrackingRunLoopMode的mode,那就是NSRunLoopCommonModes。
完整代码如下:
self.proxy = [NSProxyInprovement alloc];
self.proxy.aTarget = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy selector:@selector(timerFire:) userInfo:userInfo repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
但是这个样子的话还有一个缺陷,那就是NSRunloop使用了两次,为了改善这个,我们使用NSTimer的另一个方法,完整代码如下:
self.proxy = [NSProxyInprovement alloc];
self.proxy.aTarget = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self.proxy selector:@selector(timerFire:) userInfo:userInfo repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
详情只需要查看NSTimer的官方文档就可以了,官方文档写的很清楚。
经过测试,当滑动ScrollView的时候,定时器不走的那个缺陷也修复了,完美。
不过,在小编看来,bug与缺陷虽然都修复了,但是代码写起来十分的繁琐,毕竟还要引入NSProxyInprovement这个类,还要创建,传值,十分的繁琐,一点都不符合组件化开发的需求。
为此小编写了一个十分简单的组件放到了Github上,并且可支持Cocoapods(不要吝啬你手里的Star)。