For iOS development, the problem of memory leaks has been a platitude. These questions often come up in daily interviews. Leaks/Allocation in the instrument tool is used to detect memory Leaks during daily development. MLeakFinder is also available on the network, which is efficient and easy to use.

Principle of MLeakFinder –

So UIViewController first, when a UIViewController is pop or dismiss, the VC the View that’s included on the VC, or the child View is going to be released pretty quickly. So we need to see if the view, subView, etc. on this VC still exists after the UIViewController is POP or dismiss for a little while.

As you can see in UIViewController+MemoryLeak.h load, the runtime swaps viewWillAppear, viewDidAppear, DismissViewControllerAnimated: completion: the three methods.

1. Let’s look at viewWillAppear

- (void)swizzled_viewWillAppear:(BOOL)animated {
    [self swizzled_viewWillAppear:animated];
    objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}
Copy the code

When VC comes in, add the associated object and mark it as NO

2. I’m looking at viewDidAppear

- (void)swizzled_viewDidDisappear:(BOOL)animated { [self swizzled_viewDidDisappear:animated]; if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) { [self willDealloc]; }}Copy the code

As you can see from the code, get the tag of the current associated object, and when marked with YES, willDealloc will be called.

3, when do we see it marked YES?

The UINavigationController + MemoryLeak. H popViewControllerAnimated: approach we can see

- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated { UIViewController *poppedViewController = [self  swizzled_popViewControllerAnimated:animated]; if (! poppedViewController) { return nil; } // Detail VC in UISplitViewController is not dealloced until another detail VC is shown if (self.splitViewController && self.splitViewController.viewControllers.firstObject == self && self.splitViewController == poppedViewController.splitViewController) { objc_setAssociatedObject(self, kPoppedDetailVCKey, poppedViewController, OBJC_ASSOCIATION_RETAIN) return poppedViewController; } // VC is not dealloced until disappear when popped using a left-edge swipe gesture extern const void *const kHasBeenPoppedKey; objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN); return poppedViewController; }Copy the code

As you can see, VC is marked with YES when it is pop or left swipe back, which is the equivalent of view destruction.

4. Let’s focus on willDealloc

- (BOOL)willDealloc {NSString *className = NSStringFromClass([self class]); if ([[NSObject classNamesWhitelist] containsObject:className]) return NO; NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey) if ([senderPtr isEqualToNumber:@((uintptr_t)self)]) return NO; // __weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong id strongSelf = weakSelf; [strongSelf assertNotDealloc]; }); return YES; }Copy the code
  1. Step 1: Check if the current class is whitelisted and return NO if it is.

At the same time we look at the source code for building a whitelist: a singleton implementation is used to make sure there is only one, a private method

+ (NSMutableSet *)classNamesWhitelist { static NSMutableSet *whitelist = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ whitelist = [NSMutableSet setWithObjects: @"UIFieldEditor", // UIAlertControllerTextField @"UINavigationBar", @"_UIAlertControllerActionView", @"_UIVisualEffectBackdropView", nil]; // System's bug since iOS 10 and not fixed yet up to this ci. NSString *systemVersion = [UIDevice currentDevice].systemVersion; If ([systemVersion compare:@"10.0" options:NSNumericSearch]! = NSOrderedAscending) { [whitelist addObject:@"UISwitch"]; }}); return whitelist; }Copy the code

At the same time, it also supports custom add whitelist

+ (void)addClassNamesToWhitelist:(NSArray *)classNames {
    [[self classNamesWhitelist] addObjectsFromArray:classNames];
}
Copy the code

2. Step 2: Check whether this object is the object that sent the action last time. If yes, no memory check is performed

NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey) if ([senderPtr isEqualToNumber:@((uintptr_t)self)]) return NO;Copy the code

The weak pointer points to self, 2s delay, and then calls -AssertNotDealloc through this weak pointer. If freed, it sends a message to nil and returns without triggering the -AssertNotDealloc method, which says freed. If it is not released (leaked), -AssertNotDealloc is called

    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
        [strongSelf assertNotDealloc];
    });
Copy the code

5, now let’s go back to the code for: 2 [self willDealloc]

Take a look at his source code

