iOS Programming · · 10 min read

Debugging Out of Memory Issues: Catching Layout Feedback Loop with the Runtime Magic

Debugging Out of Memory Issues: Catching Layout Feedback Loop with the Runtime Magic

Let’s imagine this scenario: you’ve got a successful app with a great number of daily users and 100% crash-free rate. You are happy and your life is amazing. But at some point you start seeing negative reviews coming to the App Store saying that it constantly crashes. Checking Fabric doesn’t help – no new crashes have appeared. What could it be then?

The answer is OOM (Out of Memory) termination.

As you use RAM on an end user’s device, the operating system can decide to reclaim that memory for other processes and terminate your app. We call this an Out of Memory termination. There might be various reasons for that:

  • retain cycles;
  • race conditions;
  • abandoned threads;
  • deadlocks;
  • layout feedback loop.

There are a lot of solutions provided by Apple to debug such kind of issues:

  • Allocations and Leaks instruments for resolving retain cycles and other types of leaks
  • Memory Debugger which has been introduced in Xcode 8 and replaces some functionality from Allocations and Leaks instruments
  • Thread Sanitizers help you find race conditions, abandoned threads or deadlocks

Layout Feedback Loop

We are going to take a look into layout feedback loop. It is not a very frequent type of issue, but once you encountered it, it could give you a lot of headache.

Layout feedback loops occur when your views are running their layout code, but part way through something causes them to start their layout pass again. This might be the result of one view changing the size of one of its superviews, or perhaps because you have an ambiguous layout. Either way, this problem manifests itself as your CPU maxing out and RAM usage steadily marching upwards, all because your views are running their layout code again and again without ever returning.

Paul Hudson from HackingWithSwift

Luckily for us, in WWDC16 Apple spent the whole 15 minutes(!) introducing “Layout Feedback Loop Debugger” which helps identify the moment when a loop happens during debugging. This is just a symbolic breakpoint and the way it works is as simple as it gets: it counts the amount of times layoutSubviews() method on each view has been called in a single run loop iteration. Once it exceeds a certain threshold (let’s say, 100), the app is going to stop at this breakpoint and print a nice log that helps you to find the root cause. Here is a nice article shortly describing how to work with this debugger.

This works perfectly if you can reproduce the issue. But what if you have dozens of screens, hundreds of views but your App Store reviews only say: “This app sucks, always crashing, never use it again!!!” ? You wish you could bring all these people to your office and set up Layout Feedback Loop Debugger for them. While the first part is not entirely achievable due to GDPR, you can try to replicate UIViewLayoutFeedbackLoopDebuggingThreshold in production code.

Let’s recall how this breakpoint works: it counts invocations of layoutSubviews() and sends an event when a certain threshold was exceeded in a single runloop iteration. Sounds easy, huh?

class TrackableView: UIView {
    var counter: Int = 0

    override func layoutSubviews() {
        super.layoutSubviews()

        counter += 1;
        if (counter == 100) {
            YourAnalyticsFramework.event(name: "loop")
        }
    }
}

This code works quite well for your view. But now you want to implement it on another views. You can, of course, create a subclass of UIView, implement it there and then inherit all the views in your project from it. And then do the same for UITableView, UIScrollView, UIStackView, and etc…

You wish you could just inject this logic into whatever class you wanted without writing tons of duplicated code. And this is exactly what runtime programming allows you to do.

We are going to do the same thing – create a subclass, override the layoutSubviews() method and count its invocations. The only difference is that all of this will be done at runtime instead of creating duplicated classes in your project.

Let’s start simple – we’ll create our custom subclass and will change the original view’s class to that new subclass:

struct LayoutLoopHunter {

    struct RuntimeConstants {
        static let Prefix = “runtime”
    }

    static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) {
        // We create the name for our new class based on the prefix for our feature and the original class name
        let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))”
        let originalClass = type(of: view)

        if let trackableClass = objc_allocateClassPair(originalClass, classFullName, 0) {
            // This class hasn’t been created during the current runtime session
            // We need to register our class and swap is with the original view’s class
            objc_registerClassPair(trackableClass)
            object_setClass(view, trackableClass)
        } else if let trackableClass = NSClassFromString(classFullName) {
            // We’ve previously allocated a class with the same name in this runtime session
            // We can get it from our raw string and swap with our view the same way
            object_setClass(view, trackableClass)
        }
    }
}

objc_allocateClassPair()’s documentation tells us when this method fails:

The new class, or Nil if the class could not be created (for example, the desired name is already in use)”.

This means that you cannot have 2 classes with the same name. Our strategy is to have a single created-at-runtime class for a single view class. That’s why we form the name for the new class by prefixing the original class name.

Now let’s add a counter to our subclass. Theoretically there are two ways you can do this:

  1. Add a property which holds your counter.
  2. Create an associated object for this class.

But in face, only one way is valid. You can think of a property as something that is stored in the memory that has been allocated for your class whereas an associated object will be stored in a completely different place. Since the memory that was allocated for an existing object is fixed, the newly added property on our custom subclass is going to “steal” it from some other resource. It can lead to unexpected behaviors and hard-to-debug crashes (check here for more info). But in case of using associated objects, they will be just stored in a hash table created during runtime, which is totally safe:

static var CounterKey = "_counter"

...

objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

Our new subclass is created, the counter is set to 0. Next, let’s implement the new layoutSubviews() method and add it to our class:

let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in
    guard let _self = nullableSelf else { return }

    if let counter = objc_getAssociatedObject(_self, &RuntimeConstants.CounterKey) as? Int {
        if counter == threshold {
            onLoop()
        }

        objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}
let implementation = imp_implementationWithBlock(layoutSubviews)
class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, "v@:")

To understand what is actually going on above, let’s take a look at this struct from <objc/runtime.h>:

struct objc_method {
    SEL method_name;
    char *method_types;
    IMP method_imp;
}

Even though we don’t use this struct directly in Swift anymore, it explains pretty clearly what a method actually consists of:

  • Implementation, which is the exact function going to be executed when your method is called. It always has the receiver and message selector as its first two parameters.
  • Method types string contains the signature of your method. You can read more about its formatting here, but in our case the string we need to specify is `”v@:”`. `v` stands for `void` as our return type, while `@` and `:` stand for the receiver and message selector respectively.
  • Selector is a key by which your method implementation will be looked up during runtime.

You can imagine the witness table (it’s also called dispatch table in other programming languages) as a simple dictionary data structure. The selector, then, would be your key and the implementation — the value. What we are doing in this line:

class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, "v@:")

is just assigning a new value for the key which corresponds to the layoutSubviews() method.

Straightforward as it gets. We get the counter, increase it by one. In case it exceeds our threshold, we send the analytics event with the name of the class and any payload we want.

Let’s recall how we’ve implemented and used the key for our associated object:

static var CounterKey = “_counter”

...

