Summary of basic principles of iOS

This article is mainly through timers to comb through several solutions to strong references

Strong application (strong holding)

Assume that there are two interfaces, A and B, pushing from A to B. There are the following timer codes in INTERFACE B. When I return to A interface from B pop, I find that the timer does not stop, and its method is still executing. Why?

self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Copy the code

The main reason is that the B interface is not released, that is, the dealloc method is not executed, so the Timer cannot stop and release

Solution 1

  • rewritedidMoveToParentViewControllermethods
- (void) didMoveToParentViewController: (the parent UIViewController *) {/ / push in or pop out normal run / / even if continue to pop back to continue to push to the next layer  if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog (@ "timer go"); }}Copy the code

Solution 2

  • When defining timer, this parameter is usedclosure, so there is no need to specify target
- (void)blockTimer{ self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer)  { NSLog(@"timer fire - %@",timer); }]; }Copy the code

Now, let’s delve into the underlying reasons why B interface cannot be released when it has timer, that is, it does not go to dealloc method. We can through the official document view timerWithTimeInterval: target: the selector: the userInfo: repeats: method for the description of the target

As you can see from the documentation, the timer has a strong hold on the passed target, that is, the timer holds self. Since the timer is defined in the B interface, self also holds the timer, so self -> timer -> self forms a circular reference

In the basic principle of Block article, several solutions are provided for circular applications. We tried to solve this problem with __weak or weak references, and the code changed as follows

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
Copy the code

We run the program again for a push-pop jump. The timer method is still executing, but B’s dealloc method is not executed.

We use __weak to break the loop reference before self -> timer -> self, i.e. the reference chain becomes self -> timer -> weakSelf -> self. However, our analysis is not comprehensive here. At this time, there is also a strong holding of timer by Runloop. Because the life cycle of Runloop is longer than that of INTERFACE B, timer cannot be released, and self of interface B cannot be released. So, the original chain of references should look like this

So when you add weakSelf, it looks like this

WeakSelf with self

For weakSelf and Self, there are mainly the following two questions

  • 1.weakSelfReference counting is performed+ 1Operation?
  • 2,weakSelf 和 selfIs the pointer to the same address, to the same piece of memory?
  • With questions, we areweakSelfBefore and after printingselfReference count of
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
Copy the code

The result is that self has a reference count of 8 before and after discovery

Therefore, it can be concluded that weakSelf does not perform +1 operation on memory

  • To continue printingweakSelf 和 selfObject, and the address of the pointer
po weakSelf
po self

po &weakSelf
po &self
Copy the code

The results are as follows

It can be seen from the printing result that the current address taken by self and weakSelf are different. It means that there are two pointer addresses pointing to the same piece of memory space, that is, weakSelf and self have different memory addresses pointing to the same piece of memory space

  • As you can see from the print above, at this pointtimerTo capture the<LGTimerViewController: 0x7f890741f5b0>, it is aobject, soStrong holding cannot be solved by weakSelf. That is, the reference chain relationship is:NSRunLoop -> Timer -> weakSelf (<LGTimerViewController: 0x7F890741F5B0 >). soRunLoop has a strong hold on the entire object space, Runloop does not stop, timer and weakSelf cannot be released
  • And we are inBlockIn principleA circular reference to a block, andtimerThere is a difference. A method that uses the underlying principles of block__Block_object_assignTo be seen,blockTo capture theObject pointer address, i.e.,Weakself is the pointer address of a temporary variable, withselfIt doesn’t matter becauseWeakSelf is the new address space. So at this pointWeakSelf equals an intermediate value. Its reference relation chain isSelf -> block -> weakSelf (pointer address of temporary variable), can be accessed throughaddressgetPointer to the

So here we need to distinguish between the model referenced by the block and timer cycles

  • The timer model:self -> timer -> weakSelf -> selfThe currenttimerTo capture theB Memory of the interface, that is, the memory of the VC object, i.e.,weakSelfRepresents theVc object
  • Block model:self -> block -> weakSelf -> self, the current block capturesPointer to the address, i.e.,weakSelfRepresents theThe address of the pointer to self's temporary variable

Resolve strong references (strong holds)

The ideas of the following methods are: rely on the intermediary mode to break the strong holding, among which the fourth idea is recommended

Idea 1: Destroy the timer in another method when pop

From the previous explanation, we know that the Runloop indirectly holds self because of its strong holding of the timer (because the timer captures the VC object). Therefore, the dealloc method cannot be executed. You need to see if there is another way to destroy the timer during pop. This method is didMoveToParentViewController

  • didMoveToParentViewControllerMethod, which must be called when a viewController is added or removed from a viewController. To tell iOS that adding or deleting child controllers is complete.
  • Rewrite it in BdidMoveToParentViewControllermethods
- (void) didMoveToParentViewController: (the parent UIViewController *) {/ / push in or pop out normal run / / even if continue to pop back to continue to push to the next layer  if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog (@ "timer go"); }}Copy the code

Route 2: The mediator pattern, which does not use self, depends on other objects

In timer mode, what we care about is that fireHome executes, and we don’t care who the target is, because it’s not convenient to use self (because of the strong hold problem), so we can change target to another object, like target to NSObject, Hand fireHome to Target for execution

  • Change the timer target from self to objc
/ / * * * * * * * * * * 1, define other object * * * * * * * * * * @ property (nonatomic, strong) id target; / / * * * * * * * * * * 1, modifying target * * * * * * * * * * the self. The target = [[NSObject alloc] init]; class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:"); self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES]; / / * * * * * * * * * * 3, imp * * * * * * * * * * void fireHomeObjc (id obj) {NSLog (@ - % @ "% s", __func__, obj); }Copy the code

The result is as follows

After dealloc is executed, timer continues. The reason is that the release of the mediator is addressed, but the collection of the mediator, i.e., self.target, is not. So this approach is flawed

This can be resolved by undoing the timer in the dealloc method, as shown below

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}
Copy the code

The result of the run is as follows: when a POP is found, the timer is released, and the broker is also released

Idea 3: Customize the encapsulation Timer

In this way, according to the principle of idea 2, the encapsulation timer is customized. The steps are as follows

  • Custom timerWapper

    • In the initialization method, define a timer whose target is itself. So the timer in the timerWapper, it’s always listening to itself, judging the selector, and it’s already given the selector to the target that’s passed in, and there’s a method called fireHomeWapper, and in that method, it’s telling you if the target exists

      • ifThe target is, the vc needs to know, that is, send the selector message to the target passed in, and pass in the timer parameter as well, so the VC can knowfireHomeMethod, which is why the timer method can be executed in this way
      • ifTarget does not existTo release the current timerWrapper, breaking RunLoop’s strong hold on timeWrapper (TimeWrapper < - x - RunLoop)
    • Release timer from custom CJL_invalidate method. This method is called in the VC dealloc method, which causes the timerWapper to be released, breaking the VC’s strong hold on timeWrapper (vc-× -> timeWrapper).

/ / * * * * * * * * * * *. H file * * * * * * * * * * * @ interface CJLTimerWapper: NSObject - (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; - (void)cjl_invalidate; @ the end / / * * * * * * * * * * * m file * * * * * * * * * * * # import "CJLTimerWapper. H # import" < objc/message. H > @ interface CJLTimerWapper () @property(nonatomic, weak) id target; @property(nonatomic, assign) SEL aSelector; @property(nonatomic, strong) NSTimer *timer; @end @implementation CJLTimerWapper - (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget Selector :(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{if (self == [super init]) {// pass vc self.target = aTarget; // The timer method self.aSelector = aSelector; if ([self.target respondsToSelector:self.aSelector]) { Method method = class_getInstanceMethod([self.target class], aSelector); const char *type = method_getTypeEncoding(method); // Add the timerWapper method class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type); // Start a timer, target is self, The listening your self. The timer = [NSTimer scheduledTimerWithTimeInterval: ti target: self selector: aSelector the userInfo: the userInfo repeats:yesOrNo]; } } return self; } runloop void fireHomeWapper(CJLTimerWapper *wapper){if (wapper. Target){// If (wapper. Target){ //objc_msgSend sends a selector message to the target that is passed in, and the timer parameter is passed in as well, so that the VC can see why the 'fireHome' method works. Void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend; lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer); }else{// Release the current timerWrapper [wapper.timer invalidate] if the target does not exist; wapper.timer = nil; (void)cjl_invalidate{[self.timer invalidate]; self.timer = nil; } - (void)dealloc { NSLog(@"%s",__func__); } @endCopy the code
  • The use of timerWapper
// define self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 Target :self selector:@selector(fireHome) userInfo:nil repeats:YES]; // Release - (void)dealloc{[self.timerWapper cjl_invalidate]; }Copy the code

The result is as follows

This method seems to be cumbersome with many steps. Moreover, for timerWapper, method needs to be continuously added and a series of processing needs to be carried out.


\