- (BOOL)willDealloc {// if (! [super willDealloc]) { return NO; } / / second step [self willReleaseChildren: self. ChildViewControllers]; [self willReleaseChild:self.presentedViewController]; if (self.isViewLoaded) { [self willReleaseChild:self.view]; } return YES; }Copy the code

1, step 1: The parent willDealloc, directory 4 above, will be called via super

Step 2: Call willReleaseChildren, willReleaseChild to iterate over the child objects of this object to see if they are released

- (void)willReleaseChild:(id)child {
    if (!child) {
        return;
    }
    
    [self willReleaseChildren:@[ child ]];
}


- (void)willReleaseChildren:(NSArray *)children {
    NSArray *viewStack = [self viewStack];
    NSSet *parentPtrs = [self parentPtrs];
    for (id child in children) {
        NSString *className = NSStringFromClass([child class]);
        [child setViewStack:[viewStack arrayByAddingObject:className]];
        [child setParentPtrs:[parentPtrs setByAddingObject:@((uintptr_t)child)]];
        [child willDealloc];
    }
}
Copy the code

Call the willReleaseChildren method, get the current object viewStack, parentPtrs, and iterate over children, setting the viewStack, parentPtrs, Then call willDealloc.

ViewStask, parentPtrs implementation through the source code

- (NSArray *)viewStack { NSArray *viewStack = objc_getAssociatedObject(self, kViewStackKey); if (viewStack) { return viewStack; } NSString *className = NSStringFromClass([self class]); return @[ className ]; } - (void)setViewStack:(NSArray *)viewStack { objc_setAssociatedObject(self, kViewStackKey, viewStack, OBJC_ASSOCIATION_RETAIN); } - (NSSet *)parentPtrs { NSSet *parentPtrs = objc_getAssociatedObject(self, kParentPtrsKey); if (! parentPtrs) { parentPtrs = [[NSSet alloc] initWithObjects:@((uintptr_t)self), nil]; } return parentPtrs; } - (void)setParentPtrs:(NSSet *)parentPtrs { objc_setAssociatedObject(self, kParentPtrsKey, parentPtrs, OBJC_ASSOCIATION_RETAIN); }Copy the code

ViewStack uses arrays and parentPtrs uses collections. It’s all done by adding properties to the associated object at runtime.

ParentPtrs, in -AssertNotDealloc, determines whether the current object has an intersection with the parent node set. Take a closer look at the -AsserTNotDealloc method

- (void)assertNotDealloc {// First step if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {return; } // step 2 [MLeakedObjectProxy addLeakedObject:self]; NSString *className = NSStringFromClass([self class]); NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]); }Copy the code

1. In the first step, we see whether there is intersection by judging parentPtrs

See its source code:

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs { NSAssert([NSThread isMainThread], @"Must be in main thread."); static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ leakedObjectPtrs = [[NSMutableSet alloc] init]; }); if (! ptrs.count) { return NO } if ([leakedObjectPtrs intersectsSet:ptrs]) { return YES; } else { return NO; }}Copy the code

If there is an intersection, return. Otherwise, go to step 2

Step 2: addLeakedObject

+ (void)addLeakedObject:(id)object { NSAssert([NSThread isMainThread], @"Must be in main thread."); MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init]; proxy.object = object; proxy.objectPtr = @((uintptr_t)object); proxy.viewStack = [object viewStack]; static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey; objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN); [leakedObjectPtrs addObject:proxy.objectPtr]; #if _INTERNAL_MLF_RC_ENABLED [MLeaksMessenger alertWithTitle:@"Memory Leak" message:[NSString stringWithFormat:@"%@", proxy.viewStack] delegate:proxy additionalButtonTitle:@"Retain Cycle"]; #else [MLeaksMessenger alertWithTitle:@"Memory Leak" message:[NSString stringWithFormat:@"%@", proxy.viewStack]]; #endif }Copy the code

Step 1: Construct the MLeakedObjectProxy object and associate a proxy with the passed leak object object

The second step: Objc_setAssociatedObject (Object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN) If the proxy holds an object, if the Object is released, the proxy also releases the object

Step 3: Store proxy.objectptr (the actual object address) into the collection leakedObjectPtrs

Step 4: Frame AlertView If _INTERNAL_MLF_RC_ENABLED == 1, the frame will add the option of detecting cyclic references. If _INTERNAL_MLF_RC_ENABLED == 0, only stack information is displayed.

For the MLeakedObjectProxy class, this is generated when a memory leak is detected and exists as an attribute of the leak object. If the leak object is released, MLeakedObjectProxy is also released and -dealloc is called

The collection leakedObjectPtrs removes the object’s address and pops up again, indicating that the object has been freed

6. I am also trying to rewrite this framework. Welcome to communicate with me