Learn from MLeaksFinder how to detect memory leaks in real time
Memory is a shared resource on mobile devices. If an App fails to properly manage memory, it will run out of memory, flash back and severely degrade performance.
Many functional modules of our App share the same memory space. If one module consumes too much memory resources, it will have a serious impact on the whole App.
Note: All of our scenarios below are based on ARC.
There are several ways to detect memory leaks
When we were developing, there were some very obvious memory leaks that the compiler would have directly detected and warned about, as shown in the following figure
The static test
Alternatively, we can use Product->Analyze to further detect some simple memory leaks. Of course, this approach is not really the focus of our research because it is too simple and obvious.
Instrument
Instrument is now a testing tool that comes with Xcode. We can troubleshoot memory Leaks/Allocations using the inside.
As you can see from Apple’s developer documentation, there are three types of memory for an app:
- Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).
- Abandoned memory: Memory still referenced by your application that has no useful purpose.
- Cached memory: Memory still referenced by your application that might be used again for better performance.
Leaks only checks Leaked memory, which was common in the MRC era because it was easy to forget to call release, but more common in the ARC era were Leaked memory caused by circular references. The Leaks tool does not detect such memory Leaks and has limited applications.
For Abandoned memory, this can be detected using Instrument Allocations. Detection is done using Mark Generation, and each time you click on Mark Generation, Allocations generates a memory snapshot of current App, and Allocations records the time period from last memory snapshot to this memory snapshot, Information about newly allocated memory. Here’s the simplest example:
We could keep repeating push and pop the same UIViewController, and theoretically, before push and after pop, our app would go back to the same state. Therefore, newly allocated memory during the push process should be dealloc removed after the POP, except in cases where there may be preheated data and cache data during the previous push. If memory continues to grow after several pushes and pops, there is a memory leak. Therefore, we Mark Generation before each push and after each pop to see if memory is growing without limit. This approach is described in the WWDC video Session 311 – Advanced Memory Analysis with Instruments, and in apple’s developer documentation Finding Abandoned Memory.
This is still an inconvenient way to find memory leaks:
- First, you have to open Allocations
- Second, you have to repeat it scene by scene
- Can not be timely informed of the leak, have to do the above operation, very tedious
MLeaksFinder
MLeaksFinder provides a better solution for memory leak detection.
- Simply by introducing MLeaksFinder, we can automatically detect memory leaky objects while the App is running and immediately alert them,
- No additional tools to open.
- There is no need to repeat the operation scenario by scenario to detect memory leaks.
Principle: When a ViewController is popped or dismissed, we assume that the ViewController, its child ViewController, its View, its subView, and so on, will be released soon, If a View or ViewController is not released, we assume that the object is leaking.
The source code interpretation
1.MLeaksFinder.h
#import "NSObject+MemoryLeak.h" //#define MEMORY_LEAKS_FINDER_ENABLED 0 #ifdef MEMORY_LEAKS_FINDER_ENABLED // The macro is used to control when the MLLeaksFinder library is enabled. You can customize this timing, and by default, it is enabled in DEBUG mode. RELEASE mode does not start // it is pre-compiled #define _INTERNAL_MLF_ENABLED MEMORY_LEAKS_FINDER_ENABLED #else #define _INTERNAL_MLF_ENABLED DEBUG #endif //_INTERNAL_MLF_RC_ENABLED macro is used to control whether to enable cyclic reference detection #define MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED 0 #ifdef MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED #define _INTERNAL_MLF_RC_ENABLED MEMORY_LEAKS_FINDER_RETAIN_CYCLE_ENABLED //COCOAPODS Since MLeaksFinder references a third library (FBRetainCycleDetector) to check for circular references, you must be using COCOAPODS in your current project to use this feature. #elif COCOAPODS #define _INTERNAL_MLF_RC_ENABLED COCOAPODS #endifCopy the code
_INTERNAL_MLF_ENABLED is an expression for conditional compilation and is used to control whether other MLeaksFinder files are compiled. If _INTERNAL_MLF_ENABLED is 0 in the publishing environment, the library is disabled. If you need to close the code in either the debug or release environment, #define MEMORY_LEAKS_FINDER_ENABLED 0._internal_MLF_RC_ENABLED Indicates whether FBAssociationManager is imported to monitor circular references. Disabled by default
2.MLeaksMessenger
This file is mainly responsible for showing memory leaks.
There are two methods in MLeaksMessenger. H
+ (void)alertWithTitle:(NSString *)title message:(NSString *)message;
+ (void)alertWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id<UIAlertViewDelegate>)delegate
additionalButtonTitle:(NSString *)additionalButtonTitle;
Copy the code
We can check the.m file and find that the latter method is actually the Designated Initializer of the first method, which we can call the all-powerful Initializer method
#import "MLeaksMessenger.h"
static __weak UIAlertView *alertView;
@implementation MLeaksMessenger
+ (void)alertWithTitle:(NSString *)title message:(NSString *)message {
[self alertWithTitle:title message:message delegate:nil additionalButtonTitle:nil];
}
+ (void)alertWithTitle:(NSString *)title
message:(NSString *)message
delegate:(id<UIAlertViewDelegate>)delegate
additionalButtonTitle:(NSString *)additionalButtonTitle {
[alertView dismissWithClickedButtonIndex:0 animated:NO];
UIAlertView *alertViewTemp = [[UIAlertView alloc] initWithTitle:title
message:message
delegate:delegate
cancelButtonTitle:@"OK"
otherButtonTitles:additionalButtonTitle, nil];
[alertViewTemp show];
alertView = alertViewTemp;
NSLog(@"%@: %@", title, message);
}
@end
Copy the code
A trick here is to use static global variables to decorate the variable with __weak.
-
static __weak UIAlertView
alertView;
First call
*[alertView dismissWithClickedButtonIndex:0 animated:NO]; * * this method, alertView is nil, * * [alertView dismissWithClickedButtonIndex: 0 animated: NO] * * do not have any operation, just a box. Calling this method again (click on retain Cycle) will dimiss the existing popbox through alertView and display the new popbox. So an alertView is a pop-up that records the memory leaks that are currently displayed. Also set the __weak modifier to make the global variable weakly referenced. Once the popbox disappears, it’s automatically set to nil.
Ps. As an iOS development needs a good learning environment, so welcome your arrival, and we exchange learning together!
3.MLeakedObjectProxy
This file is the core file for detecting memory leaks and provides two methods:
+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs;
+ (void)addLeakedObject:(id)object;
Copy the code
The first method is used to determine if there are any leaking objects in the PTRS (NSSet type), and returns True if there are any. The second method is to add the object to the collection of leaking objects, while calling MLeaksMessenger’s popover method. Either to determine or to compare, a collection is always required to hold all leaking objects. Naturally check MLeakedObjectProxy.
Static NSMutableSet, leakedObjectPtrs; It’s the object of comparison. Both methods are called only in assertNotDealloc of NSObject’s category.
Let’s look at the implementation of the method in the.m file:
// To check whether the current leak object has been added to the leak object collection, if so, + (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *) PTRS {NSAssert([NSThread isMainThread], @"Must be in main thread."); /* #define NSAssert(condition, desc) condition is an expression. If the expression is false, an exception is raised and desc is printed in the log. Desc can be ignored. When the expression is true, nothing is done. */ 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; } } + (void)addLeakedObject:(id)object { NSAssert([NSThread isMainThread], @"Must be in main thread."); MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init]; MLeakedObjectProxy = [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); / / packaging yourself as MLeakedObjectProxy object in leakedObjectPtrs [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 - (void)dealloc { NSNumber *objectPtr = _objectPtr; NSArray *viewStack = _viewStack; dispatch_async(dispatch_get_main_queue(), ^{ [leakedObjectPtrs removeObject:objectPtr]; [MLeaksMessenger alertWithTitle:@"Object Deallocated" message:[NSString stringWithFormat:@"%@", viewStack]]; }); }}Copy the code
In the implementation of the above two methods, we found several key points
- To use these two methods, you must use them in the main thread
- The object to be checked must be checked to see if it has been recorded to prevent repeated addition and cause a loop
The core code to show a circular reference is shown below:
#pragma mark - UIAlertViewDelegate - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if (! buttonIndex) { return; } id object = self.object; if (! object) { return; } #if _INTERNAL_MLF_RC_ENABLED dispatch_async(dispatch_get_global_queue(0, 0), ^{ FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:self.object]; NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:20]; BOOL hasFound = NO; For (NSArray * retainCycles in retainCycles) {NSInteger index = 0; For (FBObjectiveCGraphElement * Element in retainCycle) {if (element.object == object) { NSArray *shiftedRetainCycle = [Self shiftArray:retainCycle toIndex:index]; Dispatch_async (dispatch_get_main_queue()) ^{ [MLeaksMessenger alertWithTitle:@"Retain Cycle" message:[NSString stringWithFormat:@"%@", shiftedRetainCycle]]; }); hasFound = YES; break; } ++index; } if (hasFound) { break; } } if (! HasFound) {dispatch_async(dispatch_get_main_queue(), ^{ [MLeaksMessenger alertWithTitle:@"Retain Cycle" message:@"Fail to find a retain cycle"]; }); }}); #endif} // Change the current object to the first position, - (NSArray *)shiftArray:(NSArray *)array toIndex:(NSInteger)index {if (index == 0) {return array; } NSRange range = NSMakeRange(index, array.count - index); NSMutableArray *result = [[array subarrayWithRange:range] mutableCopy]; [result addObjectsFromArray:[array subarrayWithRange:NSMakeRange(0, index)]]; return result; }Copy the code
We found that MLeaksFinder was using Facebook’s open source FBRetainCycleDetector tool when displaying circular references. We first find the leaky object through MLeaksFinder and then check for circular references through the FBRetainCycleDetector. For FBRetainCycleDetector, we can check out this article (science online required).
We can actually see that FBRetainCycleDetector treats an object, a ViewController, or a block as a node, and the associated strong references are lines. They actually form a directed acyclic graph (DAG) in which we need to look for possible loops, using a depth-first search algorithm to traverse it and find loop nodes.
4.NSObject+MemoryLeak
This file is mainly used to store the tree structure of the parent nodes of the object, the method Swizzle logic, the whitelist, and the implementation to determine whether the object has memory leaks.
- (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
This method is used externally to provide calls to each method when it exits. And internally, it will first screen several situations that will not be reported. The following are three judgment conditions:
- [[NSObject classNamesInWhiteList] containsObject:className] is True. The obvious way to do this is to decide whether to whitelist
- [senderPtr isEqualToNumber: @ (uintptr_t self)] is True. The introduction is below
- __strong id strongSelf = weakSelf; Where strongify is nil. Set __weak ID weakSelf = self; , then __strong ID strongSelf = weakSelf, if the object has been released, strongSelf is nil call this method nothing happens.
Here’S the second rule:
The target object that is performing target-action does not detect memory leaks. When the user triggers the target-action method, the sender actually executes sendAction:to:forEvent before executing the Action method, Then UIApplicatoin perform sendAction: to: the from: forEvent: method, which is from the sender object.
Here use exchange intercepted sendAction: to: the from: forEvent:, then seized the sender object stored in kLatestSenderKey. Determine if the two are the same.
The reason for this relates to the target-action principle, which currently actually forms a circular reference, and this article is recommended so that we can conclude
For the target member variable, objc_storeWeak is called in the initialization method of UIControlTargetAction, which refers to the target object passed in as weak.
If a leak occurs, the following methods are called:
- (void)assertNotDealloc {
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
[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
This is where the direct judgment comes in, as I mentioned before.
*
-(void)willReleaseObject:(id)object relationship:(NSString
)relationship;
I have not found relevant references for this method, so it may have been abandoned. If you know, please feel free to comment.
Next, analyze the following three key methods:
- (void)willReleaseChild:(id)child;
- (void)willReleaseChildren:(NSArray *)children;
- (NSArray *)viewStack;
Copy the code
-(void)willReleaseChild (id) Child -(void)willReleaseChild (id) Child -(void)willReleaseChildren (NSArray *)children
- (void)willReleaseChild:(id)child { if (! child) { return; } [self willReleaseChildren:@[ child ]]; }Copy the code
The first method is executed only in UIViewController+MemoryLeak,
- (BOOL)willDealloc { if (! [super willDealloc]) { return NO; } [self willReleaseChildren:self.childViewControllers]; [self willReleaseChild:self.presentedViewController]; If (self. IsViewLoaded) {/ / judge whether a UIViewController view has been loaded [self willReleaseChild: self. View]; } return YES; }Copy the code
The latter method, which is implemented in many places, has the following internal implementation
- (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
Commenting out the irrelevant code, we actually see that the willDealloc method is called in a loop. The commented out method is the recursive self.view, which is written to a stack viewStack and displayed in an Alertview. The stack is constructed by recursively iterating through the child objects and then adding the parent object’s class name to the child object’s class name to create a view stack. If a leak occurs, simply print the view Stack of this object.
+(void)addClassNamesToWhitelist:(NSArray *)classNames; Method is clear, used to add a whitelist.
The last method
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL # if_internal_MLF_rc_enabled // Just find a place to set up FBRetainCycleDetector dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dispatch_async(dispatch_get_main_queue(), ^{ [FBAssociationManager hook]; }); }); #endif Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSEL); Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL); BOOL didAddMethod = /* class_addMethod is used to add a method to a class, OriginalSEL is a method name, and method_getImplementtation is a method implementation that returns a value of type BOOL. There is no method called originalSEL in the current class. Instead of seeing the implementaion file, and seeing the swizzledMethod implementaion file, that will return true, Otherwise return false */ class_addMethod(class, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); If (didAddMethod) {//didAddMethod is true, swizzledMethod does not exist before, class_addMethod adds a name called origninalSEL, SwizzledMoethod function. class_replaceMethod(class, swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else {//didAddMethod is false, swizzledMethod already exists, Method_exchangeImplementations (originalMethod, swizzledMethod); } #endif }Copy the code
The middle BOOL type is used for:
“For the sake of prudence, there are two things to consider. In the first case, the overridden method is notimplemented in the target class (notimplemented), but in its parent class. The second case is if the method already exists in the target class. The two cases should be treated differently. The purpose is to replace the original method with a rewrite method. But the overridden method may be overridden in a parent class or in a subclass. In the first case, you should add a new implementation method to the target class (Override) and then replace the overridden method with the original implementation (Original one). For the second case (the method overridden in the target class). So we can do method_exchangeImplementations in this case.”
This Method gives a unified Method Swizzling’s Method for other classification.
Other classes
As for the other classes, they basically implement exchange methods. Don’t dwell on.
conclusion
MleaksFinder uses the idea of AOP and does not plug and play into business code. The detection process can be simplified as:
- A method that provides a method exchange for classification unity (this method guarantees that methods can be exchanged)
- And then we iterate through the view, the viewController, and add the name to the stack at run time
- Avoid methods (whitelist, target-action methods)
- Checks for circular references
However, MLeaksFinder has some limitations. We sometimes need to add some whitelists at any time, and there are also some unnecessary errors due to incomplete design consideration or Apple’s own mistakes.
Shortcomings that occur:
- CocoaPods cannot differentiate between compilation environments when joining
- After iOS 11.2, textField will fail.
- Adding a reference to itself also takes up some of the size
- Sometimes debug flashes (this is probably a Facebook bug, but because cocoaPods can’t change it)