NSTimer in iOS

NSTimer in iOS

[[139776]]

When I was organizing the company's project a while ago, I found that the old code had a memory leak when using NSTimer. Then I organized some NSTimer related content. It's relatively simple, please forgive me.

NSTimer

fire

Let's use NSTimer to make a simple timer, outputting Fire to the console every 5 seconds. The more obvious approach is this:

  1. @interface DetailViewController ()
  2. @property (nonatomic, weak) NSTimer *timer;
  3. @end  
  4. @implementation DetailViewController
  5. - (IBAction)fireButtonPressed:(id)sender {
  6. _timer = [NSTimer scheduledTimerWithTimeInterval: 3 .0f
  7. target:self
  8. selector: @selector (timerFire:)
  9. userInfo:nil
  10. repeats:YES];
  11. [_timer fire];
  12. }
  13. -( void )timerFire:(id)userinfo {
  14. NSLog(@ "Fire" );
  15. }
  16. @end  

After running, Fire is indeed output in the console every 3 seconds. However, when we jump from this interface to other interfaces, we find that the console is still outputting Fire continuously. It seems that the Timer has not stopped.

  1. invalidate
  2.  
  3. Since it is not stopped, let's add the invalidate method in the dealloc of DemoViewController:
  4.  
  5. -( void )dealloc {
  6. [_timer invalidate];
  7. NSLog(@ "%@ dealloc" , NSStringFromClass([self class ]));
  8. }

Run again, still does not stop. The reason is that when the Timer is added to the Runloop, it will be strongly referenced by the Runloop:

  1. Note in particular that run loops maintain strong references to their timers, so you don't have to maintain your own strong reference to a timer after you have added it to a run loop.

Then Timer will have a strong reference to Target (that is, self):

  1. Target is 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.

That is to say, NSTimer strongly references self, causing self to never be released, so it cannot enter self's dealloc.

In this case, we can add an invalidate button:

  1. - (IBAction)invalidateButtonPressed:(id)sender {
  2. [_timer invalidate];
  3. }

Well, that's it. (Someone on SOF said that _timer = nil should be executed after invalidate, but I don't understand why. If you know the reason, please tell me:)

There is also this paragraph in the documentation of the invalidate method:

  1. You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.

NSTimer must be stopped on the thread where it was created, otherwise the resources will not be released correctly. It seems that there are many pitfalls.

dealloc

So the question is: If I just want this NSTimer to keep outputting until DemoViewController is destroyed, how can I stop it?

NSTimer is strongly referenced by Runloop. If you want to release it, you must call the invalidate method.

But I want to call the invalidate method in the dealloc of DemoViewController, but self is strongly referenced by NSTimer.

So I still have to release NSTimer first, but I can't release it without calling the invalidate method.

However, I cannot call the invalidate method if you don't enter the dealloc method.

Um…

HWWeakTimer

weakSelf

The key to the problem is that self is strongly referenced by NSTimer. If we can break this strong reference, the problem will be solved naturally. So a very simple idea is: weakSelf:

  1. __weak typeof(self) weakSelf = self;
  2. _timer = [NSTimer scheduledTimerWithTimeInterval: 3 .0f
  3. target:weakSelf
  4. selector: @selector (timerFire:)
  5. userInfo:nil
  6. repeats:YES];

However, this is useless. The difference between __weak and __strong here is that if self is released during the execution of these two lines of code, the target of NSTimer will become nil.

target

Since we can't extract self through __weak, we can create a fake target for NSTimer. This fake target is like an intermediary agent, and its main job is to step forward and take over the strong reference of NSTimer. The class declaration is as follows:

  1. @interface HWWeakTimerTarget : NSObject
  2. @property (nonatomic, weak) id target;
  3. @property (nonatomic, assign) SEL selector;
  4. @property (nonatomic, weak) NSTimer* timer;
  5. @end  
  6. @implementation HWWeakTimerTarget
  7. - ( void ) fire:(NSTimer *)timer {
  8. if (self.target) {
  9. [self.target performSelector:self.selector withObject:timer.userInfo];
  10. } else {
  11. [self.timer invalidate];
  12. }
  13. }
  14. @end  

Then we encapsulate a fake scheduledTimerWithTimeInterval method, but we have already replaced the original method when calling it:

  1. + (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
  2. target:(id)aTarget
  3. selector:(SEL)aSelector
  4. userInfo:(id)userInfo
  5. repeats:(BOOL)repeats {
  6. HWWeakTimerTarget* timerTarget = [[HWWeakTimerTarget alloc] init];
  7. timerTarget.target = aTarget;
  8. timerTarget.selector = aSelector;
  9. timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
  10. target:timerTarget
  11. selector: @selector (fire:)
  12. userInfo:userInfo
  13. repeats:repeats];
  14. return timerTarget.timer;
  15. }

Run again and the problem is solved.

block

Wouldn't it be better if we could use a block to call NSTimer? We can do this like this:

  1. + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
  2. block:(HWTimerHandler)block
  3. userInfo:(id)userInfo
  4. repeats:(BOOL)repeats {
  5. return [self scheduledTimerWithTimeInterval:interval
  6. target:self
  7. selector: @selector (_timerBlockInvoke:)
  8. userInfo:@[[block copy], userInfo]
  9. repeats:repeats];
  10. }
  11. + ( void )_timerBlockInvoke:(NSArray*)userInfo {
  12. HWTimerHandler block = userInfo[ 0 ];
  13. id info = userInfo[ 1 ];
  14. // or `!block ?: block();` @sunnyxx  
  15. if (block) {
  16. block(info);
  17. }
  18. }

In this way, we can write the relevant logic directly in the block:

  1. - (IBAction)fireButtonPressed:(id)sender {
  2. _timer = [HWWeakTimer scheduledTimerWithTimeInterval: 3 .0f block:^(id userInfo) {
  3. NSLog(@ "%@" , userInfo);
  4. } userInfo:@ "Fire" repeats:YES];
  5. [_timer fire];
  6. }

Yeah, that's it.

More

The above code is simply encapsulated into HWWeakTimer, welcome to try it.

<<:  Cocos game development engine adds efficient wings to HTML5 game development

>>:  Detailed explanation of JSPatch implementation principle: Let JS call/replace any OC method

Recommend

Can smart technology make cars better?

A means of transportation, a symbol of identity, ...

How do educational platforms acquire customers at low cost?

Under the epidemic, the market investment cost of...

Why do mother penguins "abandon" the first egg they lay?

Produced by: Science Popularization China Author:...

Counterpoint: Tesla's deliveries in Q4 2023 will increase by 20% year-on-year

Recently, according to Counterpoint Research data...

The most practical Android architecture design principles

Before we begin, I assume you have read my previo...

Tracking & Survival "Leo's Fortune" brings depth to casual games

Screen: Sound Effects: operate: Plot: Experience:...

How to use iPad at work to improve work efficiency

How to be more efficient with iPad requires using...

Why does China launch so many satellites?

This article was reviewed by Liu Yong, space phys...

CCTV reminds: Don’t use the WeChat “cleaning fans” APP anymore!

On November 12, Tencent released its Q3 financial...