美文网首页iOS开发攻城狮的集散地
实现NSTimer解耦及衍生的思考

实现NSTimer解耦及衍生的思考

作者: XTShow | 来源:发表于2018-03-26 10:40 被阅读19次

category + 替换target + 模仿新api = 循环引用远离NSTimer

前言

这篇文章的由来是当初去滴滴面试的时候,面试官小哥问的一个问题:如何避免NSTimer循环引用呢?第一反应就是在viewDidDisappear中invalidate掉timer;又追问:如果业务很复杂到你不能在viewDidDisappear中invalidate呢?当时心里还在暗暗疑问:什么业务能在VC都消失了还需要timer,但临时想了一下,还是大致说了一下利用中间对象解耦的方式。面试小哥表示赞同,并点了我一句:代理。
离开的路上稍微想了一下,发现确实不能完全依赖于控制器啊的viewDidDisappear之类的只有部分类才有的方法来执行timer的释放,因为timer不一定就放在VC中啊!所以需要创造一种能够通用的NSTimer的解耦方式,所以就有了这篇文章。
(PS:滴滴的面试小哥真的好腼腆好害羞啊~ 但态度什么的都超好~ 哈哈哈)

基于代理的实现方式

  • NSTimer的需求对象创建协议,使用中间对象服从该协议;
  • NSTimer的target为中间对象,selector为代理方法;
  • 中间对象实现的代理方法中,需求对象执行NSTimer实际需要调用的方法。

详细来说下
NSTimer的需求对象创建协议:

@protocol PresentVCWeakTimerDelegate<NSObject>
- (void)useTimer:(NSTimer *)timer;
@end

需求对象生成timer,这里我们利用timer的userInfo来进行方法和参数的传递:

//此处可以将TimerManager生成一个单例模式,全局所有的timer的处理都可以由他来进行,但此处为了演示manager的释放,故如此实现。
//传入需求对象(一般就是当前self),注意此处被manager全局持有时,要使用weak修饰,不然manager和self相互持有,仍然无法释放。
TimerManager *manager = [[TimerManager alloc] initWithObj:self];

//千万不要讲self封入userInfo之中,因为timer和userInfo是强引用的关系,会破坏解耦的目的。
NSDictionary *userInfo = @{
                           @"SEL":NSStringFromSelector(@selector(dosth:)),
                           @"para":@{
                                   @"name":@"XTShow",
                                   @"age":@"18"
                                   }
                           };

//此处的target是代理对象,selector是代理协议中的方法
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.delegate selector:@selector(useTimer:) userInfo:userInfo repeats:YES];

中间对象要将自身设置为需求对象的代理对象,并实现代理方法:

- (instancetype)initWithObj:(NSObject *)obj
{
    self = [super init];
    if (self) {
        self.delegateObj = obj;
        if ([obj isKindOfClass:[PresentVC class]]) {
            PresentVC *realobj = (PresentVC *)obj;
            realobj.delegate = self;
        }
    }
    return self;
}

代理方法中实际上是在让需求对象执行真正需要用timer里调用的方法:

-(void)useTimer:(NSTimer *)timer{
    NSString *selStr = timer.userInfo[@"SEL"];
    NSDictionary *para = timer.userInfo[@"para"];
    SEL selector = NSSelectorFromString(selStr);
    [self.delegateObj performSelector:selector withObject:para];
    
    //performSelector会报黄色警告,如有介意,替代方法如下
    //IMP imp = [self.delegateObj methodForSelector:selector];
    //void (*func)(id,SEL,NSDictionary *) = (void *)imp;
    //func(self.delegateObj,selector,para);
}

缺点:
需要使用NSTimer的类都要专门实现一个协议,稍微有点麻烦。

基于category的实现方式

期间我又拜读了學徒杨小胖的文章,发现了一种更简便易用的方式。但在一些小点上,还是有一些个人的看法,稍作修改后,总结出如下方案。

使用NSTimer的category,将timer的target从NSTimer需求对象替换成NSTimer类。

通过category新增的方法:

static NSString * const BlockKey = @"BlockKey";
typedef void(^SelectorBlock)(NSTimer *timer);

@interface NSTimer()

@property (nonatomic,copy) SelectorBlock block;

@end

@implementation NSTimer (CycleRetainGetOut)

+ (NSTimer *)XT_scheduledTimerWithTimeInterval:(NSTimeInterval)ti block:(void(^)(NSTimer *timer))block userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
    
    NSTimer *timer = [self scheduledTimerWithTimeInterval:ti target:self selector:@selector(performBlock:) userInfo:userInfo repeats:yesOrNo];
    timer.block = block;

    return timer;
}

