This article learns reference iOS- Basic Principle 33: Memory management (2) strong reference analysis, thanks in this

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

Strong reference (strong hold)

Problem is introduced into

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

whyB the interface is not released

Let’s delve into the bottom layer, why B interface cannot be released after 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 iOS- Underlying Principles: Block Underlying Principles article, there are several solutions for looping 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

addweakSelfAnd then, 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,weakSelfselfIs 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 results are as follows: before and after discoveryselfThe reference count of the8

One conclusion can therefore be drawn: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:As can be seen from the print result, the currentselfTake the address andweakSelfThe value of the address is different. Means that there areTwo pointer addresses, pointing to the same memory space, i.e.,WeakSelf and self have different memory addresses, pointing to the same memory spacethe

  • As can be seen from the print above, at this time, the timer captures

    , which is an object, so strong holding cannot be solved by weakSelf. That is, the reference chain relationship is: NSRunLoop -> Timer -> weakSelf (

    ). So RunLoop 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. At this timeWeakSelf equals an intermediate value. Its reference relation chain isSelf -> block -> weakSelf (pointer address of temporary variable), can be accessed throughaddressgetPointer to the. Block is printed as follows:

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)

Train of thought 2, train of thought 3, train of thought 4 all are: rely on intermediary mode, break strong hold, recommend train of thought 4 among them

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

  • DidMoveToParentViewController method, is used when a view controller to add or remove viewController, must call the method. To tell iOS that adding or deleting child controllers is complete.

  • Rewrite didMoveToParentViewController method in the interface of B

- (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

Idea 2: The mediator pattern, which does not use Self, relies 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.

Idea 4: Use a subclass of NSProxy virtual base class

Here’s one of the most common ways to handle timer strong references: the NSProxy subclass

You can use NSProxy virtual base class, which can be implemented by subclasses. NSProxy is introduced in iOS- Basic principles: Block basic principles have been introduced, and will not be repeated here

  • First, define an inheritance fromNSProxyA subclass of
/ / * * * * * * * * * * * * NSProxy subclass * * * * * * * * * * * * @ interface CJLProxy: NSProxy + (instancetype) proxyWithTransformObject (id) object; @end @interface CJLProxy() @property (nonatomic, weak) id object; @end @implementation CJLProxy + (instancetype)proxyWithTransformObject:(id)object{ CJLProxy *proxy = [CJLProxy alloc]; proxy.object = object; return proxy; } -(id)forwardingTargetForSelector:(SEL)aSelector { return self.object; }Copy the code
  • willtimerIn thetargetThe incomingNSProxy subclass object, i.e.,The Timer holds the NSProxy subclass object
/ / * * * * * * * * * * * * to solve the problem of the timer is strong hold * * * * * * * * * * * * the self in the proxy = [CJLProxy proxyWithTransformObject: self]. self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES]; - (void)dealloc{[self.timer invalidate]; self.timer = nil; }Copy the code

The main purpose of this is to shift the focus from strong references to message forwarding. The virtual base class is only responsible for message forwarding, that is, using NSProxy as an intermediate proxy, intermediary

Does the defined proxy object still exist when dealloc is released?

  • The proxy object will be released normally, because vc is released normally, so it can release its holders, namely timer and proxy. The release of timer also breaks runLoop’s strong hold on proxy. Vc-× -> proxy <-× -runloop

    • Vc is released, causingproxyThe release of
    • In the dealloc method, the timer is freed, so the runloop strong reference is freed