一键导入
leak-detection
Find and fix common memory leaks caused by listeners, closures, singletons, and lifecycle mismatches on iOS, Android, Flutter, and React Native.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Find and fix common memory leaks caused by listeners, closures, singletons, and lifecycle mismatches on iOS, Android, Flutter, and React Native.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Schedule, batch, and constrain background work with WorkManager, BGTaskScheduler, and push-over-poll patterns to minimize battery and data cost.
Attribute battery drain to CPU, radio, GPS, display, and background work using Android Battery Historian and Xcode Energy Log / Instruments Energy gauge.
Shrink APK/AAB and IPA size with R8/ProGuard, dead-code elimination, asset hygiene, and Android App Bundle / App Thinning best practices.
Cut local and CI build times on Android (Gradle config cache, build cache, KSP over KAPT) and iOS (Xcode build cache, ccache, modules). Use when clean builds exceed ten minutes.
Downsample, cache, and choose the right image format (AVIF, HEIC, WebP) to cut memory, network, and decode time on iOS and Android.
Capture and analyze heap dumps on Android (Android Studio Profiler, LeakCanary) and iOS (Instruments Allocations/Leaks). Use when memory-related crashes or growth are suspected.
| name | leak-detection |
| description | Find and fix common memory leaks caused by listeners, closures, singletons, and lifecycle mismatches on iOS, Android, Flutter, and React Native. |
Most mobile leaks are not exotic — they follow five or six patterns, and each has a standard fix. This skill catalogs the patterns and the lifecycle hooks where cleanup belongs.
Swift:
// BAD — retain cycle
manager.onEvent = { self.handle() }
// GOOD
manager.onEvent = { [weak self] in self?.handle() }
Kotlin (lambda capturing this implicitly):
// BAD — Flow.collect keeps ViewModel alive past scope
flow.collect { state = it }
// GOOD — use viewModelScope so cancellation happens on ViewModel.onCleared()
viewModelScope.launch { flow.collect { state = it } }
Dart:
// BAD — keeps State referenced even after dispose
StreamSubscription? sub;
sub = stream.listen((e) => setState(() => _value = e));
@override
void dispose() {
sub?.cancel(); // REQUIRED
super.dispose();
}
Android:
override fun onStart() {
super.onStart()
sensorManager.registerListener(this, sensor, SENSOR_DELAY_UI)
}
override fun onStop() {
sensorManager.unregisterListener(this) // REQUIRED
super.onStop()
}
iOS:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
observer = NotificationCenter.default.addObserver(...)
}
deinit {
if let o = observer { NotificationCenter.default.removeObserver(o) }
}
React Native:
useEffect(() => {
const sub = DeviceEventEmitter.addListener('evt', handle);
return () => sub.remove(); // REQUIRED
}, [handle]);
Any singleton that keeps a reference to an Activity, UIViewController, Context, or BuildContext leaks on configuration change or navigation.
Kotlin:
object Analytics {
// BAD — holds the first Activity forever
var context: Context? = null
}
Fix: store applicationContext, weak references, or pass context at call site.
Kotlin flows collected on GlobalScope or an unmanaged CoroutineScope outlive the screen:
// BAD
GlobalScope.launch { feed.collect { render(it) } }
// GOOD
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
feed.collect { render(it) }
}
}
Dart streams — always keep the StreamSubscription and cancel it in dispose(). Consider StreamSubscription fields or use useStream with hooks.
Compose remembers lambdas you pass to it. A lambda that captures a ViewModel keeps it alive until the composition leaves the composition tree — but if you pass it to a process-global object (e.g., a SharedFlow in a singleton), you leak.
SwiftUI: @StateObject owned view models survive view redraws and must be owned by the View that created them; do not store them in a singleton that re-hands them out.
ValueAnimator/ObjectAnimator in onStop.Timer in viewDidDisappear; call CADisplayLink.invalidate().AnimationController and Ticker.setInterval / cancel requestAnimationFrame handles.Uncapped caches keyed by object identity (URL, user id) grow forever. Use an LRU with a size limit. See caching-strategies skill.
val thumbnailCache = LruCache<String, Bitmap>(cacheSize = 32)
malloc_history for native-allocated leaks.leak_tracker package; run widget tests with expectLeak.Add an instrumentation test that navigates back and forth across every tab 5 times, then triggers a GC and asserts heap size is near baseline. LeakCanary has a CI output mode; iOS can use XCTMemoryMetric.
@Test
fun navigateBackAndForth_doesNotLeak() {
repeat(5) { onView(withId(R.id.tab_feed)).perform(click()); onView(withId(R.id.tab_profile)).perform(click()) }
LeakAssertions.assertNoLeaks()
}
weak self / use viewModelScope / cancel subscriptions in dispose.leak_tracker (Flutter) wired in debug.