preface

The previous series of articles covered memory layout, memory management strategies, reference counting, weak references, automatic release pools, and a general understanding of the source code for memory management. This article is about detecting memory leaks

Memory leaks

There are many kinds of memory leaks, and the following will introduce circular references to blocks and NSTimer strong references

A circular reference to a Block

Let’s take a look at the normal holding relationship, as follows

Under normal circumstances
A hold B, which is the reference count of B
+ 1

If A calls the destructor dealloc, it sends A release to B. If A no longer holds B, B’s reference count is -1. If B’s reference count is 0, B calls its own destructor to free itself

Look again at circular references

Let’s do it in code

#import "NHViewController.h"

typedef void(^NHBlock)(void);

@interface NHViewController ()
@property (nonatomic, copy) NHBlock block;

@property (nonatomic, copy) NSString *name;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"Noah";
    self.block = ^{
        NSLog(@"% @",self.name);
    };
    
    self.block();
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}

@end
Copy the code

Most likely: Capturing ‘self’ strongly in this block is likely to lead to a retain cycle Xcode: Capturing ‘self’ strongly in this block is likely to lead to a retain cycle Weak Typeof (self) weakSelf = self; Instead of self, use the following code:

__weak typeof(self) weakSelf = self;
self.block = ^{
    NSLog(@"% @",self.name);
};
self.block();
Copy the code

But there is also an early release problem. For example, if you start your App and you go to ViewControl, and now you Push it to NHViewController, and the NHViewController block does a delay, and it prints name after 2 seconds, If the Push pops immediately after the Push to the NHViewController, the printed name will be null.

#import "NHViewController.h"

typedef void(^NHBlock)(void);

@interface NHViewController ()
@property (nonatomic, copy) NHBlock block;
@property (nonatomic, copy) NSString *name;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"Noah"; __weak typeof(self) weakSelf = self; Self. block = ^{dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"% @",weakSelf.name); // self - nil name - nil
        });
    };
    
    self.block();
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end
Copy the code

Because after 2s the NHViewController has been released, the value of name has been released, so the printed name is null. The solution is to force a reference to a block while it is still executing.

__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"% @",strongSelf.name); // self - nil name - nil
    });
};
self.block();
Copy the code

There’s another way to do it, block pass, which is better for writing. The code is shown as follows:

#import "NHViewController.h"

typedef void(^NHBlockVC)(NHViewController *);

@interface NHViewController ()
@property (nonatomic, copy) NHBlockVC blockVc;
@property (nonatomic, copy) NSString *name;
@end

@implementation NHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"Noah";
    self.blockVc = ^(NHViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"% @",vc.name); 
        });
    };
    self.blockVc(self);
}

- (void)dealloc{
    NSLog(@"%s",__func__);
}
@end
Copy the code

NSTimer strong reference

API usage:

// You need to manually add it to the runloop and start the runloop of the child thread. + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; // The system adds you to the Runloop defaultMood by default. + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;Copy the code

The reason for the circular reference is that the target of the NSTimer is strongly referenced, and usually the target is the controller, which strongly references the timer, causing the circular reference. And NSRunLoop also forces a reference to the controller, as shown below

Code implementation:

#import "TimerViewController.h"

static int num = 0;
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer       *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}
- (void)dealloc{

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

You can observe that the destructor is never called. For now, we just need to fix target’s strong reference problem.

Solution:

  • The Timer is held indirectly by an intermediary agent
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NHTimerWapper : NSObject

@property (weak, nonatomic) id target;

+ (instancetype) timerWapperWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END
Copy the code
#import "NHTimerWapper.h"@implementation NHTimerWapper + (instancetype) timerWapperWithTarget:(id)target{ NHTimerWapper *wapper = [[NHTimerWapper  alloc]init]; wapper.target = target;returnwapper; } / / message redirect, specify an object to deal with - (id) forwardingTargetForSelector aSelector: (SEL) {return self.target;
}

@end
Copy the code
#import "NHTimerWapper.h"

static int num = 0;

@interface NHTimerViewController ()
@end

@implementation NHTimerViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[NHTimerWapper timerWapperWithTarget:self] selector:@selector(fireHome) userInfo:nil repeats:YES];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

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

Print:

2020-02-20 21:18:48.332365+0800 002- strong quote [3761:1484710] Hello word - 1 2020-02-20 21:18:49.332319+0800 [3761:1484710] hello word -2 2020-02-20 21:18:50.332302+0800 002- [3761:1484710] Hello word - 3 2020-02-20 21:18:51.296455+0800 002- Strong reference [3761:1484710] -[NHTimerViewController dealloc]Copy the code
  • Inherit the NSProxy class for message processing

NSProxy is a strange class. It’s in the same class as NSObject, and it’s an abstract class, so you can’t use it directly unless you inherit from it

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NHProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end

NS_ASSUME_NONNULL_END
Copy the code
#import "NHProxy.h"

@interface NHProxy()
@property (nonatomic, weak) id object;
@end

@implementation NHProxy
+ (instancetype)proxyWithTransformObject:(id)object{
    NHProxy *proxy = [NHProxy alloc];
    proxy.object = object;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    return [self.object methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.object];
}

@end
Copy the code
#import "NHProxy.h"
static int num = 0;

@interface NHTimerViewController ()
@end

@implementation NHTimerViewController


- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[NHProxy proxyWithTransformObject:self] selector:@selector(fireHome) userInfo:nil repeats:YES];
    
}
- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

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

Print:

2020-02-20 21:34:37.878850+0800 002- Strong quote [3906:1491522] Hello word - 1 2020-02-20 21:34:38.878544+0800 [3906:1491522] hello word -2 2020-02-20 21:34:39.879129+0800 002- [3906:1491522] Hello word - 3 2020-02-20 21:34:40.878787+0800 002- Strong quote [3906:1491522] Hello Word - 4 2020-02-20 21:34:41.721979+0800 002- Strong quote [3906:1491522] -[NHTimerViewController dealloc]Copy the code

The NSProxy class is one of the more powerful classes, and it can play any intermediary role

Detection tools

Instruments

Instruments is a flexible and powerful test tool for performance analysis, dynamic tracking, and analysis of OS X and iOS code. It makes it very easy to collect data about the performance and behavior of one or more system processes, and can track the generated data over time and examine the collected data. Different types of data can also be collected widely.

To summarize what Instrument can do:

  • Instruments is a performance analysis and testing tool for dynamically tracking and analyzing OS X and iOS code;
  • Instruments supports multithreaded debugging;
  • You can use Instruments to record and play back graphical user interface operations
  • You can save recorded graphical interface actions and Instruments as templates for later access.
  • Tracking down problems in code (even those that are hard to copy);
  • Analyze program performance;
  • Implement automatic test of program;
  • Part of the program to implement the pressure test;
  • Perform system-level general problem tracking debugging;
  • Gives you a better understanding of the inner workings of the program.

There are many articles about using Instruments on the web, so I will not cover them too much. Here are a few articles that summarize the use of Instruments -Core Animation

MLeaksFinder

MLeaksFinder is an automatic memory leak detection tool for iOS. After MLeaksFinder is introduced, it can automatically detect and warn of memory leaks during daily development and service logic debugging. Developers don’t have to open tools like instrument or run extra processes to find memory leaks. Also, since a memory leak can be found as soon as the business logic runs after the code is modified, the developer can quickly realize where the problem was written. This timely detection of memory leaks greatly reduces the cost of fixing memory leaks.

The principle of

Add a method to the base NSObject class, the -WillDealloc method, which points to self with a weak pointer and, after a short period of time (3 seconds), calls -AssertNotDealloc with the weak pointer. -AssertNotDealloc is used for direct middle assertion.

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}
Copy the code

So, when we think an object should be released, call this method before release. If 3 seconds later it is released successfully, weakSelf points to nil, does not call the -AssertNotDealloc method, and does not assert. If it is not released (leaked), -AssertNotDealloc will be called in the assertion. So, when a UIViewController is pop or dismiss (we think it should be released), we go through all the views on that UIViewController, we go to -WillDealloc, and if it’s not released after 3 seconds, Will be in the assertion.

MLeaksFinder iOS Memory Leak Detection Tool MLeaksFinder is an efficient way to detect memory leaks – MLeaksFinder


Refer to the article

IOS: Block circular reference RunTime message mechanism & NSTimer circular reference