Performance: Finding and Fixing Real Bottlenecks

Build a practical, measurement-driven understanding of Objective-C performance — CPU cost, memory footprint, allocation pressure, autorelease pools, collection complexity, string operations, JSON parsing, image processing, main-thread discipline, network latency, disk I/O, caching, batching, lazy loading, and reuse — tied together by the discipline of profiling with Instruments rather than guessing.

Performance is not one thing. A slow app might be burning CPU cycles on an inefficient algorithm, ballooning its memory footprint with objects nobody needs anymore, stalling the main thread on work that should have moved to a background queue, or simply waiting on a slow network connection that no amount of clever local code can speed up. Nearly every earlier lesson in this course already touched on one piece of this picture — autorelease pools, collection Big-O, NSMutableString, GCD, Core Data faulting, table view cell reuse, Instruments itself — without stopping to connect them into a single coherent view of 'what makes Objective-C code fast or slow, and how do you actually find out which one you have.' This lesson is that synthesis: a tour of the areas that most commonly cause real performance problems in real Objective-C apps, and, running through every one of them, the same underlying discipline — measure first, find the actual bottleneck, fix that specific thing, and measure again — that the earlier Instruments lesson taught and that this lesson ends by returning to explicitly.

CPU: algorithmic complexity is usually the root cause

CPU-bound slowness — an operation that simply takes too long to compute — is, in the overwhelming majority of real cases, not caused by Objective-C's message-dispatch overhead or any other language-level cost. It's caused by algorithmic complexity: doing more work than the problem actually requires, usually because of a nested loop, a repeated linear search where a hash lookup would do, or an operation that recomputes something it already computed a moment ago. A method that searches an NSArray for a matching element inside a loop that's already iterating that same array is doing O(n²) work for a problem that O(n) work, with the right data structure, would solve.

Beyond raw algorithmic shape, CPU time is also spent on expensive per-call work that's easy to under-price mentally: a regular expression compiled fresh on every call instead of once and reused, a date formatter created inside a hot loop instead of held as a property, a deeply nested KVC key path evaluated repeatedly instead of cached. None of these show up as 'bad algorithms' in the textbook sense, but they add up to the same symptom — real CPU time spent doing work that didn't need to be redone.

The practical takeaway is that CPU optimization almost never starts with micro-tuning individual lines of Objective-C — it starts with asking whether the overall shape of the computation is doing more work than necessary, and whether any of that work is being needlessly repeated. Instruments' Time Profiler, covered in the earlier Instruments lesson, exists specifically to answer that question with data rather than guesswork, which is why this lesson keeps returning to it rather than offering a checklist of micro-optimizations to apply blindly.

Memory, allocations, and object creation

Memory footprint is a genuinely separate concern from CPU time — code can run fast while still using far more memory than it needs to, and that excess footprint carries its own cost: more pressure on the system, a higher risk of the OS terminating the app under memory pressure, and, on the way there, real allocation overhead from creating and destroying all those objects in the first place. The earlier Memory Debugging lesson covered finding leaks and retain cycles with Instruments and the Memory Graph Debugger; this lesson's concern is different and complementary — not 'is memory being freed correctly,' but 'is more memory being allocated than the task actually requires, even when everything is freed correctly and on time.'

Allocating many short-lived objects, especially inside a hot loop, has a real, measurable cost that's easy to underestimate: every alloc/init pair does real work — heap allocation, zeroing memory, running an initializer — and Objective-C's reference counting adds its own bookkeeping overhead to every retain and release along the way. A loop that creates a new NSDictionary or a new NSString on every single iteration, when the same object could be created once outside the loop and reused or mutated in place, is paying that allocation cost thousands of times over for no benefit.

This is the direct motivation for reusing objects where it's sensible rather than reflexively creating a fresh one every time: an NSMutableArray or NSMutableDictionary that's cleared and refilled on each iteration of an outer loop, rather than freshly allocated each time, an NSDateFormatter held as a property and reused across many calls rather than instantiated per call, a fixed-size buffer reused across iterations rather than reallocated. None of this means avoiding object creation altogether — Objective-C's whole object model depends on it, and premature reuse in the name of performance is its own mistake — but it does mean noticing when a hot loop is paying allocation costs repeatedly for something that doesn't actually need to change identity every time through.

Autorelease pools and collection complexity

The earlier dedicated lesson on autorelease pools covered how they work; the performance-relevant consequence is what an inner @autoreleasepool does inside a tight loop that generates a lot of autoreleased objects — many Foundation convenience methods (stringWithFormat:, arrayWithObjects:, and similar) return autoreleased objects, and without an inner pool, every one of those objects accumulates for the entire lifetime of the outer pool, ballooning peak memory in exact proportion to how many loop iterations ran before the outer pool finally drained. Wrapping the loop body in its own @autoreleasepool block drains that memory on every iteration instead, trading a small amount of pool-management overhead for a dramatically lower peak memory footprint — a genuine, measurable performance technique, not just a memory-hygiene one.

Collection choice is a performance decision in exactly the same concrete sense, and the earlier lessons on NSArray, NSMutableArray, NSDictionary, and NSSet already laid out each type's Big-O characteristics — this lesson's point is that those characteristics are not academic trivia, they translate directly into real running time at scale. Looking up a value by key in an NSDictionary is O(1); finding whether a particular object is present in an NSArray is O(n), because it has to check elements one at a time. Code that repeatedly calls containsObject: on a large NSArray inside a loop, when an NSSet's O(1) membership test would do the identical job, is choosing a real, measurable performance penalty for no benefit beyond habit or unfamiliarity with the alternative.

This is why 'which collection type' deserves to be treated as a real design decision made up front, not a stylistic afterthought: an NSMutableArray used purely for fast membership testing is usually a sign the code should have reached for NSMutableSet instead, and an NSDictionary keyed for O(1) lookup where the code was previously doing a linear scan through an array of key-value pairs is often one of the single highest-leverage, lowest-risk performance fixes available in a codebase, because it changes the algorithm's fundamental complexity rather than merely shaving constant-factor overhead off an already-correct approach.

String operations and JSON parsing

The earlier lesson on NSMutableString already established its core performance advantage: building a string incrementally with appendString: or appendFormat: avoids the repeated allocation and copying that comes from concatenating immutable NSString instances one at a time in a loop, since each immutable concatenation typically allocates a brand-new string containing the combined characters, discarding the previous one. For a loop building a long string piece by piece, that difference compounds directly with the number of iterations, and it's one of the most reliable, easy-to-apply performance wins available for string-heavy code.

JSON parsing carries its own real cost that scales with payload size: NSJSONSerialization parses an entire JSON document into memory as a complete object graph before handing any of it back, which is straightforward and correct but means a very large payload pays its full parsing cost, and its full memory cost, up front, all at once, before the app can use even the first piece of data inside it. For payloads in the range typical apps deal with, this whole-document approach is entirely appropriate and not worth second-guessing.

For genuinely large JSON payloads — the kind where whole-document parsing shows up as a measured, meaningful bottleneck rather than a theoretical one — a streaming or incremental parser, which processes the document as a sequence of tokens and can begin exposing data before the whole payload has even arrived or finished parsing, can reduce both peak memory and time-to-first-usable-data. This is worth knowing exists, but it's a real trade-off with real added complexity, and reaching for it before NSJSONSerialization has actually been measured and shown to be the bottleneck is exactly the kind of premature optimization this lesson's closing section on profiling warns against.

Image processing and main-thread work

Decoding and resizing images is genuinely expensive work — decompressing a JPEG or PNG into raw pixel data, and then resampling that pixel data to a new size, both involve real, substantial computation over every pixel in the image, and a screen full of thumbnails, each requiring its own decode-and-resize pass, can easily add up to real, user-visible cost. This is one of the most common concrete sources of a sluggish, stuttering UI in apps that display a lot of images, and it is almost never something to accept as an unavoidable cost of doing business — it's almost always a candidate for moving off the thread that's responsible for keeping the UI responsive.

That leads directly to the umbrella principle tying this entire lesson together: anything expensive — image decoding, JSON parsing of a large payload, a computation with real algorithmic weight, disk I/O — needs to be kept off the main thread, because the main thread's responsiveness is exactly what determines whether the UI feels smooth or stutters. Every frame the system wants to draw has a fixed, small time budget; any main-thread work that runs long enough to blow through that budget shows up directly as a dropped frame, a UI freeze, or, in the worst case, the watchdog terminating the app for being unresponsive for too long.

