BuySellAds.com

Our DNA is written in Objective-C
Jump

UIView Background Queue Debugging

Over the the past few days we’ve been chasing an elusive bug that was testing the limits of our sanity. We repeated the following conversation about 3 times:

“Hey, we did some changes. The jumping views should not occur any more. We didn’t see it even after 2 hours of testing”
“I’m still getting it, right after launching the app.”
“&”§%, $%&% &!”

I would bet that this happened to you to, especially when working with background queues for updating some data and then updating UI to reflect the new information.

Similar to “did you reboot your PC?” being the standard answer to 99% of Windows problems, we iOS developers found that “are you maybe calling UIView methods on a background thread?” solves the Lion-share of problems with views. Here’s a convenient way how you can quickly find these elusive issues too.

Problems you see when updating UIKit views on something other than the main queue include, delayed animations, weird positions, views not redrawing and many others. Apple certainly has gotten better over the past iOS updates, but this means that it becomes less obvious where you are manipulating UIViews on background queues, since the symptoms become decoupled from the method call.

In our case we found by means of adding KVO for the “frame” keyPath that a view’s frame was being manipulated on a background queue. Further down the stack we say that this was done in a layoutSubviews… and there the stack ended. You couldn’t see who caused the layoutSubviews since iOS does that only once per run loop. Multiple places could have called setNeedsLayout on the view and then layoutSubviews would only be executed once.

Counterintuitively the layoutSubviews got called on the same background queue that setNeedsLayout was triggered. It was a completion handler for downloading an image. Completion blocks are typically called on a background queue and we where calling setNeedsLayout there.

This lead to my idea of having a way to globally enable main-queue checking for the important UIView methods. You don’t have to check it for each and every method since almost all of them either call setNeedsDisplay or setNeedsLayout. For example setBackgroundColor: also calls setNeedsDisplay.

Hacked!

Now, how do you replace Apple’s methods with your own so that you can add extra behavior? Method Swizzling!

You can find that in DTFoundation. Update: this code is no longer current, get the latest version here.

UIView+DTDebug.m

#import "UIView+DTDebug.h"
#import "NSObject+DTRuntime.h"
 
@implementation UIView (DTDebug)
 
- (void)methodCalledNotFromMainQueue:(NSString *)methodName
{
    NSLog(@"-[%@ %@] being called on background queue. Break on -[UIView methodCalledNotFromMainQueue:] to find out where", NSStringFromClass([self class]), methodName);
}
 
- (void)_setNeedsLayout_MainQueueCheck
{
    if (dispatch_get_current_queue() != dispatch_get_main_queue())
    {
        [self methodCalledNotFromMainQueue:NSStringFromSelector(_cmd)];
    }
 
    // not really an endless loop, this calls the original
    [self _setNeedsLayout_MainQueueCheck]; 
}
 
- (void)_setNeedsDisplay_MainQueueCheck
{
    if (dispatch_get_current_queue() != dispatch_get_main_queue())
    {
        [self methodCalledNotFromMainQueue:NSStringFromSelector(_cmd)];
    }
 
    // not really an endless loop, this calls the original
    [self _setNeedsDisplay_MainQueueCheck];
}
 
- (void)_setNeedsDisplayInRect_MainQueueCheck:(CGRect)rect
{
    if (dispatch_get_current_queue() != dispatch_get_main_queue())
    {
        [self methodCalledNotFromMainQueue:NSStringFromSelector(_cmd)];
    }
 
    // not really an endless loop, this calls the original
    [self _setNeedsDisplayInRect_MainQueueCheck:rect];
}
 
+ (void)toggleViewMainQueueChecking
{
    [UIView swizzleMethod:@selector(setNeedsLayout) 
            withMethod:@selector(_setNeedsLayout_MainQueueCheck)];
    [UIView swizzleMethod:@selector(setNeedsDisplay) 
            withMethod:@selector(_setNeedsDisplay_MainQueueCheck)];
    [UIView swizzleMethod:@selector(setNeedsDisplayInRect:)
            withMethod:@selector(_setNeedsDisplayInRect_MainQueueCheck:)];
}
 
@end

You only need to care about +toggleViewMainQueueChecking which globally enables the main queue checking. It replaces the three mentioned methods with our own and then calls the original implementation. This way your app still works, but each call to these methods, on any UIView subclass, is now being checked.

The check methods seemingly calling themselves is not really an endless loop. The swizzling exchanges implementations, but it does not exchange the method selectors in your code. So after one toggle, you actually are calling the replaced method with _setNeedsDisplay_MainQueueCheck.

How to Use

Since each swizzling exchanges the method implementations calling toggle twice restores the original behavior. And of course you should never use this in a production app. But for developing you can add something like this to your app delegate:

Adding a symbolic breakpoint

With this breakpoint in place execution halts as soon as this method is being called so that you can inspect the call stack. From this method you simply go down to the first method that is your own and you have caught the culprit red-handed.

Found it!

Aha! In this instance setCatalogImage: uses UIButton’s setImage:forState: which in turn triggers a setNeedsLayout. The system API is in gray while our own methods are black. So this catches also a setImage, even though we didn’t overwrite it.

To fix this problem, just wrap it in a dispatch_async on the main queue.

Update: May or May Not

While the above code seems to does its job very well there is a problem with it, as Javier Soto pointed out in a pull request. According to the GCD documentation we are not supposed to compare the current queue like that:

When dispatch_get_current_queue() is called on the main thread, it may or may not return the same value as dispatch_get_main_queue(). Comparing the two is not a valid way to test whether code is executing on the main thread.

If you thinking about it, we always heard that you should only call UIKit methods on the main thread. Which is why we changed the actual implementation of UIView+DTDebug to check for this instead of main queues.

The main principle is the same, but I renamed all instances of queue to thread to reflect the difference.

Conclusion

During the time that I was writing this blog post my colleague already found 3 places in our codebase where this was happening. I’m pretty sure that if you use this technique on any larger app you can possibly finds instances of this problem.

The methods described in this article can be found as part of my DTFoundation framework.If you find a method on UIView that does not trigger the setNeedsX methods then please let me know, so that we can add it.

Please comment (either here or on Twitter) if you were able to squash any main thread bugs in your code with this.


Categories: Recipes

%d bloggers like this: