Files
knowledge-kit/Chapter1 - iOS/1.43.md
2026-02-22 22:12:36 +01:00

6.0 KiB
Raw Permalink Blame History

Debugging Tricks

  1. In daily development we often encapsulate a feature module and expose a method for external use. But many times callers don't pass parameters according to the contract. We use assertions to catch this. However, in production if assertions are used the app will crash. Xcode provides a small feature to address this.

NS_BLOCK_ASSERTIONS: prevents NSAssert from firing in Release builds; adding this single macro will filter out NSAssert. How: In Build Settings search for Preprocessor Macros, then add NS_BLOCK_ASSERTIONS under the Release configuration.

Breakpoints

Categories

Breakpoints include Normal Breakpoint, Exception Breakpoint, OpenGL ES Error Breakpoint, Symbolic Breakpoint, Test Failure Breakpoint, and Watchpoint. Use different breakpoint types according to the scenario.

NSAssert and dispatch_once

NSAssert is very common in development, especially when building SDKs and libraries. Assertions help catch problems during development and enforce expected behavior. NSAssert essentially raises an exception; when an exception occurs it triggers the C function objc_exception_throw.

NSAssert assertion

Callback information example:

*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff23c7127e __exceptionPreprocess + 350
	1   libobjc.A.dylib                     0x00007fff513fbb20 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff23c70ff8 +[NSException raise:format:arguments:] + 88
	3   Foundation                          0x00007fff256e9b51 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
	4   TEst                                0x0000000106edfeef -[AppDelegate application:didFinishLaunchingWithOptions:] + 287
	5   UIKitCore                           0x00007fff48089ad8 -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 232
	6   UIKitCore                           0x00007fff4808b460 -[UIApplication _callInitializationDelegatesWithActions:forCanvas:payload:fromOriginatingProcess:] + 3980
	7   UIKitCore                           0x00007fff48090f05 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1226
	8   UIKitCore                           0x00007fff477c57a6 -[_UISceneLifecycleMultiplexer completeApplicationLaunchWithFBSScene:transitionContext:] + 179
	9   UIKitCore                           0x00007fff4808d514 -[UIApplication _compellApplicationLaunchToCompleteUnconditionally] + 59
	10  UIKitCore                           0x00007fff4808d813 -[UIApplication _run] + 754
	11  UIKitCore                           0x00007fff48092d4d UIApplicationMain + 1621
	12  TEst                                0x0000000106ee0144 main + 116
	13  libdyld.dylib                       0x00007fff5227ec25 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

You can clearly see when an assertion fails Xcode can precisely locate the line of code where NSAssert occurred—source is available. But in some scenarios exceptions cannot be traced to the real origin. For example, in large apps people often build Pods as static libraries to speed up builds; in those cases the exception cannot be precisely traced to the offending source line. If the assertion occurs inside a GCD block and the source context is missing, you also cannot pinpoint it.

Example output:

*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff23c7127e __exceptionPreprocess + 350
	1   libobjc.A.dylib                     0x00007fff513fbb20 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff23c70ff8 +[NSException raise:format:arguments:] + 88
	3   Foundation                          0x00007fff256e9b51 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
	4   TEst                                0x000000010f242e95 __57-[AppDelegate application:didFinishLaunchingWithOptions:]_block_invoke + 229
	5   libdispatch.dylib                   0x000000010f55fdd4 _dispatch_call_block_and_release + 12
	6   libdispatch.dylib                   0x000000010f560d48 _dispatch_client_callout + 8
	7   libdispatch.dylib                   0x000000010f56ede6 _dispatch_main_queue_callback_4CF + 1500
	8   CoreFoundation                      0x00007fff23bd4049 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
	9   CoreFoundation                      0x00007fff23bceca9 __CFRunLoopRun + 2329
	10  CoreFoundation                      0x00007fff23bce066 CFRunLoopRunSpecific + 438
	11  GraphicsServices                    0x00007fff384c0bb0 GSEventRunModal + 65
	12  UIKitCore                           0x00007fff48092d4d UIApplicationMain + 1621
	13  TEst                                0x000000010f243134 main + 116
	14  libdyld.dylib                       0x00007fff5227ec25 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Add a Symbolic Breakpoint in Xcode for the symbol objc_exception_throw; then you can see the full stack in Xcodes left-side navigator.

objc_exception_throw

Check GCDs _dispatch_client_callout to see if theres anything unusual. You can find libdispatch source here: https://opensource.apple.com/tarballs/libdispatch/.

// object.m
#undef _dispatch_client_callout
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
	@try {
		return f(ctxt);
	}
	@catch (...) {
		objc_terminate();
	}
}

Youll find _dispatch_client_callout wraps the GCD block invocation in a try/catch that catches Objective-C exceptions and then calls objc_terminate, which breaks the call stack.

Theres a scenario where a crash shows up inside dispatch_once because code inside dispatch_once threw an Objective-C exception. Big companies often see this early on; later they add code around assertions specifically to record an owner or provide better diagnostics.