objc_setAssociatedObject(trackableClass, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

Why did we use var for our static key property for the counter variable and passed it everywhere by reference? The answer is hidden in basics of Swift language – Strings, like all the other value types, are passed by value. So, when you pass it to the closure, the string is being copied to a different address which would result in a completely different key in the associated objects table. The ampersand always ensures that the same address is given as value for the key parameter. Try the following code:

func printAddress(_ string: UnsafeRawPointer) {
    print("\(string)")
}

let str = "test"

printAddress(str)
printAddress(str)
let closure = {
    printAddress(str)
    printAddress(str)
}
closure()
// The addresses for last two function calls will always be different

Passing the key by reference is always a good idea because sometimes, even if you are not using a closure, the address of your variable can still be changed due to memory management. For our example, if you run the above code for certain amount of times, you may be able to see different addresses even for the first two calls of printAddress().

Let’s go back to our runtime magic. There is an important thing which we haven’t yet done in our new layoutSubviews() implementation. The thing that we normally do every time we override a method from an ancestor class – call the superclass implementation. The documentation of layoutSubviews() says:

The default implementation of this method does nothing on iOS 5.1 and earlier. Otherwise, the default implementation uses any constraints you have set to determine the size and position of any subviews.

For avoiding any unexpected layout behaviour, we have to call the superclass’s implementation. This will not be that straightforward as it usually is:

let selector = #selector(originalClass.layoutSubviews)
let originalImpl = class_getMethodImplementation(originalClass, selector)

// @convention(c) tells Swift this is a bare function pointer (with no context object)
// All Obj-C method functions have the receiver and message as their first two parameters
// Therefore this denotes a method of type `() -> Void`, which matches up with `layoutSubviews`
typealias ObjCVoidVoidFn = @convention(c) (Any, Selector) -> Void
let originalLayoutSubviews = unsafeBitCast(originalImpl, to: ObjCVoidVoidFn.self)
originalLayoutSubviews(view, selector)

What is actually going on here is that instead of the usual way to call a method (i.e. perform the selector which is going to look up in the witness table for the implementation), we are retrieving the needed implementation ourselves and calling it directly from our code.

Let’s see our implementation so far:

static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) {
    // We create the name for our new class based on the prefix for our feature and the original class name
    let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))”
    let originalClass = type(of: view)

    if let trackableClass = objc_allocateClassPair(originalClass, classFullName, 0) {
        // This class hasn’t been created during the current runtime session
        // We need to register our class and swap is with the original view’s class
        objc_registerClassPair(trackableClass)
        object_setClass(view, trackableClass)

        // Now we can create the associated object
        objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

        // Adding our layoutSubviews implementation
        let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in
            guard let _self = nullableSelf else { return }

            let selector = #selector(originalClass.layoutSubviews)
            let originalImpl = class_getMethodImplementation(originalClass, selector)

            // @convention(c) tells Swift this is a bare function pointer (with no context object)
            // All Obj-C method functions have the receiver and message as their first two parameters
            // Therefore this denotes a method of type `() -> Void`, which matches up with `layoutSubviews`
            typealias ObjCVoidVoidFn = @convention(c) (Any, Selector) -> Void
            let originalLayoutSubviews = unsafeBitCast(originalImpl, to: ObjCVoidVoidFn.self)
            originalLayoutSubviews(view, selector)

            if let counter = objc_getAssociatedObject(_self, &RuntimeConstants.CounterKey) as? Int {
                if counter == threshold {
                    onLoop()
                }

                objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
        let implementation = imp_implementationWithBlock(layoutSubviews)
        class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, “v@:“)
    } else if let trackableClass = NSClassFromString(classFullName) {
        // We’ve previously allocated a class with the same name in this runtime session
        // We can get it from our raw string and swap with our view the same way
        object_setClass(view, trackableClass)
    }
}

Let’s test it by creating a simulated layout loop for a view and setting our counter for it:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        LayoutLoopHunter.setUp(for: view) {
            print("Hello, world")
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        view.setNeedsLayout() // loop creation
    }
}

Did we miss something? Let’s recall again how the UIViewLayoutFeedbackLoopDebuggingThreshold breakpoint works:

Defines how many times a view must layout its subviews in a single run loop before it is considered to be a feedback loop.

We never took into account the “single run loop” condition. If our view stays in the screen for a significant amount of time and is regularly being laid out over and over again, sooner or later our counter will exceed the threshold. But this is not because of a memory issue.

How do we solve this? Simply by resetting the counter on every run loop iteration. For doing so, we can create a DispatchWorkItem which resets our counter and pass it asynchronously on the main queue. This way, it will be called when the run loop enters the main thread next time:

static var ResetWorkItemKey = “_resetWorkItem”

...

