Common Memory Leaks in Wiki Apps: Causes and Fixes
Wiki apps accumulate memory pressure differently than typical CRUD applications. The core problem: unbounded object graphs tied to navigation history and content rendering. Three architectural pattern
What Causes Memory Leaks in Wiki Apps
Wiki apps accumulate memory pressure differently than typical CRUD applications. The core problem: unbounded object graphs tied to navigation history and content rendering. Three architectural patterns create the majority of leaks:
1. Fragment/Activity retention via static references — Wiki apps often cache rendered HTML, parsed wikitext ASTs, or image bitmaps in singleton ContentCache or ImageLoader instances. When a Fragment holding a WebView or RecyclerView gets destroyed but the cache retains a reference to its Context or View, the entire view hierarchy leaks.
2. Listener/observer registration without cleanup — Real-time collaboration features (WebSocket listeners, Firebase ValueEventListener, RxJava Disposable) frequently outlive their UI scope. A PageEditFragment registering a TextWatcher on a shared DocumentModel but never unregistering on onDestroyView() keeps the fragment alive.
3. WebView JavaScript bridge leaks — Wiki apps rendering content via WebView with @JavascriptInterface callbacks create cross-language reference cycles. The WebView holds a reference to the Java object; the Java object holds a closure referencing the Activity. Neither GC can collect the cycle.
4. Image pipeline bitmap pooling misuse — Glide/Coil/Fresco bitmap pools are designed for reuse, but wiki apps loading high-resolution diagrams (PlantUML, Mermaid, math formulas) often disable pooling or set inBitmap constraints incorrectly, causing native memory bloat that doesn't show in Java heap.
---
Real-World Impact
| Metric | Typical Wiki App Baseline | With Unfixed Leaks |
|---|---|---|
| Crash-free sessions (Play Console) | 99.2% | 94–96% (OOM crashes) |
| Avg. session length | 12 min | 4–6 min (app killed by LMK) |
| 1-star "crash" reviews / month | 2–3 | 15–40 |
| DAU retention (day 7) | 38% | 22% |
| Support tickets / 1k users | 3 | 18 |
Users don't report "memory leak." They report: "App closes when I open 5 pages," "Scrolling lags after 10 minutes," "Phone gets hot reading long articles." On Play Store, these become "keeps crashing" 1-star reviews. For subscription-based wikis (Notion-style, Confluence Cloud mobile), each 1% retention drop = ~$12k–$45k MRR depending on tier.
---
5–7 Specific Manifestations in Wiki Apps
1. WebView History Stack Retention
Symptom: Opening 20+ articles via internal links → OOM on 21st.
Root cause: WebView maintains full forward/back history with rendered DOM snapshots. clearHistory() doesn't release native memory.
Detection: adb shell dumpsys meminfo shows dalvik-webview native heap growing 15–30 MB per navigation.
2. ImageSpan / DynamicDrawableSpan Leak in TextView
Symptom: Long articles with inline images (formulas, emoji, diagrams) cause lag after scrolling.
Root cause: SpannableString holding ImageSpan referencing Drawable → Bitmap → Context. TextView recycling in RecyclerView doesn't clear spans.
Detection: LeakCanary shows TextView → SpannableString → ImageSpan → BitmapDrawable → Context chain.
3. Wikitext Parser AST Cache Leak
Symptom: Editing 5+ pages sequentially → ANR on 6th edit.
Root cause: ParserCache (LRU map) stores DocumentNode ASTs keyed by page ID. Nodes hold references to ParseContext → ProjectSettings → Application. Cache eviction policy misses WeakReference cleanup.
Detection: Heap dump shows 500+ DocumentNode instances retained by static ParserCache.INSTANCE.
4. Real-time Sync Disposable Leak
Symptom: Collaborative editing session > 15 min → UI freezes, battery drain.
Root cause: PageCollabManager.subscribeToChanges() returns Disposable stored in ViewModel. ViewModel cleared but Disposable not disposed because onCleared() not called (process death).
Detection: adb shell dumpsys activity broadcasts shows CompositeDisposable size growing per session.
5. Offline-First Queue Retention
Symptom: App killed after background sync of 200+ pending edits.
Root cause: WorkManager ListenableWorker holds List with full wikitext content. setForegroundAsync() keeps worker alive; List never cleared after Result.success().
Detection: WorkManager diagnostics show WORKER_RUNNING > 30 min with 200 MB heap.
6. Mermaid/PlantUML Renderer Native Leak
Symptom: Opening 10 diagram-heavy pages → crash in libskia.so.
Root cause: Skia/Chromium renderer called via JNI for SVG output. PictureRecorder/Canvas not close()'d after toPicture(). Native memory untracked by JVM GC.
Detection: Debug.getNativeHeapAllocatedSize() spikes 80 MB per diagram; not visible in hprof.
7. Search Index TokenStream Leak
Symptom: Full-text search slows down after 50 queries; heap grows 200 MB.
Root cause: Lucene TokenStream / TokenFilter chain not close()'d after IndexSearcher.search(). CachingTokenFilter holds AttributeSource → CharTermAttribute → char[] buffers.
Detection: jcmd + MAT shows CharTermAttributeImpl dominating retained set.
---
How to Detect Memory Leaks
Automated CI Detection
# .github/workflows/memory-leak-check.yml
- name: Run monkey test with leak detection
run: |
python -m susatest.agent \
--apk app/build/outputs/apk/debug/app-debug.apk \
--personas curious,power_user,impatient \
--duration 600 \
--leak-detection enabled \
--output junit:leak-results.xml
SUSA's impatient persona rapidly navigates back/forward 200+ times; curious opens every link in a 500-page wiki. Leak detection hooks into ActivityLifecycleCallbacks + LeakCanary ObjectWatcher to assert zero retained Activity/Fragment instances post-navigation.
Local Tooling Stack
| Tool | Purpose | Wiki-Specific Config |
|---|---|---|
| LeakCanary 2.12+ | Auto-detect retained View/Context | LeakAssertions.assertNoLeaks() in onDestroy() of PageFragment |
| Android Studio Profiler | Native + Java heap | Record "Open 30 articles → back to home" flow; filter dalvik-webview |
Perfetto / adb shell perfetto | System-wide memory pressure | Trace lmk_kill events correlated with WebView native heap |
| MAT (Eclipse Memory Analyzer) | Heap dump dominator tree | OQL: SELECT * FROM instanceof android.webkit.WebView WHERE retainedHeap > 10M |
| SUSA CLI | Autonomous exploration | susatest explore --apk app.apk --personas adversarial,accessibility --max-steps 500 --detect-leaks |
What to Look For
Activity/Fragmentcount > 1 afteronDestroy()in LeakCanary summary- Native heap > 150 MB sustained (wiki apps often hit 300+ MB with diagrams)
WebViewmNativeClassgrowing per navigation (checkWebView.getWebViewClient().getClass())Bitmapcount inBitmapPoolnot decreasing afterRecyclerViewscrollDisposable/Subscriptioncount inCompositeDisposable> number of active screens
---
How to Fix Each Example
1. WebView History Stack
// PageWebViewClient.kt
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
// Clear forward history; keep only last 3 for back navigation
if (view.copyBackForwardList().getCurrentIndex() > 3) {
val db = view.getDatabase() // internal API via reflection
db?.execSQL("DELETE FROM history WHERE id < ?",
arrayOf(view.copyBackForwardList().getItemAtIndex(0).id))
}
}
// In Fragment.onDestroyView()
webView.destroy() // Critical: releases native renderer
webView = null
Alternative: Use ChromeCustomTabs for external links; reserve WebView only for editable preview.
2. ImageSpan Leak
// WikiTextView.kt
fun setWikiSpans(spannable: Spannable) {
// Clear existing spans BEFORE setting new ones
val existingSpans = spannable.getSpans(0, spannable.length, ImageSpan::class.java)
existingSpans.forEach { span ->
(span.drawable as? BitmapDrawable)?.bitmap?.recycle()
spannable.removeSpan(span)
}
// Use Weak
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free