The earlier lessons on Grand Central Dispatch and concurrency already taught the mechanism for solving this: dispatch the expensive work — the image decode, the JSON parse, the heavy computation — to a background queue, and dispatch only the final, cheap step of updating UI state back to the main queue once that work completes. This lesson's contribution is tying that mechanism explicitly to the broader principle it serves: main-thread discipline is not one isolated technique among many in this lesson, it's the organizing idea that several of this lesson's other topics — image processing, JSON parsing, even some caching strategies — ultimately exist to protect.

Network latency and disk I/O

Network latency is, in a meaningful sense, a different category of performance problem from everything else in this lesson, because it's largely irreducible: a round trip to a server has a floor set by the physical distance the data has to travel and the network conditions in between, and no amount of local code optimization changes that floor. In a genuinely network-bound scenario — where most of an operation's wall-clock time is spent waiting on a response, not computing anything locally — optimizing local Objective-C code is optimizing the part of the picture that was never the bottleneck in the first place.

What does move the needle in a network-bound scenario is reducing how much network round-tripping has to happen at all: fewer separate requests (batching several needs into one call rather than firing off five separate ones), smaller payloads (requesting only the fields actually needed rather than an entire object graph), and reusing connections rather than tearing down and re-establishing them repeatedly. None of this makes any single round trip faster, but it reduces how many round trips, and how much data per round trip, the irreducible latency has to be paid for.

Disk I/O sits in a similar category to network latency in that it's comparatively slow relative to in-memory work, though usually far faster and more predictable than a network round trip. Reading or writing a file involves real, measurable overhead beyond just moving bytes — filesystem lookups, and on some storage, genuine physical latency — and code that reads the same file repeatedly, or that writes many small updates to disk one at a time instead of batching them into fewer, larger writes, is paying that overhead far more often than the actual data volume requires. As with network calls, the fix is usually not making any one disk operation faster, but doing fewer of them.

Caching, batching, and lazy loading

Caching avoids redundant expensive work by storing the result of a computation, a parse, a network fetch, or a decode, and reusing that stored result the next time the same thing is needed, rather than redoing the original expensive work from scratch. The earlier Persistence lesson drew an important distinction directly relevant here: a cache and durable, must-not-lose storage are not the same category of data, even though both might live on disk — a cache is allowed to be evicted, cleared, or rebuilt at any time without real consequence beyond having to redo the work once, where durable data loss is a real problem for the user. Conflating the two — treating a cache as if it were durable storage, or treating durable storage as freely disposable the way a cache is — leads to real bugs in both directions.

Batching groups many small operations into fewer, larger ones, and this course has already taught two concrete, well-established examples of exactly this technique: wrapping several related SQLite statements inside a single transaction rather than committing each one individually, and Core Data's batch operations, which apply a change across many managed objects without the overhead of faulting each one into memory individually first. Both exist for the identical underlying reason — the fixed overhead of 'doing one operation' (a transaction commit, a fault, a round trip) is being paid once for many logical changes instead of once per change, and that difference compounds directly with how many operations are being batched together.

Lazy loading defers expensive work until it's actually needed, rather than doing it eagerly up front whether or not it turns out to be necessary. Core Data's fault mechanism, already taught earlier in this course, is a genuine, concrete example of exactly this: a managed object relationship is represented by a lightweight placeholder fault until the moment something actually accesses its properties, at which point Core Data transparently loads the real data — meaning an app that never actually touches a particular relationship never pays the cost of loading it at all, a saving that's entirely automatic and requires no explicit lazy-loading code from the app itself.

Reuse: the table view cell as the canonical example

Reuse, as a general performance principle, means recycling an existing, already-allocated object for a new logical purpose instead of allocating and configuring a brand-new one every time one is needed — and this course has already taught the single most canonical, most widely-encountered example of the principle in the entire Cocoa ecosystem: UITableView and UICollectionView cell reuse. Scrolling a table with thousands of rows does not allocate thousands of cell objects — it allocates only as many as can be visible on screen at once, plus a small buffer, and recycles that fixed, small pool of cells as rows scroll off one edge of the screen and new rows need to appear at the other, via dequeueReusableCellWithIdentifier:.

