weak能否解决NSTimer释放的问题

我们都知道Timer只有在invalidate之后才会从runloop中被移除,repeat为NO的timer在执行一次之后就会自动移除;我们在使用重复的timer的时候,如果是想在dealloc中调用timer的invalidate方法,往往会造成泄漏,target的dealloc方法不会调用,放在界面viewWillAppear创建和viewWillDisappear的时候invalidate,很多场景也不适用(eg: targetVC 跳转到另外界面再回来)

那我们使用weak来能否解决timer释放的问题了?

尝试1:使用weak修饰timer实例

@property (weak, nonatomic) NSTimer *testRepeatTimer;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.testRepeatTimer = [NSTimer scheduledTimerWithTimeInterval:1
                                                            target:self
                                                          selector:@selector(testTimerAction)
                                                          userInfo:nil
                                                           repeats:YES];
}

- (void)dealloc {
    [_testRepeatTimer invalidate];
}

调试发现,dealloc方法也不会调用;为什么了?先看看函数的注释

  • -(void)invalidate
Summary

Stops the timer from ever firing again and requests its removal from its run loop.
Declaration

- (void)invalidate;
Discussion

This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.

从这里注释可知:runloop是强持有timer的,声明为weak只是vc不持有
再看看invalidate内部做了什么处理:

/**
 * Marks the timer as invalid, causing its target/invocation and user info
 * objects to be released.<br />
 * Invalidated timers are automatically removed from the run loop when it
 * detects them.
 */
- (void) invalidate
{
  /* OPENSTEP allows this method to be called multiple times. */
  _invalidated = YES;
  if (_target != nil)
    {
      DESTROY(_target);
    }
  if (_info != nil)
    {
      DESTROY(_info);
    }
}

invalidate内部是将强持有的target和info进行释放,并且标记timer的invalidated为YES,runloop在检测到timer被invalidate之后会自动从runloop中移除

DESTROY的实现没有找到,不过在GUN的源码中发现类似的定义,作用就是release然后置为nil

/* Destroy holder for internal ivars.
 */
#define GS_DESTROY_INTERNAL(name) \
if (nil != _internal) { [_internal release]; _internal = nil; }
  • +(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
Summary

Creates a timer and schedules it on the current run loop in the default mode.
Declaration

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
Discussion

After ti seconds have elapsed, the timer fires, sending the message aSelector to target.
Parameters

ti  
The number of seconds between firings of the timer. If ti is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead.
target  
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
aSelector   
The message to send to target when the timer fires.
The selector should have the following signature: timerFireMethod: (including a colon to indicate that the method takes an argument). The timer passes itself as the argument, thus the method would adopt the following pattern:
- (void)timerFireMethod:(NSTimer *)timer
userInfo    
The user info for the timer. The timer maintains a strong reference to this object until it (the timer) is invalidated. This parameter may be nil.
repeats 
If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
Returns

A new NSTimer object, configured according to the specified parameters.
Open in Developer Documentation

从这段注释可知:timer是强持有target的直到timer invalidate
通过源码看看内部是如何实现的:

/**
 * Create a timer which will fire after ti seconds and, if f is YES,
 * every ti seconds thereafter. On firing, invocation will be performed.<br />
 * This timer will automatically be added to the current run loop and
 * will fire in the default run loop mode.
 */
+ (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti
                                 invocation: (NSInvocation*)invocation
                                    repeats: (BOOL)f
{
    id t = [[self alloc] initWithFireDate: nil
                                 interval: ti
                                   target: invocation
                                 selector: NULL
                                 userInfo: nil
                                  repeats: f];
    [[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode];
    RELEASE(t);
    return t;
}


/** <init />
 * Initialise the receive, a newly allocated NSTimer object.<br />
 * The ti argument specifies the time (in seconds) between the firing.
 * If it is less than or equal to 0.0 then a small interval is chosen
 * automatically.<br />
 * The fd argument specifies an initial fire date copied by the timer...
 * if it is not supplied (a nil object) then the ti argument is used to
 * create a start date relative to the current time.<br />
 * The f argument specifies whether the timer will fire repeatedly
 * or just once.<br />
 * If the selector argument is zero, then then object is an invocation
 * to be used when the timer fires.  otherwise, the object is sent the
 * message specified by the selector and with the timer as an argument.<br />
 * The object and info arguments will be retained until the timer is
 * invalidated.
 */
- (id) initWithFireDate: (NSDate*)fd
               interval: (NSTimeInterval)ti
                 target: (id)object
               selector: (SEL)selector
               userInfo: (id)info
                repeats: (BOOL)f
{
    if (ti <= 0.0)
    {
        ti = 0.0001;
    }
    if (fd == nil)
    {
        _date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
                 initWithTimeIntervalSinceNow: ti];
    }
    else
    {
        _date = [fd copyWithZone: NSDefaultMallocZone()];
    }
    _target = RETAIN(object);
    _selector = selector;
    _info = RETAIN(info);
    if (f == YES)
    {
        _repeats = YES;
        _interval = ti;
    }
    else
    {
        _repeats = NO;
        _interval = 0.0;
    }
    return self;
}

- (void) addTimer: (NSTimer*)timer
          forMode: (NSString*)mode
{
    GSRunLoopCtxt   *context;
    GSIArray    timers;
    unsigned      i;
    
    if ([timer isKindOfClass: [NSTimer class]] == NO
        || [timer isProxy] == YES)
    {
        [NSException raise: NSInvalidArgumentException
                    format: @"[%@-%@] not a valid timer",
         NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
    }
    if ([mode isKindOfClass: [NSString class]] == NO)
    {
        [NSException raise: NSInvalidArgumentException
                    format: @"[%@-%@] not a valid mode",
         NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
    }
    
    NSDebugMLLog(@"NSRunLoop", @"add timer for %f in %@",
                 [[timer fireDate] timeIntervalSinceReferenceDate], mode);
    
    context = NSMapGet(_contextMap, mode);
    if (context == nil)
    {
        context = [[GSRunLoopCtxt alloc] initWithMode: mode extra: _extra];
        NSMapInsert(_contextMap, context->mode, context);
        RELEASE(context);
    }
    timers = context->timers;
    i = GSIArrayCount(timers);
    while (i-- > 0)
    {
        if (timer == GSIArrayItemAtIndex(timers, i).obj)
        {
            return;       /* Timer already present */
        }
    }
    /*
     * NB. A previous version of the timer code maintained an ordered
     * array on the theory that we could improve performance by only
     * checking the first few timers (up to the first one whose fire
     * date is in the future) each time -limitDateForMode: is called.
     * The problem with this was that it's possible for one timer to
     * be added in multiple modes (or to different run loops) and for
     * a repeated timer this could mean that the firing of the timer
     * in one mode/loop adjusts its date ... without changing the
     * ordering of the timers in the other modes/loops which contain
     * the timer.  When the ordering of timers in an array was broken
     * we could get delays in processing timeouts, so we reverted to
     * simply having timers in an unordered array and checking them
     * all each time -limitDateForMode: is called.
     */
    GSIArrayAddItem(timers, (GSIArrayItem)((id)timer));
    i = GSIArrayCount(timers);
    if (i % 1000 == 0 && i > context->maxTimers)
    {
        context->maxTimers = i;
        NSLog(@"WARNING ... there are %u timers scheduled in mode %@ of %@",
              i, mode, self);
    }
}

可以看到在初始化timer的时候,内部会对target和info进行retain操作;将timer添加到runloop中的时候会将timer添加到数组中也进行了retain操作

由于runloop是强持有timer的,并且只有在调用了invalidate的时候才会将repeat的timer移除,同时timer强持有了target,导致target不会被释放,那么timer的invalidate调用放到target的dealloc中也无济于事;所以只有当我们调用了invalidate之后,NSTimer内部持有的target和info会被释放,runloop检测到timer被置为invalidated之后将timer移除,这样就能正常释放了

尝试2:使用weakSelf初始化timer


- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    __weak typeof(self) weakSelf = self;
    self.testRepeatTimer = [NSTimer scheduledTimerWithTimeInterval:1
                                                            target:weakSelf
                                                          selector:@selector(testTimerAction)
                                                          userInfo:nil
                                                           repeats:YES];
}

- (void)dealloc {
    [_testRepeatTimer invalidate];
}

调试发现,dealloc方法也不会调用;原因也同上,虽然target参数传递的是weak指针,但是timer还是强持有weak指针指向的对象,同样导致无法释放

如何解决

问题的根本:

  • timer强持有target
  • runloop强持有timer
  • repeat的timer只有在调用了invalidate之后才会被runloop释放
  • 为了timer和target的生命周期一致,我们在target的dealloc中invalidate timer
    target被强持有了,不会走dealloc,就内存泄漏了

我们发现只要能使得timer不强持有target(eg:例子中的vc),那么target的dealloc就能正常执行,timer invalidate之后,timer就会被释放了,内存泄漏解决;我们需要一个中间者能够将timer的selector转发给target,同时让timer去持有这个中间者,那么问题就解决了

OC中刚好有这样一个类NSProxy可以完成这个工作

@interface NSProxy <NSObject> {
    Class   isa;
}

+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;

- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);

// - (id)forwardingTargetForSelector:(SEL)aSelector;

@end

我们子类化NSProxy,内部weak引用target,在消息转发的时候调用target去执行;这样timer持有的是proxy,target能正常释放,target释放了,timer就释放了、proxy也会释放,内存泄漏问题解决;
具体的实现可以参照YYWeakProxy,就不重复造轮子了;使用起来也比较简单

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

推荐阅读更多精彩内容