@end

其中的block就是需求对象需要执行的方法,而且此处我没有使用userInfo来传递block,而是在category中新增了一个block属性:

#import <objc/runtime.h>

- (void)setBlock:(SelectorBlock)block {
    objc_setAssociatedObject(self, &BlockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (SelectorBlock)block {
    return objc_getAssociatedObject(self, &BlockKey);
}

用他来传递block,保证需求对象的方法的执行:

+ (void)performBlock:(NSTimer *)timer {
    if (timer.block) {
        timer.block(timer);
    }
}

同时保证userInfo这个参数不会被废掉。

在iOS10中,新增了3个含block的NSTimer初始化方法,而且自带防循环引用的“特效”!通过观察这三个api发现,他们会将timer作为block的参数提供给需求对象,因此,我也进一步将当前的timer放入了block,传递给需求对象,毕竟timer中还是有一些属性可能会使用到的。

总结以上,实际使用时,已经与官方api很相似了:

#import "NSTimer+CycleRetainGetOut.h"

__weak __typeof__(self)weakSelf = self;
self.timer = [NSTimer XT_scheduledTimerWithTimeInterval:1 block:^(NSTimer *timer) {
    [weakSelf dosthWithTimer:timer];
} userInfo:@"useTimerInWeak" repeats:YES];

还有一点,就是

-(void)dealloc{
    [self.timer invalidate];
}

的问题。在上面的两种方式中,都已经可以保证需求对象的正常释放和timer停止调用对象。但是仍然建议在dealloc方法中对timer进行invalidate处理
从invalidate的官方注释中就可以发现:

This method is the only way to remove a timer from an NSRunLoop object.
...
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

这个方法是唯一的能够将timer从runloop中移除的方式,而且还能解决userInfo的强引用问题。
我们只是解决了循环引用问题,而并没有处理timer与runloop的关系,因此invalidate方法还是建议调用的。
还有就是我看到很多教程中在invalidate后还会将timer=nil,个人认为这步其实是不需要的,因为通过log可以发现,self.timer在invalidate之后,已经置为null了,再nil一下,效果重复,不会起到其他作用。

以上就是让NSTimer远离循环引用的解决方案,

但是!

在测试的过程中,我还是遇到了一些问题,想与大家分享一下,也希望能有大神不吝赐教,为我指点迷津。

在常见的NSTimer解耦的文章中,都是在强调需求对象的释放,但是timer的释放呢?只是需求对象被释放和停止方法调用就能说明timer被释放了吗?

判断一个对象是否被释放,最直观的就是观察其dealloc方法是否被调用

我通过两种方式来尝试获取NSTimer的dealloc方法:

1.创建NSTimer的子类
实践中会发现NSTimer的子类只能通过new来初始化,常规的scheduledTimerWithTimeInterval之类的初始化方法,使用后会直接崩溃。通过查阅资料1资料2发现,NSTimer是一个“class cluster”,类簇,并不能创建子类;苹果的官方文档更加直白

Subclassing Notes

Do not subclass NSTimer

直接就不允许创建子类。

2.在category中通过method_exchangeImplementations交换dealloc
在NSTimer的category中,重写load方法,在其中交换dealloc和自定义方法。然后发现还是不会调用,个人认为,在category中被替换的dealloc方法并不是NSTimer类真正的dealloc方法,而是我们新增的一个方法,所以即使timer真的调用了dealloc方法,也不会走此处我们新增的这个dealloc。(还是category用得少啊。。。)

既然如此,那么我就采用微小问题巨大化的方式来看一下吧:创建10000个timer!

- (void)checkTimerRelease {
    
    self.timerArray = [NSMutableArray array];
    
    __weak __typeof__(self)weakSelf = self;
    for (int i = 0; i < 10000; i++) {
        
        NSTimer *timer = [NSTimer XT_scheduledTimerWithTimeInterval:1 block:^(NSTimer *timer) {
            [weakSelf dosth];
        } userInfo:@"asd" repeats:YES];
        
        [self.timerArray addObject:timer];
    }
}

-(void)dealloc{
    for (NSTimer *timer in self.timerArray) {
        [timer invalidate];
    }
}

果不其然!需求对象(此处为VC)能够正常释放,方法调用也停止了,但是!因为生产了10000个timer而增加的6MB左右的内存并没有释放掉。此处我又在猜想,是否timer = nil真的有释放内存的效果,还有承载timer的timerArray的大小在timer释放后是否还是那么大?所以又进行了修改:

-(void)dealloc{
    for (NSTimer __strong *timer in self.timerArray) {
        [timer invalidate];
        timer = nil;
    }
    self.timerArray = nil;(或self.timerArray = [NSMutableArray array];)
}

事实证明,并木有用~

难道是基于category的方法有问题?用基于代理的方式来试验下。。。并不能将10000个都解耦,因为代理是一对一的。。。
那么就用最传统的公认的方式来尝试下:

- (void)checkTimerReleaseInTradition {
    
    self.timerArray = [NSMutableArray array];
    
    for (int i = 0; i < 10000; i++) {
        
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(dosth) userInfo:nil repeats:YES];//即使此时repeats设置为NO,也不会释放内存
        
        [self.timerArray addObject:timer];
    }
}

-(void)viewDidDisappear:(BOOL)animated{
    [super viewDidDisappear:animated];

    for (NSTimer *timer in self.timerArray) {
        [timer invalidate];
    }
}

哎呦!内存还是没释放掉!

那么再用iOS10中的block系列api尝试下:

- (void)creatTimerInNewApi {
    __weak __typeof__(self)weakSelf = self;
    for (int i = 0; i < 10000; i++) {
        if (@available(iOS 10.0, *)) {
            [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
                [weakSelf dosth];
            }];
        }
    }
}

