@onekeyfe/react-native-chart-webview 3.0.54 → 3.0.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ChartWebview.podspec +3 -0
- package/android/src/main/java/com/margelo/nitro/chartwebview/ChartWebview.kt +74 -16
- package/android/src/main/java/com/margelo/nitro/chartwebview/PooledChartWebView.kt +139 -13
- package/ios/ChartWebview.swift +185 -31
- package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +9 -0
- package/lib/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +2 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +4 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +6 -0
- package/lib/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +7 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +5 -0
- package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +1 -0
- package/lib/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +24 -0
- package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +2 -0
- package/lib/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +2 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +12 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +1 -0
- package/lib/nitrogen/generated/shared/json/ChartWebviewConfig.json +1 -0
- package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +9 -0
- package/lib/nitrogen/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +2 -0
- package/lib/nitrogen/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +4 -0
- package/lib/nitrogen/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +6 -0
- package/lib/nitrogen/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +7 -0
- package/lib/nitrogen/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +5 -0
- package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +1 -0
- package/lib/nitrogen/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +24 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +2 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +2 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +12 -0
- package/lib/nitrogen/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +1 -0
- package/lib/nitrogen/nitrogen/generated/shared/json/ChartWebviewConfig.json +1 -0
- package/lib/typescript/src/ChartWebview.nitro.d.ts +1 -0
- package/lib/typescript/src/ChartWebview.nitro.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridChartWebviewSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/views/JHybridChartWebviewStateUpdater.cpp +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/chartwebview/HybridChartWebviewSpec.kt +6 -0
- package/nitrogen/generated/ios/c++/HybridChartWebviewSpecSwift.hpp +7 -0
- package/nitrogen/generated/ios/c++/views/HybridChartWebviewComponent.mm +5 -0
- package/nitrogen/generated/ios/swift/HybridChartWebviewSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridChartWebviewSpec_cxx.swift +24 -0
- package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridChartWebviewSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.cpp +12 -0
- package/nitrogen/generated/shared/c++/views/HybridChartWebviewComponent.hpp +1 -0
- package/nitrogen/generated/shared/json/ChartWebviewConfig.json +1 -0
- package/package.json +1 -1
- package/src/ChartWebview.nitro.ts +10 -0
package/ChartWebview.podspec
CHANGED
|
@@ -21,6 +21,9 @@ Pod::Spec.new do |s|
|
|
|
21
21
|
|
|
22
22
|
s.dependency 'React-jsi'
|
|
23
23
|
s.dependency 'React-callinvoker'
|
|
24
|
+
# DEBUG instrumentation: route native chart lifecycle logs into the shared
|
|
25
|
+
# OneKeyLog file (app-latest.log) via `import ReactNativeNativeLogger`.
|
|
26
|
+
s.dependency 'ReactNativeNativeLogger'
|
|
24
27
|
|
|
25
28
|
load 'nitrogen/generated/ios/ChartWebview+autolinking.rb'
|
|
26
29
|
add_nitrogen_files(s)
|
|
@@ -98,34 +98,37 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
98
98
|
|
|
99
99
|
// --- Source props ---
|
|
100
100
|
|
|
101
|
+
// Source/bridge setters call applySource (synchronous) — NOT scheduleReconcile,
|
|
102
|
+
// which caused an infinite reconcile loop (reconcile -> setSource -> prop
|
|
103
|
+
// re-apply -> reconcile ...). applySource sets warmDriver + warm-boots the page
|
|
104
|
+
// even when this host is not the visible owner, so post-attach bridge arrival
|
|
105
|
+
// still makes this host the warm driver (page->native callbacks fall back to it).
|
|
101
106
|
private var _uri: String? = null
|
|
102
107
|
override var uri: String?
|
|
103
108
|
get() = _uri
|
|
104
|
-
set(value) { _uri = value;
|
|
109
|
+
set(value) { _uri = value; applySource() }
|
|
105
110
|
|
|
106
111
|
private var _localBundle: String? = null
|
|
107
112
|
override var localBundle: String?
|
|
108
113
|
get() = _localBundle
|
|
109
|
-
set(value) { _localBundle = value;
|
|
114
|
+
set(value) { _localBundle = value; applySource() }
|
|
110
115
|
|
|
111
116
|
private var _entry: String? = null
|
|
112
117
|
override var entry: String?
|
|
113
118
|
get() = _entry
|
|
114
|
-
set(value) { _entry = value;
|
|
119
|
+
set(value) { _entry = value; applySource() }
|
|
115
120
|
|
|
116
121
|
private var _paramsJson: String? = null
|
|
117
122
|
override var paramsJson: String?
|
|
118
123
|
get() = _paramsJson
|
|
119
|
-
set(value) { _paramsJson = value;
|
|
124
|
+
set(value) { _paramsJson = value; applySource() }
|
|
120
125
|
|
|
121
126
|
// Document-start bridge JS (single source of truth in the TS layer). Stored and
|
|
122
127
|
// handed to the pooled WebView when we claim it, before its first load.
|
|
123
128
|
private var _bridgeScript: String? = null
|
|
124
129
|
override var bridgeScript: String?
|
|
125
130
|
get() = _bridgeScript
|
|
126
|
-
|
|
127
|
-
// run before the bridge is registered, so setSource deferred), this triggers it.
|
|
128
|
-
set(value) { _bridgeScript = value; applySourceIfOwner() }
|
|
131
|
+
set(value) { _bridgeScript = value; applySource() }
|
|
129
132
|
|
|
130
133
|
// --- Singleton props ---
|
|
131
134
|
// `pooled` + non-empty `reuseKey` => the backing WebView is shared (keyed by
|
|
@@ -162,6 +165,22 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
162
165
|
scheduleReconcile()
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
// --- Debugging ---
|
|
169
|
+
// Make the backing WebView inspectable via chrome://inspect. Driven by the
|
|
170
|
+
// app's "Enable Native Webview Debugging" dev-mode toggle, mirroring the main
|
|
171
|
+
// react-native-webview (which calls WebView.setWebContentsDebuggingEnabled).
|
|
172
|
+
// Stored and applied to the backing WebView both here (on prop change) and at
|
|
173
|
+
// host claim, since `backing` is null until the first reconcile assigns it.
|
|
174
|
+
// CAVEAT: setWebContentsDebuggingEnabled is PROCESS-GLOBAL — see
|
|
175
|
+
// PooledChartWebView.setInspectable.
|
|
176
|
+
private var _webviewDebuggingEnabled: Boolean? = null
|
|
177
|
+
override var webviewDebuggingEnabled: Boolean?
|
|
178
|
+
get() = _webviewDebuggingEnabled
|
|
179
|
+
set(value) {
|
|
180
|
+
_webviewDebuggingEnabled = value
|
|
181
|
+
backing?.setInspectable(value)
|
|
182
|
+
}
|
|
183
|
+
|
|
165
184
|
// --- Event callbacks ---
|
|
166
185
|
override var onMessage: ((message: String) -> Unit)? = null
|
|
167
186
|
override var onLoadEnd: (() -> Unit)? = null
|
|
@@ -219,6 +238,9 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
219
238
|
val key = effectiveKey()
|
|
220
239
|
val entry = ChartWebviewPool.acquireShared(key, context)
|
|
221
240
|
backing = entry
|
|
241
|
+
// Apply this host's debug preference to the (possibly freshly created) backing
|
|
242
|
+
// WebView. Mirrors the main react-native-webview's setWebContentsDebuggingEnabled.
|
|
243
|
+
entry.setInspectable(_webviewDebuggingEnabled)
|
|
222
244
|
// Refcount the pool entry once per host (reconcile runs many times). Balanced
|
|
223
245
|
// by releaseShared in dispose(). If the reuseKey changed, release the old one.
|
|
224
246
|
if (adoptedPoolKey != key) {
|
|
@@ -226,13 +248,24 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
226
248
|
ChartWebviewPool.adopt(key)
|
|
227
249
|
adoptedPoolKey = key
|
|
228
250
|
}
|
|
251
|
+
// WARM-BOOT (mirror of iOS): boot the shared offline page + register as the
|
|
252
|
+
// warm DRIVER as soon as ANY referencing host has the bridge, even when not
|
|
253
|
+
// the visible owner (offscreen prewarm runs with attached=false/active=false).
|
|
254
|
+
// page->native callbacks fall back to warmDriver when there's no owner, so the
|
|
255
|
+
// bars-state / load-end signals aren't dropped during warm — otherwise the
|
|
256
|
+
// chart stays on the loading mask. Idempotent: setSource dedupes on same URL,
|
|
257
|
+
// setBridgeScript no-ops once registered.
|
|
258
|
+
val bs = _bridgeScript
|
|
259
|
+
if (!bs.isNullOrEmpty()) {
|
|
260
|
+
entry.setBridgeScript(bs)
|
|
261
|
+
entry.warmDriver = this
|
|
262
|
+
entry.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
263
|
+
}
|
|
229
264
|
if (wantsOwnership()) {
|
|
230
265
|
entry.owner = this
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
entry.setBridgeScript(_bridgeScript ?: "")
|
|
266
|
+
// PERF: a host is now showing the chart — make sure the renderer is running.
|
|
267
|
+
entry.resume()
|
|
234
268
|
entry.attachTo(container)
|
|
235
|
-
entry.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
236
269
|
// Keep a fresh frame of OUR content while we own the WebView, so that when
|
|
237
270
|
// we later go inactive (and the shared WebView reloads the other slot's
|
|
238
271
|
// chart) our slot freezes to its own last frame, not the other content.
|
|
@@ -248,6 +281,11 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
248
281
|
if (wasOwner) entry.owner = null
|
|
249
282
|
stopOwnCapture()
|
|
250
283
|
showPlaceholder(ownSnapshot)
|
|
284
|
+
// PERF: if nobody owns the shared WebView now, pause its renderer so it
|
|
285
|
+
// doesn't keep burning a CPU core + GPU + RAM in the background (Android
|
|
286
|
+
// doesn't auto-throttle offscreen WebViews like iOS does). Resumed on the
|
|
287
|
+
// next CLAIM. No-op until the page's first load completed.
|
|
288
|
+
entry.pauseIfIdle()
|
|
251
289
|
}
|
|
252
290
|
}
|
|
253
291
|
|
|
@@ -257,17 +295,28 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
257
295
|
if (!wantsOwnership()) return
|
|
258
296
|
val pooled = backing ?: PooledChartWebView.create(context, effectiveKey())
|
|
259
297
|
backing = pooled
|
|
298
|
+
pooled.setInspectable(_webviewDebuggingEnabled)
|
|
260
299
|
pooled.owner = this
|
|
261
300
|
pooled.setBridgeScript(_bridgeScript ?: "")
|
|
262
301
|
pooled.attachTo(container)
|
|
263
302
|
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
264
303
|
}
|
|
265
304
|
|
|
266
|
-
|
|
305
|
+
// Apply the source synchronously when a source/bridge prop changes. For a
|
|
306
|
+
// pooled host this ALSO registers it as the warmDriver and warm-boots the page
|
|
307
|
+
// even when it is not the visible owner, so page->native callbacks (bars-state /
|
|
308
|
+
// load-end) fall back to it when no host owns the pool. No scheduleReconcile —
|
|
309
|
+
// that looped. backing is null until the first reconcile assigns it (the
|
|
310
|
+
// reuseKey/pooled/active setters still scheduleReconcile), so warmDriver is set
|
|
311
|
+
// on the first prop change after the pool entry is acquired.
|
|
312
|
+
private fun applySource() {
|
|
267
313
|
val pooled = backing ?: return
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
314
|
+
val bs = _bridgeScript
|
|
315
|
+
if (isPooled() && !bs.isNullOrEmpty()) {
|
|
316
|
+
pooled.setBridgeScript(bs)
|
|
317
|
+
pooled.warmDriver = this
|
|
318
|
+
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
319
|
+
} else if (pooled.owner == this) {
|
|
271
320
|
pooled.setBridgeScript(_bridgeScript ?: "")
|
|
272
321
|
pooled.setSource(_uri, _localBundle, _entry, _paramsJson)
|
|
273
322
|
}
|
|
@@ -359,7 +408,7 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
359
408
|
// WebView so it doesn't leak a Chromium renderer + JavascriptInterface per
|
|
360
409
|
// mount/unmount. Pooled (shared) backing is intentionally left alive in the
|
|
361
410
|
// warm pool — other hosts may still share it; its lifetime is the pool's.
|
|
362
|
-
fun dispose() {
|
|
411
|
+
override fun dispose() {
|
|
363
412
|
stopOwnCapture()
|
|
364
413
|
container.removeCallbacks(reconcileRunnable)
|
|
365
414
|
container.removeCallbacks(revealFallbackRunnable)
|
|
@@ -368,10 +417,19 @@ class HybridChartWebview(val context: ThemedReactContext) : HybridChartWebviewSp
|
|
|
368
417
|
if (pendingRevealHost == this) pendingRevealHost = null
|
|
369
418
|
val entry = backing
|
|
370
419
|
if (entry != null && entry.owner == this) entry.owner = null
|
|
420
|
+
if (entry != null && entry.warmDriver == this) entry.warmDriver = null
|
|
371
421
|
// Balance the pool adopt() if this host ever joined a pooled key. Kept warm by
|
|
372
422
|
// default (single-instance cache); the release path makes destroy() reachable.
|
|
373
423
|
adoptedPoolKey?.let { ChartWebviewPool.releaseShared(it) }
|
|
374
424
|
adoptedPoolKey = null
|
|
425
|
+
// Pooled host going away: release the shared WebView from THIS host's
|
|
426
|
+
// container, which is about to be dropped from the view tree. The pool keeps
|
|
427
|
+
// the WebView alive (warm) for the next host, but if we leave it parented to
|
|
428
|
+
// our dropped container the next host's attach can't clear that stale parent
|
|
429
|
+
// (removeView on a detached container isn't applied synchronously) and retries
|
|
430
|
+
// forever → blank/white chart. detachFrom is parent-checked, so if another host
|
|
431
|
+
// already re-claimed the WebView we leave it where it is.
|
|
432
|
+
if (entry != null && isPooled()) entry.detachFrom(container)
|
|
375
433
|
// A private (non-pooled) instance is owned solely by this host — tear it down
|
|
376
434
|
// so it doesn't leak a Chromium renderer + JavascriptInterface.
|
|
377
435
|
if (entry != null && !isPooled()) entry.destroy()
|
|
@@ -52,6 +52,9 @@ class PooledChartWebView private constructor(
|
|
|
52
52
|
const val ASSET_HOST = "appassets.androidplatform.net"
|
|
53
53
|
private const val DEFAULT_ENTRY = "index.html"
|
|
54
54
|
|
|
55
|
+
// Operational log tag for this pool (lifecycle + error paths only).
|
|
56
|
+
private const val TAG = "ChartWebviewPool"
|
|
57
|
+
|
|
55
58
|
// Tiny platform transport shim: defines the single hook the shared TS bridge
|
|
56
59
|
// (CHART_BRIDGE_JS) calls. The bulk of the bridge lives in the TS layer and
|
|
57
60
|
// arrives via the `bridgeScript` ctor arg — this is the only platform-specific
|
|
@@ -148,7 +151,63 @@ class PooledChartWebView private constructor(
|
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
/** The host currently displaying this WebView; page events route here. */
|
|
151
|
-
var
|
|
154
|
+
private var _owner: HybridChartWebview? = null
|
|
155
|
+
var owner: HybridChartWebview?
|
|
156
|
+
get() = _owner
|
|
157
|
+
set(value) {
|
|
158
|
+
_owner = value
|
|
159
|
+
}
|
|
160
|
+
// The host that warm-booted the page and can drive its symbol / receive its
|
|
161
|
+
// callbacks while there is no VISIBLE owner yet. Separate from `owner` (the
|
|
162
|
+
// YIELD path clears `owner`); callbacks fall back to this so bars-state /
|
|
163
|
+
// load-end aren't dropped during warm. Mirror of the iOS warmDriver.
|
|
164
|
+
private var _warmDriver: HybridChartWebview? = null
|
|
165
|
+
var warmDriver: HybridChartWebview?
|
|
166
|
+
get() = _warmDriver
|
|
167
|
+
set(value) {
|
|
168
|
+
_warmDriver = value
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// PERF (Android only): Android's in-process WebView/Chromium does NOT throttle
|
|
172
|
+
// an offscreen/unowned page (unlike iOS WKWebView). Left running, the warm
|
|
173
|
+
// pooled page keeps its rAF render loop + websockets + compositing alive
|
|
174
|
+
// FOREVER after the user leaves the chart — pinning a CPU core, growing RAM to
|
|
175
|
+
// OOM, and stealing the GPU from RN's RenderThread (every other screen stalls).
|
|
176
|
+
// We pause the WebView whenever no host owns it (after the first load) and
|
|
177
|
+
// resume on claim. Uses the PER-INSTANCE onPause()/onResume() — NOT the static
|
|
178
|
+
// pauseTimers()/resumeTimers(), which are process-global and would also freeze
|
|
179
|
+
// the app's other WebViews (inpage provider / web-embed / dapp browser).
|
|
180
|
+
private var paused = false
|
|
181
|
+
private var hasLoadedOnce = false
|
|
182
|
+
|
|
183
|
+
fun markLoaded() {
|
|
184
|
+
hasLoadedOnce = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Pause the renderer when nobody owns the shared WebView. onPause() stops
|
|
188
|
+
// drawing/compositing/animations (frees the GPU so navigation is smooth again)
|
|
189
|
+
// WITHOUT changing the view's visibility/attachment — we must NOT toggle
|
|
190
|
+
// visibility here, that left the WebView blank/white after re-claim. Skipped
|
|
191
|
+
// until the first load so we never freeze a booting page. Resumed (+ redraw)
|
|
192
|
+
// on the next CLAIM so the chart paints again.
|
|
193
|
+
fun pauseIfIdle() {
|
|
194
|
+
if (paused || !hasLoadedOnce || owner != null) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
paused = true
|
|
198
|
+
android.util.Log.i(TAG, "pool[$key] PAUSE (renderer idle)")
|
|
199
|
+
runOnUiThread { webView.onPause() }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fun resume() {
|
|
203
|
+
if (!paused) return
|
|
204
|
+
paused = false
|
|
205
|
+
android.util.Log.i(TAG, "pool[$key] RESUME")
|
|
206
|
+
runOnUiThread {
|
|
207
|
+
webView.onResume()
|
|
208
|
+
webView.invalidate()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
152
211
|
|
|
153
212
|
private var assetLoader: WebViewAssetLoader? = null
|
|
154
213
|
private var lastLoadedUrl: String? = null
|
|
@@ -179,9 +238,28 @@ class PooledChartWebView private constructor(
|
|
|
179
238
|
addJavascriptInterface(ChartBridge(), "AndroidChartBridge")
|
|
180
239
|
}
|
|
181
240
|
|
|
241
|
+
// Apply the app's "Enable Native Webview Debugging" dev-mode toggle, mirroring
|
|
242
|
+
// how the main react-native-webview calls WebView.setWebContentsDebuggingEnabled.
|
|
243
|
+
// Called by the host both on the prop change and at host claim, so the toggle is
|
|
244
|
+
// honored even when this entry was created before the prop arrived.
|
|
245
|
+
//
|
|
246
|
+
// CAVEAT (PROCESS-GLOBAL): setWebContentsDebuggingEnabled is a STATIC method that
|
|
247
|
+
// flips remote-debugging for EVERY WebView in the whole process. Once any WebView
|
|
248
|
+
// (this chart, the main react-native-webview, etc.) enables it, it stays enabled
|
|
249
|
+
// process-wide until the app is killed — Android exposes no per-WebView toggle and
|
|
250
|
+
// no way to read the current value. So a null/false preference here cannot turn
|
|
251
|
+
// debugging back OFF once another WebView (or a prior true value) turned it ON; it
|
|
252
|
+
// simply does not re-enable it. We therefore only ever call the setter with `true`
|
|
253
|
+
// when explicitly enabled, leaving the process-global state untouched otherwise.
|
|
254
|
+
fun setInspectable(enabled: Boolean?) {
|
|
255
|
+
if (enabled == true) {
|
|
256
|
+
runOnUiThread { WebView.setWebContentsDebuggingEnabled(true) }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
182
260
|
init {
|
|
183
261
|
val n = liveCount.incrementAndGet()
|
|
184
|
-
android.util.Log.d(
|
|
262
|
+
android.util.Log.d(TAG, "WebView CREATED key=$key liveCount=$n")
|
|
185
263
|
|
|
186
264
|
webView.webViewClient = object : WebViewClientCompat() {
|
|
187
265
|
override fun shouldInterceptRequest(
|
|
@@ -191,11 +269,12 @@ class PooledChartWebView private constructor(
|
|
|
191
269
|
|
|
192
270
|
override fun onPageFinished(view: WebView, url: String?) {
|
|
193
271
|
super.onPageFinished(view, url)
|
|
272
|
+
markLoaded()
|
|
194
273
|
// The bridge is delivered exclusively via the origin-scoped document-start
|
|
195
274
|
// script (see registerBridgeForOrigins). We deliberately do NOT re-inject
|
|
196
275
|
// it here via evaluateJavascript — that ran on every page event regardless
|
|
197
276
|
// of URL and would expose the privileged bridge to untrusted pages/frames.
|
|
198
|
-
owner?.dispatchLoadEnd()
|
|
277
|
+
(owner ?: warmDriver)?.dispatchLoadEnd()
|
|
199
278
|
// Prime the snapshot so the first move already has a frame to mask with.
|
|
200
279
|
refreshSnapshotSoon()
|
|
201
280
|
}
|
|
@@ -217,7 +296,26 @@ class PooledChartWebView private constructor(
|
|
|
217
296
|
private inner class ChartBridge {
|
|
218
297
|
@JavascriptInterface
|
|
219
298
|
fun postMessage(message: String) {
|
|
220
|
-
runOnUiThread {
|
|
299
|
+
runOnUiThread {
|
|
300
|
+
val target = owner ?: warmDriver
|
|
301
|
+
target?.dispatchMessage(message)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Robustly detach the WebView from [parent], even when [parent] is a disposed
|
|
307
|
+
// host container that is mid-teardown / detached from the window and holds the
|
|
308
|
+
// child in a transition / disappearing-children list — where a plain removeView()
|
|
309
|
+
// is deferred and leaves webView.parent set, stranding the shared WebView and
|
|
310
|
+
// blanking the next chart slot. endViewTransition() clears any pending transition
|
|
311
|
+
// hold; removeViewInLayout() is the in-layout fallback if the child is still held.
|
|
312
|
+
private fun forceDetach(parent: ViewGroup) {
|
|
313
|
+
if (webView.parent !== parent) return
|
|
314
|
+
try { parent.endViewTransition(webView) } catch (e: Throwable) {}
|
|
315
|
+
parent.removeView(webView)
|
|
316
|
+
if (webView.parent === parent) {
|
|
317
|
+
try { parent.removeViewInLayout(webView) } catch (e: Throwable) {}
|
|
318
|
+
parent.requestLayout()
|
|
221
319
|
}
|
|
222
320
|
}
|
|
223
321
|
|
|
@@ -225,23 +323,30 @@ class PooledChartWebView private constructor(
|
|
|
225
323
|
fun attachTo(container: ViewGroup) {
|
|
226
324
|
val generation = attachGeneration.incrementAndGet()
|
|
227
325
|
runOnUiThread {
|
|
228
|
-
attachToContainer(container, generation,
|
|
326
|
+
attachToContainer(container, generation, retriesLeft = 12)
|
|
229
327
|
}
|
|
230
328
|
}
|
|
231
329
|
|
|
232
|
-
private fun attachToContainer(container: ViewGroup, generation: Int,
|
|
330
|
+
private fun attachToContainer(container: ViewGroup, generation: Int, retriesLeft: Int) {
|
|
233
331
|
if (generation != attachGeneration.get()) return
|
|
234
332
|
if (webView.parent === container) return
|
|
235
333
|
|
|
236
|
-
(webView.parent as? ViewGroup)?.
|
|
334
|
+
(webView.parent as? ViewGroup)?.let { forceDetach(it) }
|
|
237
335
|
val currentParent = webView.parent
|
|
238
|
-
if (currentParent != null) {
|
|
239
|
-
|
|
240
|
-
|
|
336
|
+
if (currentParent != null && currentParent !== container) {
|
|
337
|
+
// The old container still hasn't released the WebView (a disposed host's
|
|
338
|
+
// container mid-teardown / detached from window: removeView is deferred and
|
|
339
|
+
// getParent() stays set). Retry — but on the TARGET container's handler,
|
|
340
|
+
// which is attached to the window so its queue keeps draining, instead of the
|
|
341
|
+
// detached webView/old parent whose post() runnables may never run.
|
|
342
|
+
if (retriesLeft > 0) {
|
|
343
|
+
container.post {
|
|
344
|
+
attachToContainer(container, generation, retriesLeft = retriesLeft - 1)
|
|
345
|
+
}
|
|
241
346
|
} else {
|
|
242
347
|
android.util.Log.w(
|
|
243
|
-
|
|
244
|
-
"Skip attach key=$key because WebView parent was not cleared: $currentParent",
|
|
348
|
+
TAG,
|
|
349
|
+
"Skip attach key=$key after retries because WebView parent was not cleared: $currentParent",
|
|
245
350
|
)
|
|
246
351
|
}
|
|
247
352
|
return
|
|
@@ -260,6 +365,27 @@ class PooledChartWebView private constructor(
|
|
|
260
365
|
refreshSnapshotSoon()
|
|
261
366
|
}
|
|
262
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Remove the WebView from [container] ONLY if it is still parented there.
|
|
370
|
+
*
|
|
371
|
+
* Called on a pooled host's final teardown (dispose): that host's container is
|
|
372
|
+
* about to be dropped from the view tree, and a pooled host deliberately does
|
|
373
|
+
* NOT detach on the YIELD path (to avoid racing a rapid re-claim). Without this,
|
|
374
|
+
* the shared WebView stays parented to the dropped/detached container; the next
|
|
375
|
+
* host's [attachTo] then keeps hitting "old parent not cleared" (removeView on a
|
|
376
|
+
* detached container isn't applied synchronously) and retries forever — leaving
|
|
377
|
+
* the new chart slot blank/white. The `parent === container` guard makes this
|
|
378
|
+
* safe against ordering: if a new host has already re-claimed and reparented the
|
|
379
|
+
* WebView, we must NOT rip it back off, so we only remove when WE still hold it.
|
|
380
|
+
*/
|
|
381
|
+
fun detachFrom(container: ViewGroup) {
|
|
382
|
+
runOnUiThread {
|
|
383
|
+
if (webView.parent === container) {
|
|
384
|
+
forceDetach(container)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
263
389
|
/** Remove the WebView from its current parent (keeps it alive, warm). */
|
|
264
390
|
fun detachFromParent() {
|
|
265
391
|
runOnUiThread {
|
|
@@ -437,7 +563,7 @@ class PooledChartWebView private constructor(
|
|
|
437
563
|
(webView.parent as? ViewGroup)?.removeView(webView)
|
|
438
564
|
webView.destroy()
|
|
439
565
|
val n = liveCount.decrementAndGet()
|
|
440
|
-
android.util.Log.d(
|
|
566
|
+
android.util.Log.d(TAG, "WebView DESTROYED key=$key liveCount=$n")
|
|
441
567
|
}
|
|
442
568
|
}
|
|
443
569
|
|