The performance payoff of that design is enormous and direct: without reuse, scrolling a long list would mean constantly allocating and deallocating cell objects — each one a real, non-trivial object graph including its own subviews, layout constraints, and possibly image views with decoded image data — at a rate tied directly to scroll speed, which would make smooth 60fps scrolling essentially unachievable for any list of meaningful length. With reuse, that entire allocation cost is paid once, up front, for a small, fixed number of cells, and every subsequent 'new' row appearing on screen is really just an existing cell being reconfigured with new data.

The general lesson generalizes well beyond table views: any place in an app where the same kind of object is being created and destroyed repeatedly, at a rate tied to some ongoing, repetitive user action, is a candidate for the same reuse strategy — a pool of reusable buffer objects, a pool of formatter objects, a pool of view objects recycled rather than reallocated — even outside of UIKit's own built-in reuse mechanisms.

Profiling: the discipline that ties everything together

Every technique in this lesson — reusing objects, choosing the right collection type, batching writes, moving work off the main thread, adding an inner autorelease pool — is a specific fix for a specific kind of bottleneck. None of them is universally worth applying everywhere, all the time, regardless of whether that specific bottleneck is actually present in the code being worked on. Applying performance techniques speculatively, based on intuition about what's probably slow rather than actual measurement of what is slow, routinely wastes effort on code that was never the problem while leaving the real bottleneck untouched.

This is exactly why the earlier Instruments lesson taught its loop as a discipline rather than a one-time checklist: measure first, to find out where time and memory are actually going, rather than guessing; find the actual bottleneck that the measurement points to, which is very often not the piece of code that intuition would have flagged; change only that specific thing, using whichever of this lesson's techniques actually fits the bottleneck that was found; and measure again, to confirm the change produced a real, verifiable improvement rather than just a plausible-sounding one.

That loop — Measure, Find, Change, Measure again — is the single idea this entire lesson has been building toward, and it's the right way to read everything above it: not as a checklist of optimizations to apply everywhere on principle, but as a catalogue of the specific fixes available once Instruments has actually pointed at a specific, measured bottleneck. CPU profiling points toward algorithmic or per-call fixes; the Allocations instrument points toward object-creation or reuse fixes; Time Profiler samples pointing at the main thread during a stutter point toward moving work to a background queue. The tool tells you which chapter of this lesson to open; it should never be skipped in favor of guessing which chapter looks right.

Code Examples

Converting the blocked-ID list to an NSSet once, before the loop, turns every membership check inside the loop from a linear scan into a constant-time hash lookup -- the exact Big-O difference the earlier NSSet lesson taught, applied to a realistic filtering scenario. For a large blockedIDs array checked against a large incomingMessages array, this changes the algorithm's overall complexity, not just its constant factor.

NSSet<NSString *> *blockedIDSet = [NSSet setWithArray:[self loadBlockedIDs]];

Without the inner @autoreleasepool, every decoded UIImage, resized thumbnail, and JPEG NSData produced across thousands of paths would accumulate for the entire method call before the outer pool finally drained, making peak memory scale with the size of imagePaths. The inner pool drains all of it every single iteration instead.

@autoreleasepool { ... } // every autoreleased object created this iteration drains right here

Common mistake: Forgetting the inner pool here is a common, easy-to-miss cause of a batch-processing method spiking memory and getting terminated under memory pressure.

The genuinely expensive work -- decoding and resizing -- runs on a background queue, keeping the main thread free to keep the UI responsive; only the trivial final step of handing the finished thumbnail back runs on the main queue, exactly the main-thread discipline this lesson treats as its organizing principle. This is the same dispatch_async-to-background / dispatch_async-to-main shape taught in the earlier GCD and Concurrency lessons, applied specifically to the image-processing cost described above.

Wrapping the whole batch of inserts in a single BEGIN/COMMIT pays SQLite's per-transaction overhead -- disk syncs and journal bookkeeping -- once for the entire batch instead of once per row, exactly the batching technique the earlier SQLite lesson taught, here applied to a realistic logging scenario.

sqlite3_exec(db, "BEGIN", NULL, NULL, NULL); // once, for the whole batch

Common mistake: Committing after every single row is one of the most common, highest-impact SQLite performance mistakes -- disk-sync overhead per transaction can dominate total time for a large batch.

