👉 JCLeaksFinder

TL; DR

  • Use MLeaksFinder to find the memory leak object
  • Use the FBRetainCycleDetector to get the chain of circular references
  • Traverse the global object to get the global object reference chain

The JCLeaksFinder component provides a unified package and interface optimization for all of these functions, enabling memory leak detection in a single line of code. Welcome!

[JCLeaksConfig sharedInstance].callback = ^(NSObject * _Nonnull leakedObject, NSSet * _Nonnull retainInfo, NSArray<NSString *> * _Nonnull viewStack) {
    // show alert or do something
};
Copy the code

JCLeaksFinder also supports rich custom configurations.

interface JCLeaksConfig : NSObject

+ (JCLeaksConfig *)sharedInstance;

/// memory leak detection result callback
LeakedObject -> Leak object
/// retainInfo -> Reference chain information, which may contain multiple. Don't worry about 'retainInfo' specific data, directly call '[retainInfo description]' output results.
/// viewStack -> Leak object level information
@property(nonatomic.copy) JCLeaksFinderCallback callback;

/// Check threshold. The default value is 5s. Exit the page 'detectThresholdInSeconds' after seconds to detect if there is a memory leak.
@property(nonatomic.assign) NSUInteger detectThresholdInSeconds;

// check the maximum chain length of circular references, default is' 10 '.
@property(nonatomic.assign) NSUInteger retainCycleMaxLength;

// check the maximum chain length of global object references. Default is' 15 '.
@property(nonatomic.assign) NSUInteger globalRetainMaxLength;

/// Whether to detect global object references. The default is' YES '. Detection of global object references takes a high time (about 2-3s) and is performed in child threads
@property(nonatomic.assign) BOOL checkGlobalRetain;

/// Add custom global objects. Default is' nil '.
/// Some objects are not global objects, but will live for the lifetime of the APP, such as rootNavigationController, rootTabBarController, etc
// extraGlobalObjects will also be referenced as global objects when detecting global objects
@property(nonatomic.copy) NSArray<NSObject *> *extraGlobalObjects;

/// Add a whitelist class name
- (void)addClassNamesToWhiteList:(NSArray<NSString *> *)classNames;

/// Add a whitelist object. The object is not held internally.
- (void)addObjectToWhiteList:(NSObject *)object;

@end
Copy the code

Test results

JCLeaksFinder returns the leak object, reference information, and View hierarchy to the business layer, which can customize the notification form, such as pop-up Alert.

introduce

The so-called memory leak is that the allocated memory of the program is not released or cannot be released for some reason, resulting in the waste of system memory, resulting in the program running speed slowing down or even system crash and other serious consequences. In short, you can’t free up memory you no longer use.

The most common types of memory leaks encountered in iOS development are:

  1. There is a circular reference, so the object cannot be released
  2. Being held by a global object (such as a singleton), causing the object to be unable to be released
  3. (non-ARC managed objects) no active release

This article focuses on the first two types of memory leak detection. The third type of memory leak problem is beyond the scope of this article.

The target

  1. Automatically detects memory leaks and generates alarms in time
  2. Automatic access to the reference chain, efficient repair

In general, the more automated the better, the more complete the information, the better. Therefore, this article does not cover how to manually detect memory leaks using Xcode/Instrument.

Memory leak detection

This article covers only page-level memory leak detection, including ViewController and its View/Subviews.

Detecting memory leaks can be a tricky problem. As we know from the definition at the beginning of this article, a memory leak is an inability to free memory that is no longer in use. So what memory is no longer used? Obviously, this problem is unsolvable without specific contextual information.

However, in certain scenarios, we can infer that a particular object belongs to a memory object that is no longer in use. For example, when a page exits, it is reasonable to assume that the page (ViewController) should be destroyed along with its View and all Subviews. These memory objects are useless after the page exits.

There are a number of solutions for detecting memory leaks in pages, the best known of which is MLeaksFinder. MLeaksFinder detects that the page and its associated View are empty after a page exits for a period of time. If they are not empty, a memory leak may have occurred. The specific principle of this paper is no longer described, you can understand.

After plugging in MLeaksFinder, if a memory leak is detected after exiting the page, we can output the following message:

The 2021-01-30 21:50:15. 869024 + 0800 Example [15854-39962324] Possibly the Memory Leak. In case that JCRetainCycleViewController should not be dealloced, override -willDealloc in JCRetainCycleViewController by returning NO. View-ViewController stack: ( JCRetainCycleViewController )Copy the code

Chain fetch

Now we know that there is a memory leak, and we know which object is leaking, but we do not know by whom the leaking object is being referenced. It’s like, we know something is missing, but we don’t know who the thief is. How do you catch the culprit?

Without the help of other tools, we can only

  • Look at the relevant code line by line
  • Repeat the scene of the problem, inXcodeMemory GraphTo locate the object.

Obviously, these two solutions are not elegant enough, time-consuming and difficult to find the problem. Is there a way to automatically get the chain of references to the leaking object?

Circular reference chain

FBRetainCycleDetector is a tool for detecting circular references. The main principle is to generate the reference diagram of an object and then perform depth-first traversal. If the existence of a ring is found, it indicates that a circular reference has occurred.

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
// Add detection objects
[detector addCandidate:leakedObject];
// Check for circular references
NSSet *result = [detector findRetainCycles];
Copy the code

The biggest problem of FBRetainCycleDetector is that it needs to provide the candidate (leak object) first. How can leak objects be obtained? MLeaksFinder has already found it for us!

MLeaksFinder is responsible for finding the leaking object and FBRetainCycleDetector is responsible for obtaining the circular reference chain of the leaking object. Perfect!