不行不行还是不行啊~内存仍没有释放掉。

难道这仍旧是引用的原因吗?不应该啊,需求对象已经释放掉了,谁还在引用着timer呢?难道是timer的特性?那么我们继续往表层走,不使用NSTimer了,直接使用最基本的NSObject。

- (void)newOBj {
    self.timerArray = [NSMutableArray array];
    for (int i = 0; i < 10000; i++) {
        NSObject *obj = [NSObject new];
        [self.timerArray addObject:obj];
    }
}

内存还是没完全释放掉!但应该是因为NSObject实例对象所需的内存空间很小,所以未被释放掉的内存空间也很小。

最后我又尝试了用@autoreleasepool将创建的对象还有数组都暴露进去,还是没有用。

那么此时问题就不应该局限于NSTimer或者循环引用的问题了,而是有一些我并不了解的内存管理机制:因为这里的对象的retain计数已经是0了,但并没有像当初学习的时候所说的,立即被释放掉,而是仍占用着内存;亦或是因为,我们在每次创建对象的时候,会有类似于记录的附加信息写入内存?个人认为不太可能,如果真是那样的话,那么记录的大小几乎与创建的对象一样大了,太夸张了。

总结

那么现在来看,上面的两种针对NSTimer的解耦方式是没有问题的,各位可以放心使用。
问题在于,对于大量对象的内存占用,系统内部到底是如何处理的呢?希望有大神能够指点迷津啊!万分感谢!


Demo

相关文章

  • 实现NSTimer解耦及衍生的思考

    category + 替换target + 模仿新api = 循环引用远离NSTimer 前言 这篇文章的由来是当...

  • 解耦

    解耦 对于大型重构, 最有效的手段就是 解耦, 解耦的目的使实现代码高聚合、松耦合。 解耦为何如此...

  • 封装、继承、多态

    封装:隐藏实现细节通过公共方法向外暴露该对象的功能作用:解耦 封装:解耦隐藏对象的实现细节通过公共方法来向外暴露该...

  • RunTime之消息转发之NSTimer循环引用的解决方案

    消息转发通常用于解耦,在此有个实际例子就是打破NSTimer的引用循环,YYKit框架中有一个YYWeakProx...

  • BeeHive学习总结

    设计原则: 解耦,避免对接口依赖使用invoke以及动态链接库实现对接口的解耦 BeeHive每个模块都是有生命周...

  • iOS 组件化(一)

    组件化 组件化就是将模块单独抽离,分层,通过制定的通讯方式,实现解耦 组件化优点 模块间的解耦 模块重用 提交团队...

  • Github 创建私有库

    基于当前GitHub规则,创建私有库方法。 实现iOS的组件化开发 组件化,是一种解耦方式,适用团队协同开发及开源...

  • Spring实战随笔(四) AOP

    之前我们学习了DI,它有助于解耦,AOP能够实现关注业务对象和横切关注点的解耦。(横切关注点cross-cutti...

  • 设计模式

    创建型 工厂作用:解耦代码代码的解耦 一个对象我要实现单例 简单的是在这个类 中把构造方法 设置为private ...

  • Spring Boot自定义注解

    利用aop我们可以实现业务代码与系统级服务例如日志记录、事务及安全相关业务的解耦,使我们的业务代码更加干净整洁。自...

网友评论

    本文标题:实现NSTimer解耦及衍生的思考

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