// Slow: O(n) per lookup, O(n*m) overall for m lookups against an n-item array
NSArray<NSString *> *blockedIDs = [self loadBlockedIDs]; // large array
for (Message *message in incomingMessages) {
    if ([blockedIDs containsObject:message.senderID]) {
        continue; // skip blocked senders
    }
    [visibleMessages addObject:message];
}

// Fast: O(1) per lookup, O(m) overall
NSSet<NSString *> *blockedIDSet = [NSSet setWithArray:[self loadBlockedIDs]];
for (Message *message in incomingMessages) {
    if ([blockedIDSet containsObject:message.senderID]) {
        continue;
    }
    [visibleMessages addObject:message];
}
- (void)processLargeImageBatch:(NSArray<NSString *> *)imagePaths {
    for (NSString *path in imagePaths) {
        @autoreleasepool {
            UIImage *image = [UIImage imageWithContentsOfFile:path];
            UIImage *thumbnail = [self resizeImage:image toSize:CGSizeMake(100, 100)];
            NSData *jpegData = UIImageJPEGRepresentation(thumbnail, 0.8);
            [jpegData writeToFile:[self thumbnailPathForPath:path] atomically:YES];
            // every autoreleased object created this iteration drains right here
        }
    }
}
- (void)loadThumbnailForPath:(NSString *)path
                  completion:(void (^)(UIImage *thumbnail))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // expensive: decode + resize, off the main thread
        UIImage *fullImage = [UIImage imageWithContentsOfFile:path];
        UIImage *thumbnail = [self resizeImage:fullImage toSize:CGSizeMake(80, 80)];

        dispatch_async(dispatch_get_main_queue(), ^{
            // cheap: just hand the finished result back to the UI
            completion(thumbnail);
        });
    });
}
// Slow: one transaction commit per row
for (LogEntry *entry in pendingEntries) {
    sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
    [self insertLogEntry:entry intoDatabase:db];
    sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
}

// Fast: one transaction for the whole batch
sqlite3_exec(db, "BEGIN", NULL, NULL, NULL);
for (LogEntry *entry in pendingEntries) {
    [self insertLogEntry:entry intoDatabase:db];
}
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);

Measure, Find, Change, Measure again

Every individual technique in this lesson -- an inner autorelease pool, an NSSet instead of an NSArray, a batched SQLite transaction, a background-dispatched image decode -- is a real, valid fix for a real, specific kind of bottleneck. None of them is a fix worth applying on faith. The discipline the earlier Instruments lesson taught -- measure first to see where time and memory are actually going, find the specific bottleneck the measurement reveals, change only that one thing using whichever technique actually fits it, and measure again to confirm the change genuinely helped -- is what turns this lesson's catalogue of techniques from a list of plausible-sounding tricks into a reliable way to make an app measurably faster. Performance work that skips straight to changing code, on intuition about what's probably slow, routinely gets the target wrong; performance work anchored in that four-step loop rarely does.

Optimizing local Objective-C code inside a method whose actual wall-clock time is dominated by a network round trip.

In a network-bound operation, focus on reducing request count and payload size instead -- measure with Instruments or network logging first to confirm where the time is actually going before optimizing anything local.

Network latency is largely irreducible from local code; time spent shaving milliseconds off local computation that was never the bottleneck produces no measurable improvement.

Allocating a brand-new NSDateFormatter (or a fresh compiled NSRegularExpression) on every call inside a hot loop.

Create the formatter or regular expression once, hold it as a property or a static, and reuse the same instance across every call.

Both are genuinely expensive to construct; recreating either one per call inside a loop that runs many times adds real, easily avoidable CPU cost.

Decoding and resizing images directly on the main thread inside a table view's cellForRowAtIndexPath:.

Dispatch the decode/resize work to a background queue and update the cell's image view back on the main queue once it completes, guarding against the cell having been reused for a different row by the time the work finishes.

Main-thread image processing is one of the most common concrete causes of scroll stutter in image-heavy lists, and it's avoidable with the standard background-dispatch pattern this course already taught.

Applying every performance technique in this lesson speculatively across a codebase because it 'seems like it should help,' without measuring first.

Profile with Instruments to find the actual, measured bottleneck, apply the one technique that fits it, and measure again to confirm the fix actually helped before moving on.

Un-measured optimization routinely spends effort on code that was never slow while leaving the real bottleneck, which intuition rarely predicts correctly, completely untouched.

What is usually the actual root cause of CPU-bound slowness in real Objective-C code, according to this lesson? A. Objective-C's message-dispatch overhead B. Algorithmic complexity -- doing more work than the problem requires, or needlessly repeating expensive work C. Using @property instead of manual ivars D. ARC's automatic retain/release calls

B. Algorithmic complexity -- doing more work than the problem requires, or needlessly repeating expensive work — The lesson identifies algorithmic shape and needlessly repeated expensive per-call work as the dominant real-world cause of CPU-bound slowness, not language-level dispatch overhead.

What does wrapping the body of a tight loop in an inner @autoreleasepool primarily accomplish? A. It makes the loop run on a background thread automatically B. It drains autoreleased objects created during each iteration immediately, capping peak memory instead of letting them accumulate for the outer pool's lifetime C. It converts the objects created inside the loop to be managed with manual retain/release instead of ARC D. It prevents the loop from creating any autoreleased objects at all

B. It drains autoreleased objects created during each iteration immediately, capping peak memory instead of letting them accumulate for the outer pool's lifetime — Without the inner pool, autoreleased objects accumulate until the outer pool eventually drains; the inner pool drains them every iteration, keeping peak memory bounded regardless of loop length.

Which earlier-taught Foundation mechanism does this lesson cite as a real, automatic example of lazy loading? A. NSOperation's state machine B. Core Data's fault mechanism C. UITableView cell reuse D. NSMutableString's incremental append

B. Core Data's fault mechanism — Core Data represents an unaccessed relationship as a lightweight fault and only loads the real data the first time something actually touches it -- automatic lazy loading.

Why is network latency treated as a different category of performance problem from the rest of this lesson's topics? A. Because it can always be eliminated with better local Objective-C code B. Because it's largely irreducible -- the fix in a network-bound scenario is reducing request count and payload size, not optimizing local computation C. Because NSURLSession automatically compensates for it D. Because it only matters on cellular connections, not Wi-Fi

B. Because it's largely irreducible -- the fix in a network-bound scenario is reducing request count and payload size, not optimizing local computation — A round trip's latency floor is set by distance and network conditions, not local code; in a network-bound scenario, minimizing round trips and payload size is what actually helps.

What is UITableView/UICollectionView cell reuse presented as the canonical example of in this lesson? A. Lazy loading B. Caching C. The general reuse principle -- recycling existing objects instead of constantly allocating and destroying new ones D. Batching

C. The general reuse principle -- recycling existing objects instead of constantly allocating and destroying new ones — Cell reuse recycles a small, fixed pool of cell objects across an arbitrarily long scrolling list, the canonical concrete example of the general reuse performance principle.

What four-step loop does this lesson's closing section identify as the discipline tying all of its individual techniques together? A. Refactor, Test, Deploy, Repeat B. Guess, Fix, Ship, Monitor C. Measure, Find the bottleneck, Change, Measure again D. Profile once at the start of the project, then optimize freely from memory

C. Measure, Find the bottleneck, Change, Measure again — The lesson explicitly ties every technique back to the Measure -> Find -> Change -> Measure again loop from the earlier Instruments lesson, treating measurement as the discipline that should drive which technique gets applied.

A photo-gallery app's main thread hangs briefly every time the user scrolls to a new row of thumbnails. Using this lesson's topics, list the sequence of steps you'd take to diagnose and fix it -- not just the final answer, but the order in which you'd investigate.

First, profile with Instruments' Time Profiler while reproducing the stutter, to confirm where main-thread time is actually going rather than guessing. If the samples point at image decode/resize calls running on the main thread inside cellForRowAtIndexPath:, move that work to a background queue via dispatch_async, dispatching only the final image-view update back to the main queue -- guarding against the cell being reused for a different row before the background work finishes. If cell reuse itself isn't already being used (fresh cells being allocated instead of dequeued), fix that first, since it would independently explain allocation-related main-thread cost. Finally, re-profile with Time Profiler under the same scroll scenario to confirm the hang is actually gone before considering it fixed.

Start with the profiling discipline from the closing section before jumping to a specific fix -- which Instruments tool would actually show you where the hang is coming from?