Memory optimization tool

Static analysis :Analyze

Analyze mainly analyzes the following four types of problems:

  1. Logic error: Accessing null Pointers or uninitialized variables, etc

  2. Memory management errors, such as memory leaks

  3. Declaration error: variable never used

  4. Api call error: library and framework not included

Instruments and Allocations

This tool shows the actual usage of the application and can be sorted by size. We just need to find out which ones have high occupancy, analyze their causes and find corresponding solutions

  1. The red arrow, this red X is where the memory leak is

  2. Find the “field” pattern in the image above and change the Statictics to “Call Trees” so you can see the Call Tree setting at the bottom. Filter out the system methods

Activity detector – ActivityMonitor

Perform the operation (after entering a page and exiting)-> filter your APP. If Memory does not fade, a Memory leak will occur

Zombie object -Zombiles

  • Zombie object An object used to detect memory errors EXC_BAD_ACCESS, which can catch any attempts to access bad memory

  • If you send a message to a zombie object, it crashes at run time and prints an error log. The log allows you to locate the method and class name called by the wild pointer object

How do I enable Zombile Object detection

1. In Xcode, go to Edit Scheme -> Diagnostics -> Zombie Objects****

void printClassInfo(id obj) { Class cls = object_getClass(obj); Class superCls = class_getSuperclass(cls); NSLog(@"self:%s - superClass:%s", class_getName(cls), class_getName(superCls)); } int main(int argc, const char * argv[]) { @autoreleasepool { People *aPeople = [People new]; NSLog(@"before release!" ); printClassInfo(aPeople); [aPeople release]; NSLog(@"after release!" ); printClassInfo(aPeople); } return 0; }Copy the code

Viewing Printed Information

ZombieObjectDemo[1357:84410] before release!
ZombieObjectDemo[1357:84410] self:People - superClass:NSObject
ZombieObjectDemo[1357:84410] after release!
ZombieObjectDemo[1357:84410] self:_NSZombie_People - superClass:nil
Copy the code

2. According to the printed information, after zombie object detection is enabled, the class of People released becomes _NSZombie_People. In this way, the released object will become a zombie object

3. Next open Instruments ->Zombies and see what dealloc is doing. Zombie Objects hook the object’s dealloc method and zombify the object by calling its own __dealloc_zombie method. This is also explained in the dealloc method annotation in the Runtime source nsobject. mm file. As follows:

// Replaced by NSZombies
- (void)dealloc {
    _objc_rootDealloc(self);
}
Copy the code

Look at the object dealloc method call stack

Simulate zombie object generation

CLS = object_getClass(self); Const char *zombieClsName = "_NSZombie_" + clsName; const char *zombieClsName = "_NSZombie_" + clsName; ZombieCls = objc_lookUpClass(zombieClsName); zombieClsName = objc_lookUpClass(zombieClsName); if (! ZombieCls) {//5, get zombie object Class _NSZombie_ Class baseZombieCls = objc_lookUpClass(" _NSZombie_"); //6, Create zombieClsName class zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0); } // delete member variables and associated references from the object if the memory has not been freed. objc_destructInstance(self); // Change the isa pointer to objc_setClass(self, zombieCls);Copy the code

How is the Zombile Object triggered

[people release] abort ___forwarding___. The assembly code at this point shows the _NSZombie_ keyword. Message sent to deallocated instance %p”. So you can probably guess that the system is manipulating the message forwarding process.

CoreFoundation`___forwarding___:
    0x7fff3f90b1cd <+269>:  leaq   0x35a414(%rip), %rsi      ; "_NSZombie_"
Copy the code

So let’s summarize its call process:

CLS = object_getClass(self); Const char *clsName = class_getName(CLS); _NSZombie_ if (string_has_prefix(clsName, Const char *originalClsName = substring_from(clsName, 10); Const char *selectorName = sel_getName(_cmd); If (' *** * - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self); // Abort (); }Copy the code

Diagnostics also has a few options to help us find the app’s memory issues

1. Enable Malloc Scribble

After the memory is applied, fill the memory with 0xAA. After the memory is released, fill the memory with 0x55. This means that if memory is accessed without being initialized, or if memory is accessed after being freed, an exception will be raised and the problem will leak out as quickly as possible.

Scribble is a debugging solution provided by the malloc library libsystem_malloc.dylib itself

2. Enable Malloc Guard Edges

Add protection on the front and back pages when applying for large memory. For details, see Protection mode.

3. Enable Guard Mallocs

Use libgmalloc to catch common memory problems, such as out of bounds and continued use after release.

Since libgmalloc does not exist on the real machine, this feature can only be used on the emulator.

4. Enable Zombie Objects

Zombie replaces the implementation of Dealloc by generating Zombie objects. When the object reference count reaches zero, the object requiring dealloc is converted to Zombie objects. If a message is later sent to the zombie object, an exception is thrown and the corresponding message is printed so that the debugger can easily find the exception location.

5. Enable Address Sanitizer (Xcode7 +)

AddressSanitizer works when an application creates a variable to allocate memory, freezing the memory next to it and marking it as poisoned memory. When the program accesses poisoned memory (out of bounds access), it throws an exception and prints the corresponding log information. Debuggers can identify bugs based on interrupt location and log information. If the variable is freed, the memory occupied by the variable is also marked as poisoned memory, and accessing this memory will also throw an exception (accessing the freed object).

MLeaksFinder

Tencent is an open source memory leak search tool, which can immediately remind the memory leak in the process of using APP

The Memory of the Xcode Graph

This tool can complement MLeaksFinder in finding memory leaks, analyzing circular reference relationships between objects. In addition, by analyzing the Live Objects at a certain moment, we can analyze which are unreasonable

Now you are in breakpoint mode. You can view the Issue panel and select the Runtime on the right:

A lot of exclamation marks is a problem. Closure captures leaked Specifies the name of the object. After expansion, click to see the memory graph corresponding to the issue displayed in the middle panel

FBMemoryProfiler

Git is created by FaceBook

Reasons for high memory usage:

Using an irrational API

1. For large image resources that are only used once or rarely used, the [UIImage imageNamed:] method is used to load them

Image loading, there are two ways, one is [UIImage imageNamed:], after loading the system will cache, and there is no API can clean; The other option is [UIImage imageWithContentsOfFile:] or [[UIImage Alloc] initWithContentsOfFile:]. The system does not cache the image, and when the image is no longer referenced, its memory is completely freed.

Based on the above characteristics, the latter should be used for large image resources that are used only once or infrequently. When using the latter, be careful not to put images inside Assets.

2. Some of the images themselves are perfect for stretching with the 9 image mechanic, but are not optimized accordingly

The memory footprint of images is very large, and for images suitable for stretching with the 9-slice mechanism, it is possible to cut out a much smaller image than the actual size, thus greatly reducing the memory footprint. Like the picture below

The part between the left and right vertical lines is solid color, so in the design of the cut figure, as long as this part of the cut very small. We can then use Xcode’s Slicing feature to specify which parts of the image don’t stretch and which parts do. When loading images, load them in the normal way.

3. The method -[UIColor colorWithPatternImage:] is used when it is not necessary

There is code in the project that uses UILabel and sets the background color of the label to an image. To convert the image to color, the above method is used. This method refers to an image that has been loaded into memory, and then creates another image in memory, which is very memory intensive

Solution: In this scenario, it makes sense to use UIButton and set the image as the background image. Although UIButton generates two more views than UILabel, it’s worth it compared to the memory footprint of the image.

4. Use the Core Graphics API to change the color of a UIImage object unnecessarily

Using this API causes an additional image to be generated in memory, which is very memory intensive. The logical thing to do is:

  • Set the tintColor property of UIView
  • Will be loaded pictures UIImageRenderingModeAlwaysTemplate manner
view.tintColor = theColor;
UIImage *image = [[UIImage imageNamed:name] imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate]
Copy the code

5. Too large when creating solid color based images

Sometimes, we need to create UIImage based on color and use it as the background color of UIButton in different states. Since the image is a solid color, there is no need to create an image that is the same size as the view, just 1px wide and 1px high

+ (UIImage*)createImageWithColor:(UIColor *)color {return [self createImageWithColor: color andSize: CGSizeMake(1, 1)]; } + (UIImage*)createImageWithColor:(UIColor*)color andSize:(CGSize)size {CGRect rect=CGRectMake(0,0, size.width, size.height); UIGraphicsBeginImageContext(rect.size); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(context, [color CGColor]); CGContextFillRect(context, rect); UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return theImage; }Copy the code

6. Creating a horizontal gradient image is too large

In some parts of the project, a horizontal left-to-right gradient image was created in memory using Core Graphics based on color. The size of the image is the size of the view, which causes a large memory overhead in some occasions. Take a 400×60 view on the @3x device as an example, its memory overhead is:

400 x 3 x 60 x 3 x 4/1024 = 210KB. But this image, 400px wide and 1px high, would have exactly the same effect, with a memory overhead of 400 * 1 * 4/1024 = 1.56KB

7. In a custom UIView subclass, draw with the drawRect: method

Custom drawRects consume a lot of memory for your APP, and the larger the view, the more memory it consumes. Memory consumption = (width * scale * height * scale * 4/1024/1024)MB

In almost all cases, drawing needs can be fulfilled through a CAShapeLayer. CAShapeLayer (drawRect:)

It has the following advantages:

  • Render fast. CAShapeLayer uses hardware acceleration to draw the same Graphics much faster than using Core Graphics.
  • Use memory efficiently. A CAShapeLayer doesn’t need to create a boarding graphic like a regular CALayer, so no matter how big it is, it doesn’t take up too much memory.
  • Will not be clipped by layer boundaries.
  • There will be no pixelation

8. In a custom CALayer subclass, draw with the – (void)drawInContext: method

Similar to the previous tip, use CAShapeLayer to draw as much as possible.

9. UILabel size is too large

If the size of a UILabel is larger than its intrinsicContentSize, it incurs unnecessary memory consumption. Therefore, during view layout we should try to make the size of the UILabel equal to its intrinsicContentSize. The reader can write a simple example program about this, and then analyze it using the Instruments tool, and you can see that the Core Animation item is significantly increased in Allocations.

10. Set the background color for UILabel

If the background color is not clearColor, whiteColor, it will incur memory overhead. So, once you get to this situation, you can change your view structure to UIView+UILabel, which gives UIView a background color, and UILabel is just for displaying text.

You can also verify this by writing a sample program using the Instruments tool.

Images downloaded from the Internet are too large

Almost all iOS applications use the framework SDWebImage to load web images. Sometimes the image load is too large, for this case, also need to be analyzed according to the specific scene, adopt different solutions.

1. The view is too large and the image cannot be zoomed

If the image size is reasonable, all we can do is remove the downloaded image from the memory cache when the view is freed. Example code is as follows:

- (void)dealloc { for (NSString *imageUrl in self.datas) { NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL: [NSURL URLWithString: imageUrl]]; [[SDImageCache sharedImageCache] removeImageForKey: key fromDisk: NO withCompletion: nil]; }}Copy the code

The code above will make the high memory usage only appear on one page, and once you return from that page, the memory will return to normal.

2. The image should be zoomed when the view is small

If the view used to display the image is small and the downloaded image is large, we should scale the image and then save the scaled image to SDWebImage’s in-memory cache.

Example code is as follows:

// Add the following classification method to UIImage:  - (UIImage*)aspectFillScaleToSize:(CGSize)newSize scale:(int)scale { if (CGSizeEqualToSize(self.size, newSize)) { return self; } CGRect scaledImageRect = CGRectZero; CGFloat aspectWidth = newSize.width / self.size.width; CGFloat aspectHeight = newSize.height / self.size.height; CGFloat aspectRatio = MAX(aspectWidth, aspectHeight); scaledImageRect.size.width = self.size.width * aspectRatio; scaledImageRect.size.height = self.size.height * aspectRatio; ScaledImageRect. Origin. X = (newSize. Width - scaledImageRect. Size, width) / 2.0 f; ScaledImageRect. Origin. Y = (newSize. Height - scaledImageRect. Size, height) / 2.0 f; int finalScale = (0 == scale) ? [UIScreen mainScreen].scale : scale; UIGraphicsBeginImageContextWithOptions(newSize, NO, finalScale); [self drawInRect:scaledImageRect]; UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return scaledImage; } - (UIImage*)aspectFitScaleToSize:(CGSize)newSize scale:(int)scale { if (CGSizeEqualToSize(self.size, newSize)) { return self; } CGRect scaledImageRect = CGRectZero; CGFloat aspectWidth = newSize.width / self.size.width; CGFloat aspectHeight = newSize.height / self.size.height; CGFloat aspectRatio = MIN(aspectWidth, aspectHeight); scaledImageRect.size.width = self.size.width * aspectRatio; scaledImageRect.size.height = self.size.height * aspectRatio; ScaledImageRect. Origin. X = (newSize. Width - scaledImageRect. Size, width) / 2.0 f; ScaledImageRect. Origin. Y = (newSize. Height - scaledImageRect. Size, height) / 2.0 f; int finalScale = (0 == scale) ? [UIScreen mainScreen].scale : scale; UIGraphicsBeginImageContextWithOptions(newSize, NO, finalScale); [self drawInRect:scaledImageRect]; UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return scaledImage; } // localhost [self.leftimageView sd_setImageWithURL:[NSURL URLWithString:md.image] placeholderImage:[UIImage] placeholderImage imageNamed:@"discover_position"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { if (image) { UIImage *scaledImage = [image aspectFillScaleToSize: self.leftImageView.bounds.size scale: 2]; if (image ! = scaledImage) { self.leftImageView.image = scaledImage; [[SDWebImageManager sharedManager] saveImageToCache: scaledImage forURL: imageURL]; } } }];Copy the code

Caching mechanisms for third-party libraries

1. Lottie Animation frame

The Lottie framework caches information such as animation frames by default, and if an application uses animation in many ways, a large amount of cached information can accumulate over time. However, some cache information may never be used again, such as the cache caused by the animation of a splash page.

The memory footprint caused by Lottie’s cache can be handled in either of the following ways as you wish:

  • Prohibit the cache
[[LOTAnimationCache sharedCache] disableCaching];
Copy the code
  • Do not disable caching, but clear the entire cache, or the cache of an animation, when appropriate
// Clear all caches. For example, if the splash screen page is not accessed again after startup, it can clear the caches caused by the animation of this interface. [[LOTAnimationCache sharedCache] clearCache]; // After returning from a page, you can remove the cache caused by the animation used for that page. [[LOTAnimationCache sharedCache] removeAnimationForKey:key];Copy the code

2. SDWebImage

The cache mechanism of SDWebImage is divided into Disk and Memory. The Memory layer enables the image to be accessed without file IO process and improves the performance. By default, Memory stores decompressed image data, which can result in significant Memory overhead. If you want to optimize the memory footprint, you can choose to store compressed image data and add the following code to the application startup:

[SDImageCache sharedImageCache].config.shouldDecompressImages = NO;
[SDWebImageDownloader sharedDownloader].shouldDecompressImages = NO;
Copy the code

3. Implement resident memory for objects that do not need to reside in memory

Interface objects such as sidebars and Actionsheets should not be implemented as resident memory. Instead, they should be created when needed and destroyed when used.

4. Redundant fields in data model

As the data returned from the server is parsed into the model, there may be some fields that are no longer used as the version iterations. If a large number of such model objects are generated, cleaning up redundant fields in the model can also save a certain amount of memory footprint.

5. Memory leaks

I don’t want to go over this too much