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.
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?
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:
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:
- Add a property which holds your counter.
- 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:
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:
To understand what is actually going on above, let’s take a look at this struct from <objc/runtime.h>
:
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 forvoid
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:
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:
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:
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:
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:
Let’s test it by creating a simulated layout loop for a view and setting our counter for it:
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:
The final code:
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.
The article is first published on AppCoda.com.
Follow us on social media platforms:
Facebook: facebook.com/AppCodamobile/
Twitter: twitter.com/AppCodaMobile
Instagram: instagram.com/AppCodadotcom