if let previousResetWorkItem = objc_getAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey) as? DispatchWorkItem {
    previousResetWorkItem.cancel()
}
let currentResetWorkItem = DispatchWorkItem { [weak view] in
    guard let strongView = view else { return }
    objc_setAssociatedObject(strongView, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
DispatchQueue.main.async(execute: currentResetWorkItem)
objc_setAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey, currentResetWorkItem, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

The final code:

struct LayoutLoopHunter {

    struct RuntimeConstants {
        static let Prefix = “runtime”

        // Associated objects keys
        static var CounterKey = “_counter”
        static var ResetWorkItemKey = “_resetWorkItem”
    }

    static func setUp(for view: UIView, threshold: Int = 100, onLoop: @escaping () -> ()) {
        // We create the name for our new class based on the prefix for our feature and the original class name
        let classFullName = “\(RuntimeConstants.Prefix)_\(String(describing: view.self))”
        let originalClass = type(of: view)

        if let trackableClass = objc_allocateClassPair(originalClass, classFullName, 0) {
            // This class hasn’t been created during the current runtime session
            // We need to register our class and swap is with the original view’s class
            objc_registerClassPair(trackableClass)
            object_setClass(view, trackableClass)

            // Now we can create the associated object
            objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

            // Adding our layoutSubviews implementation
            let layoutSubviews: @convention(block) (Any?) -> () = { nullableSelf in
                guard let _self = nullableSelf else { return }

                let selector = #selector(originalClass.layoutSubviews)
                let originalImpl = class_getMethodImplementation(originalClass, selector)

                // @convention(c) tells Swift this is a bare function pointer (with no context object)
                // All Obj-C method functions have the receiver and message as their first two parameters
                // Therefore this denotes a method of type `() -> Void`, which matches up with `layoutSubviews`
                typealias ObjCVoidVoidFn = @convention(c) (Any, Selector) -> Void
                let originalLayoutSubviews = unsafeBitCast(originalImpl, to: ObjCVoidVoidFn.self)
                originalLayoutSubviews(view, selector)

                if let counter = objc_getAssociatedObject(_self, &RuntimeConstants.CounterKey) as? Int {
                    if counter == threshold {
                        onLoop()
                    }

                    objc_setAssociatedObject(view, &RuntimeConstants.CounterKey, counter+1, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                }

                // Dispatch work item for reseting the counter on every new run loop iteration
                if let previousResetWorkItem = objc_getAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey) as? DispatchWorkItem {
                    previousResetWorkItem.cancel()
                }
                let counterResetWorkItem = DispatchWorkItem { [weak view] in
                    guard let strongView = view else { return }
                    objc_setAssociatedObject(strongView, &RuntimeConstants.CounterKey, 0, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
                }
                DispatchQueue.main.async(execute: counterResetWorkItem)
                objc_setAssociatedObject(view, &RuntimeConstants.ResetWorkItemKey, counterResetWorkItem, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
            let implementation = imp_implementationWithBlock(layoutSubviews)
            class_addMethod(trackableClass, #selector(originalClass.layoutSubviews), implementation, “v@:“)
        } else if let trackableClass = NSClassFromString(classFullName) {
            // We’ve previously allocated a class with the same name in this runtime session
            // We can get it from our raw string and swap with our view the same way
            object_setClass(view, trackableClass)
        }
    }
}

Conclusion

That’s it! Now you can set up analytics events for all your suspicious views, release the app and find out where exactly the issue occurs. You can narrow it down to a particular view and resolve the issue with the help of your users without them even knowing about this.

One last thing to mention is that with great power comes great responsibility. Runtime programming is very error-prone so it’s very easy to introduce another critical issue for your app without knowing. That’s why it’s always recommended to wrap all the dangerous code in your app in some kind of killswitch which you can trigger from the backend and disable the feature when you see that it’s causing issues. Here is a nice article about Feature Flags from Firebase.

The full code is available in this GitHub repository and also being distributed via CocoaPods for tracking Layout Loops in your projects.

P.S. I want to say special thanks to Aleksandr Gusev for his help with reviewing and giving more ideas for this article.

Read next