Skip to content

Release the map view and detach the controller when MapViewBase leaves - #12

Merged
wf9a5m75 merged 5 commits into
developfrom
fix/mapview-teardown
Nov 8, 2026
Merged

wf9a5m75 merged 5 commits into
developfrom
fix/mapview-teardown

Conversation

@wf9a5m75

@wf9a5m75 wf9a5m75 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

なぜ

MapViewBase は、コンポジションから外れるときに controller の destroy() を呼びますが、SDK の地図ビュー(MapView など)は誰も破棄していませんでした。多くの SDK では描画スレッドやリスナが地図ビューを持ち続け、そこからカメラのリスナ経由で古いコントローラとオーバーレイがすべて残ります。そのためプロバイダを切り替えるたびに前の地図がまるごとメモリに残り、区画データでは数回で OutOfMemoryError になっていました。

何を変えたか

  • MapViewBase に onDestroyMapView: ((ActualMapView) -> Unit)? を足しました。外れるときに、controller を破棄し、地図ビューを親から外して、プロバイダに 1 回だけ渡します。
    • 設定変更(回転など)でも呼びます。古い Activity のビューは二度と表示されず、回転後に引き継がれるのは保存した state です(カメラとデザインが保たれることを確認済み)。
    • 渡さないプロバイダには、黙って漏らす代わりにログで警告を出します。
  • MapViewLifecycleOwner を追加しました。ホストのライフサイクルに従いつつ、onDestroyMapView から単独で終わらせられます。lifecycleScope のコルーチン、observer、ComponentCallbacks などを「渡されたライフサイクルが終わるまで」持ち続ける SDK(ArcGIS / Mapbox)のために使います。
  • 破棄したコントローラは state から外します(core の MapViewState.detachController)。外すよう頼む相手は、controller を作った時点の state、最新の state、最初の state の 3 つです。どれも、その controller がまだ接続中のときだけ外れます。
    • example の ArcGIS 3D/2D の切り替えで、同じ MapViewBase に 1 フレームだけ別の state が渡り、片方に残っていたためです。
  • 外すときに VectorStyleMutationSupport も registry から取り除きます。Mapbox ではこれが holder とコントローラを持っていました。

互換性

onDestroyMapView の無い旧シグネチャの MapViewBase は削除しました。これに対してコンパイルされたコードは再ビルドが必要です(バイナリ互換の破壊。了承済み)。MapViewBase を直接呼ぶのはプロバイダだけで、この PR 一式で全プロバイダを新しいシグネチャに移しています。

関連 PR(同じ修正の一式)

fix/mapview-teardown は次のリポジトリにまたがります。core → compose → 各プロバイダの順にマージしてください。プロバイダの変更は、core の MapViewState.detachController と、compose の MapViewBase(onDestroyMapView = …) / MapViewLifecycleOwner を使います。

検証

タブレット(HA26Z6JL)の example-app で確認しました。区画データ(約 10 万ポリゴン)はポリゴンのタイル描画が要るので、検証には、このブランチのコミットを feat/polygon-tiling / feat/overlay-on-added の上に一時的に cherry-pick したブランチ(test/teardown-on-tiling、push していません)を使っています。

  • 地図の切り替え(Boston の City parcels、MapLibre ⇄ Google): 毎回 am dumpheap -g で GC させて、GC 後のヒープを測りました。
    • 修正前: 292 → 489 MB と増え続け、5 回目の切り替えで OutOfMemoryError(512 MB)で落ちました。ヒープダンプでは、切り替え後も古い MapLibreViewController / GoogleMapViewController が残り、それぞれ 98,843 件の PolygonEntity を抱えていました。
    • 修正後: 24 回切り替えても、GC 後のヒープは 307〜309 MB で一定でした。Native alloc も増えていません。
  • 全プロバイダの巡回(11 種類、City parcels): 巡回後のヒープダンプに残った地図コントローラは、表示中の 1 個だけでした。PolygonEntity も表示中の地図の 98,843 件だけです。FATAL は 0 件でした。
  • 回転(横 → 縦 → 横): map-design ページでデフォルト以外のデザインを選んでから回しました。Google / HERE / Mapbox / ArcGIS 3D / ArcGIS 2D / MapLibre / TomTom / MapTiler / Longdo で、デザインとカメラが保たれ、プロセスも落ちませんでした。
  • 単体テスト・apiCheck: develop を起点に 12 モジュールで testDebugUnitTest と apiCheck を流しました。新しく足したテストはすべて通っています。失敗は次の 5 つで、どれも develop の時点で同じ理由で落ちています。
    • apiCheck: arcgis / maplibre / mappls / openmobilemaps
    • openmobilemaps の conformance テスト 1 件

確認できていないもの:

  • Mappls の回転: 検証用のビルドでは Mappls の認証設定が無く、初期化に失敗していました。
  • OpenMobileMaps の回転時のデザイン保持: example のデザイン選択 UI が OMM の state に対応していません(カメラの保持は確認済み)。

MapViewBase destroyed the controller on dispose but nobody destroyed the
SDK's map view. Its render thread kept the view, and through the camera
listeners the old controller with every overlay, so each provider switch
left the previous map in memory until the app ran out of it.

MapViewBase now takes onDestroyMapView. On dispose it destroys the
controller, detaches the view and hands it to the provider once. This
also runs for a configuration change: the old Activity's view is never
shown again, and what survives a rotation is the saved state. A provider
that does not pass it gets a warning in the log instead of a silent leak.

The previous signature stays as a hidden overload so code compiled
against it still links (same JVM descriptor; the .api line differs only
in the erased generic signature).
Some SDKs keep what a map view holds -- lifecycleScope coroutines,
observers, component callbacks -- until the lifecycle they were given (or
found in the view tree) is destroyed. With the Activity's, a map the user
switched away from stays until the Activity goes. MapViewLifecycleOwner
follows the host lifecycle and can be ended on its own from
onDestroyMapView.
MapViewBase now hands the controller it destroyed back to the state
(MapViewState.detachController), so a state the app keeps no longer holds
the old map's controller and overlays. Done here for every provider; ArcGIS
did it on its own before.

The hidden overload without onDestroyMapView is removed: code compiled
against it has to be rebuilt (binary break, accepted).
…tions

A cycle through every provider (Boston parcels) still kept two old maps:

- ArcGIS 2D: the example hands the same MapViewBase a second state for a
  frame (ArcGIS 3D and 2D share a state type), the provider attached the
  controller to that one, and dispose detached it from the first. MapViewBase
  now remembers the state current when the controller was made and asks it,
  the latest and the first to let go (each only if it is still attached).
- Mapbox: the VectorStyleMutationSupport store left in the state's registry
  holds the map's holder and controller. MapViewBase removes it with the
  other two capabilities it already took down.
A change that adds API to core and uses it here is opened as a pair of
PRs on branches of the same name. CI always built core's default branch,
so the compose PR failed until core was merged (fix/mapview-teardown:
detachController(controller) not found). Use core's same-named branch
when it exists; otherwise the default branch, as before.
@wf9a5m75
wf9a5m75 merged commit 406bfb7 into develop Nov 8, 2026
2 checks passed
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.

1 participant