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

4 modules, a brief analysis of "New User"

In this article, I will explain "new additio...

Have you ever been affected by these noises at home? 3 ways to reduce the noise!

It’s so noisy! This is probably our most direct a...

Google Pixel encounters screen freeze: Android flagship becomes a rip-off

The Pixel phone carries Google's new hardware ...

Email inventor Tom Linson dies at 74

Ray Tomlinson, the American computer programmer w...

Subconscious Guidance Technique: Adjust a man to the way you like him

Introduction to the audio lecture on emotions betw...

3 methodologies for selling goods through live streaming!

Nowadays, the so-called sales promotion is actual...

IBM's SyNAPSE chip can simulate the brain

According to foreign media reports, three years l...

"China's Sky Eye" has made another important new discovery!

The search for nanohertz gravitational waves is o...

Google I/O Conference: AI as the protagonist, aiming at VR standards

Google's 2016 I/O Developer Conference was he...