2021-01-30 21:54:23.235166+0800 Example[20059:40002058] retain cycle: {(
        (
        "-> _customView -> JCLeaksCustomView ",
        "-> _block -> __NSMallocBlock__ ",
        "-> JCRetainCycleViewController "
    )
)}
Copy the code

Global object reference chains

Automatic detection of circular reference scenarios has been fixed. How to solve the problem of being held by global objects?

MLeaksFinder detects a memory leak if the global object holds the ViewController/View and the ViewController/View cannot be released when the page exits. However, at this time, there is no reference to the leaking object -> global object, only the reference to the leaking object -> leak object, so there is no circular reference and the circular reference chain cannot be obtained using the FBRetainCycleDetector.

The difficulty with this problem is that it is easy to know which objects are referred to by leaking objects (look down), but impossible to know which objects refer to leaking objects (look up). Since we can’t look up directly, we have only one way to go: find all the global objects and then look down to see if they reference the leak object.

Get all global objects

How do I find all global objects? We know that the global object is stored in the __DATA Segment __bSS section of the Mach-o file.

Detailed information about the Mach -o file format, can consult developer.apple.com/library/arc…

+ (NSArray<NSObject *> *)globalObjects {
    NSMutableArray<NSObject *> *objectArray = [NSMutableArray array];
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0; i < count; i++) {
        const mach_header_t *header = (const mach_header_t*)_dyld_get_image_header(i);
	// Filter the image to be detected
	// ...
	
        // Get the image offset
        vm_address_t slide = _dyld_get_image_vmaddr_slide(i);
        long offset = (long)header + sizeof(mach_header_t);
        for (uint32_t i = 0; i < header->ncmds; i++) {
            const segment_command_t *segment = (const segment_command_t *)offset;
            __DATA.__bss section: static memory allocation
            if(segment->cmd ! = SEGMENT_CMD_TYPE || strncmp(segment->segname,"__DATA".6) != 0) {
                offset += segment->cmdsize;
                continue;
            }
            section_t *section = (section_t *)((char *)segment + sizeof(segment_command_t));
            for (uint32_t j = 0; j < segment->nsects; j++) {
		/ / filter section
		// ...
                const uint32_t align_size = sizeof(void *);
                if (align_size <= size) {
                    uint8_t *ptr_addr = (uint8_t *)begin;
                    for (uint64_t addr = begin; addr < end && ((end - addr) >= align_size); addr += align_size, ptr_addr += align_size) {
                        vm_address_t *dest_ptr = (vm_address_t *)ptr_addr;
                        uintptr_t pointee = (uintptr_t)(*dest_ptr);
                        // Omit the code that determines whether a pointer points to an OC object
                        // ...
                        // [objectArray addObject:(NSObject *)pointee];
                    }
                }
            }
            offset += segment->cmdsize;
        }
        // ...
    }
    return objectArray;
}
Copy the code

Check whether the pointer points to an OC object. If the pointer does not point to a valid OC object, filter it out. See blog.timac.org/2016/1124-t…

Output reference chain

Once you have all the global objects, the next thing you need to do is find out which global object references the leak object.

How do I find it? A reference graph of the global object is generated, and then a depth-first traversal is performed. If the existence of a leaking object is found, the global object references the leaking object.

Wait, isn’t that the same mechanism as FBRetainCycleDetector? Is there any way to reuse FBRetainCycleDetector’s detection logic?

I don’t think so, because there’s no circular reference, right?

In the spirit of not reinventing the wheel, we decided to force the wheel of the FBRetainCycleDetector. There is no circular reference, we just make a circular reference!

- (void)checkLeakedObject:(NSObject *)leakedObject withGlobalObjects:(NSArray<NSObject *> *)globalObjects {
    // If leakedObject is held by a global object, there is actually no circular reference chain. Here, the associatedObject is manually set to create a circular reference so that the Detector can detect it.
    [FBAssociationManager hook];
    for (NSObject *obj in globalObjects) {
        objc_setAssociatedObject(leakedObject, $(@"jc_apm_fake_%p", obj).UTF8String, obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    // Start detection and filter out unwanted data
    FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
    [detector addCandidate:leakedObject];
    NSSet *result = [detector findRetainCycles];

    // Omit the filtering logic here, because the global object may itself have circular references that need to be filtered out of the reference chain containing leakedObject
    // filter...

    // Remove the manually set associatedObject
    for (NSObject *obj in globalObjects) {
        objc_setAssociatedObject(leakedObject, $(@"jc_apm_fake_%p", obj).UTF8String, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [FBAssociationManager unhook];
Copy the code

After adding a reference to the global object to the leak object, if the global object also references the leak object, then the circular reference will naturally occur and the FBRetainCycleDetector can be used to obtain the reference chain.

Finally, change the added __associated_object to [Global] for output, and the result is very clear.

2021-01-30 21:55:12.071707+0800 Example[20059:40002058] retain info: {(
        (
        "-> JCGlobalRetainViewController ",
        "-> [Global] -> __NSArrayM "
    )
)}
Copy the code

conclusion

This article describes how to use automated tools to detect memory leaks at the page level and output detailed circular references and global object references so that developers can quickly and efficiently find and fix memory leaks.

It is worth noting that automatic detection of memory leaks must have False Positive, that is, the scenario that is not a memory leak is determined to be a memory leak. Because objects, whether referenced by a loop or by a global object, should not be considered a memory leak as long as they are expected to be useful. Automatic memory leak detection tools typically provide a whitelist mechanism for ignoring scenarios that should not be considered memory leaks. JCLeaksFinder also provides a configuration to pass in whitelisted classes or whitelisted objects directly.