Understanding and Reimplementing Apple's Main Thread Checker on iOS
This article explores Apple's libMainThreadChecker.dylib implementation, details its environment variables and swizzling logic, demonstrates how to replicate its functionality by enumerating UIKit classes and hooking methods, and discusses pitfalls of full‑method hooking and a bridge‑based solution for reliable runtime monitoring.
With the release of iOS 11, many teams started compiling with Xcode 9, which introduced the dynamic library libMainThreadChecker.dylib that performs main‑thread checking at runtime. This article begins by dissecting Apple’s implementation and then shows how to recreate similar functionality.
0x1 Apple’s Implementation
Inspecting the library in Hopper reveals suspicious symbols such as __library_initializer and __library_deinitializer . The library relies on several environment variables (settable via Xcode → Scheme → Arguments), the most important being MTC_VERBOSE , which enables verbose output of monitored classes.
...
Swizzling class: UIKeyboardEmojiCollectionViewCell
Swizzling class: UIKeyboardEmojiSectionHeader
Swizzling class: UIPrinterSetupPINScrollView
Swizzling class: UIPrinterSetupPINView
Swizzling class: UIPrinterSetupConnectingView
Swizzling class: UICollectionViewTableHeaderFooterView
Swizzling class: UIPrinterSetupDisplayPINView
Swizzling class: UIStatusBarMapsCompassItemView
Swizzling class: UIStatusBarCarPlayTimeItemView
Swizzling class: UIKeyboardCandidateBarCell
Swizzling class: UIKeyboardCandidateBarCell_SecondaryCandidate
Swizzling class: UIActionSheetiOSDismissActionView
Swizzling class: UIKeyboardCandidateFloatingArrowView
Swizzling class: UIKeyboardCandidateGridOverlayBackgroundView
Swizzling class: UIKeyboardCandidateGridHeaderContainerView
Swizzling class: UIStatusBarBreadcrumbItemView
Swizzling class: UIInterfaceActionGroupView
Swizzling class: UIKeyboardFlipTransitionView
Swizzling class: UIKeyboardAssistantBar
Swizzling class: UITextMagnifier
Swizzling class: UIKeyboardSliceTransitionView
Swizzling class: UIWKSelectionView
Swizzled 10717 methods in 384 classes.The library swizzles a large number of UIKit classes before the app starts, effectively inserting thread‑monitoring hooks into them.
1.1 Environment Variables
The library reads many environment variables; setting MTC_VERBOSE=1 prints which classes are being monitored.
1.2 Logic
CFAbsoluteTimeGetCurrent();
var_270 = intrinsic_movsd(var_270, xmm0);
*_indirect__main_thread_checker_on_report = dlsym(0xfffffffffffffffd, "__main_thread_checker_on_report");
if (objc_getClass("UIView") != 0x0) {
*_XXKitImage = dyld_image_header_containing_address(objc_getClass("UIView"));
*_CoreFoundationImage = dyld_image_header_containing_address(_CFArrayGetCount);
rax = objc_getClass("WKWebView");
rax = dyld_image_header_containing_address(rax);
*_WebKitImage = rax;
// ... many more lines omitted for brevity ...
_addSwizzler(r13, r12, var_258, r15, 0x1);
}
*_totalSwizzledClasses = rcx;
if (*(_envVerbose) != 0x0) {
rdx = *_totalSwizzledMethods;
fprintf(*___stderrp, "Swizzled %zu methods in %zu classes.\n", rdx, rcx);
}The core steps are:
Locate the image containing UIView (UIKit) and WKWebView (WebKit).
Collect all classes that inherit from UIView or UIApplication .
For each class, enumerate its methods, filter out a whitelist of selectors (e.g., retain , release , autorelease , etc.) and those prefixed with nsli_ or nsis_ .
Swizzle the remaining methods to redirect them to the checker.
1.3 Existing Hooking Pitfalls
Using a naïve method‑swizzling approach (e.g., AnyMethodLog or BigBang) crashes because the same IMP is used for both a class and its superclass, breaking the objc_msgSendSuper path.
0x2 Self‑Implementation
Re‑creating the checker can be done in roughly one to two hours using public APIs. The following code enumerates all UIKit classes that are subclasses of UIView or UIApplication :
NSArray *findAllUIKitClasse() {
static NSMutableArray *viewClasses = nil;
if (!viewClasses) return classes;
uint32_t image_count = _dyld_image_count();
for (uint32_t image_index = 0; image_index < image_count; image_index++) {
const my_macho_header *mach_header = (const my_macho_header *)_dyld_get_image_header(image_index);
const char *image_name = _dyld_get_image_name(image_index);
NSString *imageName = [NSString stringWithUTF8String:image_name];
if ([imageName hasSuffix:@"UIKit"]) {
unsigned int count;
const char **classes;
Dl_info info;
dladdr(mach_header, &info);
classes = objc_copyClassNamesForImage(info.dli_fname, &count);
for (int i = 0; i < count; i++) {
const char *className = (const char *)classes[i];
NSString *classname = [NSString stringWithUTF8String:className];
if ([classname hasPrefix:@"_"]) continue;
Class cls = objc_getClass(className);
Class superCls = cls;
bool isNeedChild = NO;
while (superCls != [NSObject class]) {
if (superCls == NSClassFromString(@"UIView") || superCls == NSClassFromString(@"UIApplication")) {
isNeedChild = YES; break;
}
superCls = class_getSuperclass(superCls);
}
if (isNeedChild) {
// Hook methods of cls here
[viewClasses addObject:cls];
}
}
break;
}
}
return viewClasses;
}2.1 Limitations of Full‑Method Hooking
Replacing every method’s IMP with a single forward‑invocation function loses the superclass context, causing infinite loops when a subclass calls super (e.g., UIButton initWithFrame: calling UIView initWithFrame: ).
2.2 Bridge‑Based Full Hook Solution
The proposed solution introduces a stub object ( WZQMessageStub ) that stores the original class and selector, creates a unique method name for each combination, and forwards all messages to this stub via forwardingTargetForSelector: . The stub implements methodSignatureForSelector: and forwardInvocation: to finally invoke the original implementation, preserving the correct super context.
Each method is redirected to a distinct IMP bridge → the bridge infers the current call context (class + selector) → a new intermediate selector name is constructed → forwardingTargetForSelector(self, newSelector) forwards the call. The detailed bridge implementation will be covered in a separate post.
0x3 Open Questions
Even after enabling the Main Thread Checker, the libMainThreadChecker.dylib does not appear in the Mach‑O load commands, nor is it visible via a dlopen breakpoint. The author asks how Apple loads this library at runtime.
Further discussion and community input are welcomed.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.