Using Apple’s Main Thread Checker Tool on UI Tests

Identify main thread issues while running UI Tests using the Main Thread Checker tool.


UI rendering is a complex and expensive operation. It requires synchronization and concurrency of so many objects and their states that it becomes necessary, from a performance standpoint, to make sure operations get executed from a single queue only.

Quoting from this great article on Thread-Safe Class Design by author Peter Steinberger on why UIKit is not inherently thread safe:

“It’s a conscious design decision from Apple’s side to not have UIKit be thread-safe. Making it thread-safe wouldn’t buy you much in terms of performance; it would in fact make many things slower. And the fact that UIKit is tied to the main thread makes it very easy to write concurrent programs and use UIKit. All you have to do is make sure that calls into UIKit are always made on the main thread.”

Hence, UIKit (and AppKit) APIs must be always called from the main thread. Otherwise, there is a high probability that your application will behave unexpectedly.

Safeguarding against main thread violations

Let's say you want to add a simple way to execute code on the main thread in your codebase. It would roughly be written this way wherever you are calling a UIKit API.

code block that shows an example of how to safeguard against Main Thread Violations

For obvious reasons, this would become burdensome very quickly, and significantly bloat your codebase. Fortunately, Apple has a Main Thread Checker (MTC) tool that comes in handy.

What is the Main Thread Checker?

The Main Thread Checker (MTC) is a runtime tool that throws a warning when system API calls that should be made on the main thread, such as UI operations, are incorrectly called on a background thread.

Enabling MTC

Enabling MTC for your app is very simple. Just toggle on the Main Thread Checker in the Diagnostic tab.

How to enable Main Thread Checker tool in edit scheme

From this point on, whenever you run your app and a main thread violation occurs, the control flow will hit a breakpoint letting you know that a violation has occurred.

code block that show an example of Main Thread Violation being caught by the Main Thread Checker tool

Using MTC in UI tests

Turning on MTC while running a UI test can enable your UI tests to detect these violations for you, since MTC violations can only be detected when you execute a specific code path during runtime.

Enabling MTC for your UI tests can be as simple as toggling on the Main Thread Checker option in your app’s scheme. Alternatively, you can do it programmatically with a couple of lines of code.

UI tests residing in the same project or workspace

If your UI test resides in the same workspace as your app, then you can simply check the checkbox for Main Thread Checker in your app’s scheme as shown below:

How to enable Main Thread Checker tool in edit scheme

When the UI tests are executed, Xcode will compile your app and launch it with the MTC enabled.

UI tests residing in a different project or workspace

If your UI tests live in a separate project or workspace, then the above approach will not work. Instead, tests in this configuration will not use the scheme settings of your app to launch your app.

To enable the MTC in this scenario, we would need to inject our MTC enable flag while launching the app from the UI Test workspace.

Going through Apple Docs gives us some idea of how Xcode enables MTC:

Because Main Thread Checker doesn’t require you to recompile your code, you can run it on an existing macOS binary.

Inject the dynamic library located at /Applications/Xcode.app/Contents/Developer/usr/lib/libMainThreadChecker.dylib into your executable.

So we can enable MTC by finding the correct libMainThreadChecker.dylib for the architecture and then injecting it in while launching the app.The path to dylib above (from Apple’s documentation) is for macOS. For different versions/OS of iOS Simulators, we would need to set the path to the dylib dynamically.

Finding the right path to the libMainThreadChecker.dylib

From the dyld man page:

Definition of DYLD_ROOT_PATH from the dyld man page

Copyright 1994-2021 The FreeBSD Project. All rights reserved. (https://www.freebsd.org/copyright/freebsd-doc-license/)

At launch time, the OS injects DYLD_ROOT_PATH as part of the environment variables. This can be used to find the relevant mainThreadChecker.dylib.

Printed value of ProcessInfo.processInfo.environment variable which gets passed during app launch in console.

Injecting DYLIBs in your app at launch

You can inject a dylib in your app by using DYLD_INSERT_LIBRARIES.

From the dyld man page:

Definition of DYLD_INSERT_LIBRARIES from the dyld man page

Combining the above two we can inject MTC dylib while launching the SUT app from the UI Tests like:

code block that show how to inject libMainThreadChecker.dylib into the app being UI tested.
code block that show an example of Main Thread Violation being caught by the Main Thread Checker tool

(The above will not occur if your app is not attached to the debugger, which is the case when the UI Tests and your app project are in different workspaces.)

A useful technique to deal with this scenario is to crash your app when such a violation occurs. To achieve that simply pass MTC_CRASH_ON_REPORT as ON to the environment variables.

code block that show how to inject libMainThreadChecker.dylib into the app being UI tested and enable crash mode using MTC_CRASH_ON_REPORT flag.

(From an extremely insightful article from Bryce Pauken where he lists a few hidden/poorly documented, but useful MTC features: https://bryce.co/main-thread-checker-configuration/)

Conclusion

UIKit APIs are not inherently thread-safe, which adds an additional burden on developers to sanitize all UIKit API call sites. The problem is especially exacerbated in large codebases where it is not uncommon to have several asynchronous callbacks. The Main Thread Checker tool can help mitigate some of that risk. Understanding how to use the MTC will allow you to use the UIKit APIs more effectively.

 

Technology vector created by stories - www.freepik.com

 


Manish Singh,

My name is Manish Singh and I am a Senior iOS engineer at Capital One. I am passionate about Apple's technology ecosystem and I enjoy using my skills to develop applications and tooling for iOS and Mac. During my free time I can be found exploring newer software technologies or playing Squash at my local sports center.


DISCLOSURE STATEMENT: © 2021 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.

Related Content