Skip to content

fix(ios): break strong retain cycles in method-channel and custom-style callbacks#362

Open
hebaek76916 wants to merge 1 commit intonote11g:mainfrom
hebaek76916:fix/ios-retain-cycle
Open

fix(ios): break strong retain cycles in method-channel and custom-style callbacks#362
hebaek76916 wants to merge 1 commit intonote11g:mainfrom
hebaek76916:fix/ios-retain-cycle

Conversation

@hebaek76916
Copy link
Copy Markdown

Summary

Fixes a critical iOS memory leak where NaverMapView, NaverMapController, OverlayController, and the underlying NMFNaverMapView are never deallocated after a map screen is disposed. Apps that open/close the map repeatedly eventually hit the iOS memory high-watermark (~2 GB) and are killed by the OS.

Crash signature:

Root Cause

Four separate strong retain cycles caused by passing instance methods as closure arguments. In Swift, channel.setMethodCallHandler(handle) implicitly captures self strongly, creating cycles that ARC cannot break.

Cycle 1 — NaverMapController.init

channel.setMethodCallHandler(handle) → FlutterMethodChannel → closure → self

Cycle 2 — OverlayController.init

channel.setMethodCallHandler(handler) → same pattern

Cycle 3 — NaverMapControlSenderExtensions (the real killer)

loadHandler: onCustomStyleLoaded → stored inside NMFMapView → creates NMFNaverMapView ↔ NaverMapController cycle

Cycle 4 — NaverMapViewEventDelegate.registerDelegates

loadHandler: sender?.onCustomStyleLoaded → promotes optional, captures sender strongly

Fix

All four wrap callbacks in explicit { [weak self] in self?.method() } closures. Added deinit to NaverMapController and OverlayController to clear method-channel handlers. Added delegate-unregister + removeFromSuperview() in NaverMapView.deinit as safety net.

Verification

Tested with Xcode Memory Graph after 4+ map-screen push/pop cycles:

Before (3 visits):

  • NaverMapController (3), OverlayController (3), NMFNaverMapView (3), NOverlayInfo (2039)
  • Memory climbs to ~1.37 GB and never recovers

After (4 visits):

  • All counts stay at (1) — only singletons remain
  • Memory returns to baseline after each pop
  • All deinit methods fire on every pop

Checklist

  • Tested on iOS 17
  • Verified no regression in existing map/overlay functionality
  • Android behavior unchanged (no Android code modified)

cf)
image
image

After Fix

image

…le callbacks

Fix a critical iOS memory leak where NaverMapView, NaverMapController,
OverlayController, and the underlying NMFNaverMapView are never
deallocated after a map screen is disposed.

Root cause: Four separate strong retain cycles caused by passing
instance methods as closure arguments. Swift automatically converts
instance-method references to closures that capture `self` strongly.
When stored by FlutterMethodChannel or the NMFMapView SDK, these
create unbreakable retain cycles.

Fixes applied:
1. NaverMapController.init — wrap setMethodCallHandler with [weak self]
2. OverlayController.init — wrap setMethodCallHandler with [weak self]
3. NaverMapControlSenderExtensions — wrap custom style callbacks
4. NaverMapViewEventDelegate — wrap setCustomStyleId callbacks
5. NaverMapView.deinit — unregister delegates + removeFromSuperview

Verified with Xcode Memory Graph: all native objects are fully
deallocated after each map-screen pop. Memory returns to baseline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants