OC内存管理-NSTimer

一、NSTimer 使用

1.1 使用问题

static int num = 0;
@property (nonatomic, strong) NSTimer *timer;

self.timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
//立即触发一次
[self.timer fire];

- (void)timerAction {
    num++;
    NSLog(@"timerAction: %d",num);
}

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

对于上面使用timerWithTimeInterval方式创建的timer需要主动调用NSRunLoopaddTimer触发计时器,当然也可以通过scheduledTimerWithTimeInterval方式来创建:

self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

区别是不需要再调用NSRunLoop的触发方法,scheduledTimerWithTimeInterval内部会主动调用,汇编还原源码如下:

/* @class NSTimer */
+(int)scheduledTimerWithTimeInterval:(int)arg2 target:(int)arg3 selector:(int)arg4 userInfo:(int)arg5 repeats:(id)arg6 {
    CFRunLoopAddTimer(CFRunLoopGetCurrent(), [[objc_allocWithZone(arg0, arg1, arg2, arg3, arg4, arg5) initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:r2] interval:arg2 target:arg3 selector:arg4 userInfo:arg5 repeats:r7] autorelease], **_kCFRunLoopDefaultMode);
    r0 = r19;
    return r0;
}

/* @class NSTimer */
+(int)timerWithTimeInterval:(int)arg2 target:(int)arg3 selector:(int)arg4 userInfo:(int)arg5 repeats:(id)arg6 {
    r0 = [objc_allocWithZone() initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:r2] interval:arg2 target:arg3 selector:arg4 userInfo:arg5 repeats:r7];
    r0 = [r0 autorelease];
    return r0;
}

void -[NSCFTimer invalidate]() {
    CFRunLoopTimerInvalidate(r0);
    return;
}

以上timer在使用时候的坑点是会造成循环引用,不会走dealloc。在这个过程中self -> timer -> self。我们都清楚timerself弱引用并不能解决问题,在这个过程中是runloop持有了timertimer持有了self。而即使self使用__weak修饰赋值给timertarget并不能解决问题是因为timer持有target是强持有,__weak能解决block的循环引用是因为block捕获的是__weak类型变量。

官方文档说明了targettimer强持有:

image.png

invalidate的时候timer将不再持有target

1.2 问题处理

既然invalidate调用后timer不再持有target,那么就在合适的时机调用就可以了。

1.2.1 viewWillDisappear 中处理

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}

viewWillDisappear中有个问题是退出页面和进入下一个页面都会调用这个API,如果只在退出页面的情况下要调用则需要做标记相关的处理。并且还有滑动返回等相关手势操作。

1.2.2 didMoveToParentViewController

更好的处理方式是在didMoveToParentViewController中进行相关逻辑处理:

- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
    }
}

以上在业务层面处理循环引用并不优雅,并且随着业务逻辑复杂度的增加处理成本也越来越高。

二、block 方式

iOS10以后系统提供了block形式的API供我们调用,这种形式timer不会持有target

self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    num++;
    NSLog(@"timerAction: %d",num);
}];

需要在dealloc中调用invalidate。对于iOS10之前的版本可以自己实现一个,给NSTimer添加一个分类:

+ (NSTimer *)hp_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats
{
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(hp_blockInvoke:) userInfo:[block copy] repeats:repeats];
}

+ (void)hp_blockInvoke:(NSTimer *)timer
{
    void (^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}

在这里将block的实现传递给了userInfo,在selector中取到了block进行了调用。target传递的是NSTimer类对象。这样selftimer就没有互相持有了。

调用:

self.timer = [NSTimer hp_scheduledTimerWithTimeInterval:3.0 block:^{
    num++;
    NSLog(@"timerAction: %d",num);
} repeats:YES];

同样的需要在dealloc中调用invalidate

三、临时 Target

@property (nonatomic, strong) id target;

self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(timerAction1), (IMP)timerActionFunc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self.target selector:@selector(timerAction1) userInfo:nil repeats:YES];

void timerActionFunc(){
    num++;
    NSLog(@"timerActionFunc: %d",num);
}

既然是由于target造成的循环引用,那么只要target不是self就可以解决循环引用了,使用NSObject作为临时target传递给timerNSObject添加一个timerActionFunc,最终timer会调用到timerActionFunctimerAction1是一个不存在的方法。同样需要主动调用invalidate

四、NSProxy 转发

@property (nonatomic, weak) id object;

+ (instancetype)proxyWithTransformObject:(id)object {
    HPProxy *proxy = [HPProxy alloc];
    proxy.object = object;
    return proxy;
}

//方式一:备用消息接收者(消息快速转发)
//为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self。
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

////方式二:慢速消息转发
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
//
//    if (!self.object) {
//        NSLog(@"error");
//    }
//    return [self.object methodSignatureForSelector:sel];
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation {
//
//    if (self.object) {
//        [invocation invokeWithTarget:self.object];
//    } else {
//        NSLog(@"error");
//    }
//}

在消息转发过程中,可以将消息的响应者调用回target。这样就避免了互相持有(object在这里是弱引用)。
调用:

@property (nonatomic, strong) HPProxy *proxy;

self.proxy = [HPProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];

同样的需要在dealloc中调用invalidatetarget就不被runloop间接持有了。

五、Wapper 包装 NSTimer

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;

- (instancetype)hp_initWithTimeInterval:(NSTimeInterval)timeInterval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    if (self == [super init]) {
        self.target = aTarget; // vc
        self.aSelector = aSelector; // 方法 -- vc 释放
        if ([self.target respondsToSelector:self.aSelector]) {
            //本类添加 aSelector
            Method method    = class_getInstanceMethod([self.target class], aSelector);
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], aSelector, (IMP)timeActionWapper, type);
            //aSelector 是传递进来的。
            self.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

//提供给外界主动调用 timer 释放
- (void)hp_invalidate {
    [self.timer invalidate];
    self.timer = nil;
}

void timeActionWapper(HPTimerWrapper *warpper) {
    //交给 target 自己处理 aSelector
    if (warpper.target) {
        void (*hp_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
        hp_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
    } else { // warpper target 不存在,直接释放 timer
        [warpper hp_invalidate];
    }
}

NSTimer包组成了HPTimerWrapperself只与HPTimerWrapper进行交互。
这样设计完全隔离了selftimertimerwapper构成循环引用,这里循环引用解除依赖target。当主动调用hp_invalidate解除或者target(也就是vc)消失后就主动解除了。

调用:

@property (nonatomic, strong) HPTimerWrapper *timerWapper;

self.timerWapper = [[HPTimerWrapper alloc] hp_initWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

这样做的好处是不需要在dealloc中调用hp_invalidate,在timeActionWapper中会自动调用。

总结
NSTimer循环引用问题是由于runloop -> timer -> self -> timer造成的,只要打破互相持有就能解决循环引用问题。

主要有3种方式处理:

  • 1.业务层处理循环引用
    • 1.1viewWillDisappear中调用invalidate
    • 1.2didMoveToParentViewController中调用invalidate
  • 2.中介者处理
    • 2.1 封装block形式,NSTimer作为了中介者。iOS10以后系统提供了block形式的API
    • 2.2 临时target形式,NSObject动态添加方法,NSTimer设置targetNSObject
    • 2.3 NSProxy作为NSTimertarget通过消息快速/慢速转发回调用发。
    • 2.4 warpper封装NSTimerNSTimerself彻底隔离,self作为targetwarpper中调用target中的方法。相比其它方式,warpper有自动调用invalidate的逻辑,不需要在dealloc中主动调用。
  • 3.GCD封装Timer,具体可以查看GCD底层分析(三)4.3 章节

TimerDemo
HPTimer

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容