Skip to content

Instantly share code, notes, and snippets.

@SheldonWangRJT
Last active April 11, 2021 09:45
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save SheldonWangRJT/5d2ea69f78a905c76e0c36dfc994e85c to your computer and use it in GitHub Desktop.
Save SheldonWangRJT/5d2ea69f78a905c76e0c36dfc994e85c to your computer and use it in GitHub Desktop.
WWDC18 Notes by Sheldon - Session 416 - iOS Memory Deep Dive

WWDC18 Notes - Session 416 - iOS Memory Deep Dive

All session list link: Here
Session Link: Here
Demo starts @ 25:30 in the video.
Download session slides: Here

This notes is written by Sheldon after watching WWDC18 session 416. You can find me with #iOSBySheldon in Github, Youtube, Facebook, etc.

Memory Basics in iOS

Memory are stored in into pages

  • Typically 16KB per page
  • Page has three types:
    • Clean: Allocated but not used or allocated and used by readonly objects
    • Dirty: Allocated and used by dynamic objects
    • Compressed: Will be explanded in the following section
  • App memory = number of pages * memory of each page (Dirty + Compressed, Clean memory is not considered being used)
int *array = malloc(20000 * sizeof(int))
array[0] = 32 
array[19999] = 64
// page 0 and final page is called dirty others are clean

Note: readonly objects that stored into pages are always clean (except for the final page is not fully used and later used as dirty memory)

Memory Compressor (from iOS 7)

iOS doesn't have memory swap like macOS, but iOS have the mechanism to compress the memory of un-accessed (after a while) pages and will decompress pages upon access

Memory warning:

  • App is not always the cause
  • Memory Compressor makes it more complicated.

Example:

override func didReceiveMemoryWarning() {
    cache.removeAllObjects() 
    // compressor may have to decompress first to use more memory
    super.didReceiveMemoryWarning()
}

Note:

  • Always remember compressor
  • Prefer NSCache over Dictionary (purgable and thread safe)

Memory Footprint

Memory footprint means the memory usage/record that we can track. Memory footprint limit:

  • Vary by device
  • Apps have a fairly high limit
  • Extensions have a much lower limit
  • If exceed limit, it will throw a EXC_RESOURCE_EXCEPTION

Note: Xcode 10 will catch EXC_RESOURCE_EXCEPTION (turn on all exception breakpoints)

Tools for Debugging

Tools for memory footprint:

  1. Xcode memory gage
  2. Instrument - Allocations
  3. Instrument - Leaks
  4. Instrument - VM Tracker (has info about dirty, compressed(swapped in macOS) pages)
  5. Instrument - Virtual memory trace (virtual memory performance, pages caches, etc)
  6. Xcode Memory debugger (upgraded a lot in Xcode 10), it can also export memory graph file *.memgraph.

Note: Tools 1-5 are more straight forward with friendly UI, but using terminal and *.memgraph file, we can do some more.

To work on memory graph files

  1. Xcode open memory graph debugging view
  2. Xcode menu -> file -> export memory graph file *.memgraph
  3. Open terminal and prepare to use your memory graph file

Commands

The commands shown in the demo are:

  1. $ vmmap (as similar for instrument - virtual memory trace)
  2. $ leaks (as similar for instrument - leaks)
  3. $ heap (to check memory that are allocated dynamically)
  4. $ malloc_history (to track allocation history)

Examples:

$ vmmap "filename.memgraph"
$ vmmap --summary "filename.memgraph"

Info like: Dirty, Swapped(compressed) labelling, in regions for heaps, etc)

$ vmmap --pages "filename" | grep '.dylib' | awk '{ sum += &6 } END { print "Total Dirty Pages: " sum }'

Output --- total dirty pages: 152.27

$ leaks "filename.memgraph"

Info about retain cycle, back track

$ heap "filename.memgraph" 
$ heap "filename.memgraph" -sortBySize (default sort by count)
$ heap "filename.memgraph" -address all | <classes-pattern>

Info about Class names, numbers of them, memory usage in bytes for each class
Note: Address is working well with malloc_history stack-trace logging, to turn it on, Scheme -> Run tab -> Diagnostics tab -> Logging -> check Malloc Stack

$ malloc_history -callTrace "filename.memgraph" [address]

This can show the backtrace.

Which tool to pick?

Dependent on your debugging purposes you can start with different tools, but generally, there are all pretty helpful.

  • Object Creation: malloc_history
  • Reference: leaks
  • Size: vmmap, heap

Image in iOS

The biggest concern of object related to memory management - IMAGE
Memory use if related the DIMENSION of the image, NOT file size.

The flow of image on iOS Load (590KB) -> Decode (10MB) -> Redender
A pick of 590KB image (2048px by 1536px) takes 10MB(2048 * 1536 * 4) memory in default format.

Image formatting:

  • SRGB format - default format with 4 dimension
  • Wide format - takes 8 dimensions for larger and better devices
  • Luminance and alpha format - 2 dimensions (grey and alpha)
  • Alpha 8 format - just 1 dimension - masks, texts

How to pick the best format?

Let system choose the better dimension for you and stop using UIGraphicsBeginImageContextWithOptions, old example:

// Circle via UIGraphicsImageContext
let bounds = CGRect(x: 0, y: 0, width:300, height: 100)
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
// Drawing Code
UIColor.black.setFill()
let path = UIBezierPath(roundedRect: bounds,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: 20, height: 20))
path.addClip()
UIRectFill(bounds)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Now use UIGraphicsImageRenderer - auto pick best format to render (iOS 10 and later)

// Circle via UIGraphicsImageRenderer
let bounds = CGRect(x: 0, y: 0, width:300, height: 100)
let renderer = UIGraphicsImageRenderer(size: bounds.size)
let image = renderer.image { context in
    // Drawing Code
    UIColor.black.setFill()
    let path = UIBezierPath(roundedRect: bounds,
    byRoundingCorners: UIRectCorner.allCorners,
    cornerRadii: CGSize(width: 20, height: 20))
    path.addClip()
    UIRectFill(bounds)
}
// Make circle render blue, but stay at 1 byte-per-pixel image
let imageView = UIImageView(image: image)
imageView.tintColor = .blueImages 

Down Sampling/Scaling Image

Note:

  1. UIImage is expensive for sizing and resizing (will decompress memory first, internal coordinate space transforms are expensive)
  2. Use ImageIO, it will work with out dirty memory (also API is faster)

Old example with UIKit:

import UIKit
// Getting image size
let filePath =/path/to/image.jpg”
let image = UIImage(contentsOfFile: filePath)
let imageSize = image.size
// Resizing image
let scale = 0.2
let size = CGSize(image.size.width * scale, image.size.height * scale)
let renderer = UIGraphicsImageRenderer(size: size)
let resizedImage = renderer.image { context in
    image.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
}

Better solution with ImageIO:

import ImageIO
let filePath =/path/to/image.jpg”
let url = NSURL(fileURLWithPath: path)
let imageSource = CGImageSourceCreateWithURL(url, nil)
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
let options: [NSString: Any] = [
    kCGImageSourceThumbnailMaxPixelSize: 100,
    kCGImageSourceCreateThumbnailFromImageAlways: true
]
let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options)

Other Tips

Off screen resources optimization:
Think about unload images when background the app or unload them from memory when tab bar controller disappear and load them when it's needed. This could help system release unnecessary memory and have a better chance of not letting the system automatically release your app when user put it into background.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment