跪拜 Guibai
← All articles
Frontend

How a Feed App Survives: Two-Level Caching, Weak-Network Degradation, and OOM Defense

By 大龄秃头程序员 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Feed apps that ignore peak-memory coordination still crash under heavy scrolling even with a cache in place. The combination of cost-based LRU eviction, RunLoop-gated decoding, and aggressive cancellation on memory warning prevents the transient bitmap spikes that trigger iOS jetsam events, and the instrumentation makes those spikes visible for the first time.

Summary

Feed performance collapses when peak resource usage isn't controlled. This implementation layers a SQLite L1 for instant cold-start content, then overlays a two-level image cache: a cost-evicting LRU for decoded bitmaps and URLCache for raw response data. Decoding is deferred to RunLoop idle observers so scrolling never drops frames, and in-flight request merging eliminates duplicate network calls.

Weak-network handling goes beyond a toast alert. When connectivity drops, the system suppresses refreshes and pagination while continuing to serve cached posts, keeping the UI usable without pointless retries. The OOM strategy targets instantaneous memory spikes, not just total cache size, by canceling all in-flight loads, draining the idle-work queue, and disabling prefetch the moment a memory warning fires.

Every decision is instrumented: cache-hit rates, image-load failures, and memory-warning snapshots feed an observability loop that turns OOM debugging from guesswork into data-driven investigation. The result is a feed that loads fast, scrolls smoothly, degrades gracefully, and stops bleeding before the system kills it.

Takeaways
Cold-start content comes from a SQLite L1 cache, not the network, so the feed is never blank on launch.
Image decoding is deferred to a RunLoop idle observer; fast scrolling keeps the main thread busy, so images intentionally fill in only after the user stops.
An LRU cache evicts by estimated bitmap cost (bytesPerRow × height), not by image count, because a single large decoded image can outweigh dozens of thumbnails.
In-flight request merging ensures the same image URL triggers exactly one network call, with all completion handlers called back together.
Weak-network degradation blocks refreshes and pagination when offline but continues serving cached posts, avoiding error-spam and blank screens.
On memory warning, the guard clears the LRU, cancels every in-flight load, drains the idle-work queue, and disables prefetch to prevent an immediate memory rebound.
Three-layer cancellation—cell reuse, didEndDisplaying, and cancelPrefetching—stops off-screen cells from continuing downloads and decodes.
Key events (feedCacheHit, imageCacheHit, imageLoadFailure, memoryWarning) are tracked so OOM investigations have data instead of guesswork.
Conclusions

Most feed OOMs are not caused by total memory pressure but by a brief, synchronized spike when scrolling stops and a backlog of decodes all complete at once.

Delaying image decode until the RunLoop is idle is a deliberate trade-off that prioritizes frame rate over immediate image display, and users rarely notice the sub-second delay.

Clearing the image cache on memory warning is insufficient if in-flight downloads and queued decodes are not also cancelled; memory rebounds within seconds otherwise.

Request deduplication is as much a memory optimization as a network one—it prevents duplicate decoded bitmaps from entering the LRU simultaneously.

SQLite as a feed-data L1 turns offline mode from an error state into a normal, usable condition, which changes how weak-network logic is designed.

Observability events tied to cache hits and memory warnings turn OOM debugging from a crash-log guessing game into a measurable pipeline problem.

Concepts & terms
Cost-based LRU eviction
A cache eviction policy that tracks the memory cost of each entry (e.g., bitmap bytes) and removes the least-recently-used items when the total cost exceeds a limit, rather than evicting by item count.
RunLoop idle scheduling
A technique that registers a CFRunLoopObserver for the beforeWaiting activity to execute deferred work only when the main thread has no pending events, keeping the UI responsive during scrolling.
Image downsampling
Decoding an image file into a bitmap at a smaller target pixel size, dramatically reducing memory usage compared to decoding at the original resolution and then scaling the UIImage.
In-flight request merging
A concurrency pattern where multiple callers requesting the same resource share a single network task; all completions are stored and invoked when the single task finishes.
Jetsam / OOM
iOS's system-level process termination when memory pressure exceeds limits. Unlike a crash, jetsam events often leave no usable stack trace, making instrumentation critical for diagnosis.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