@onekeyfe/react-native-perf-stats 3.0.27 → 3.0.29

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.
@@ -593,7 +593,11 @@ internal object Overlay : Application.ActivityLifecycleCallbacks {
593
593
  val view = overlayView ?: return
594
594
  if (attachedToWindowManager) {
595
595
  try {
596
- val wm = currentActivity?.getSystemService(Context.WINDOW_SERVICE)
596
+ // Use the view's own context, not currentActivity. onActivityDestroyed
597
+ // posts detach() to the main handler and then clears currentActivity
598
+ // synchronously, so by the time this runs currentActivity may already
599
+ // be null and the overlay would otherwise leak.
600
+ val wm = view.context.getSystemService(Context.WINDOW_SERVICE)
597
601
  as? WindowManager
598
602
  wm?.removeView(view)
599
603
  } catch (e: Exception) {
@@ -412,19 +412,43 @@ private final class JsFpsHolder {
412
412
 
413
413
  // MARK: - Overlay
414
414
  //
415
- // Singleton UILabel attached to the current key UIWindow. Updates always
416
- // dispatch to main. No floating-window permission needed; overlay only
417
- // shows while the app is in the foreground.
415
+ // Singleton overlay rendered on its own dedicated UIWindow at
416
+ // `.alert + 1` windowLevel. The dedicated window is required because
417
+ // React Native's <Modal> presents view controllers via UIKit's modal
418
+ // presentation, which renders above any subview added to the host
419
+ // app's main UIWindow — a UILabel attached to keyWindow ends up behind
420
+ // the modal regardless of view-hierarchy z-order. A separate UIWindow
421
+ // with a higher `windowLevel` sits above modal-presented view
422
+ // controllers and system alerts, so the overlay stays visible.
423
+ //
424
+ // `OverlayPassthroughWindow` overrides `hitTest` to return nil for
425
+ // touches outside the label, letting them fall through to the
426
+ // underlying app windows — otherwise the overlay window would swallow
427
+ // every tap on the screen.
418
428
  //
419
429
  // Inherits NSObject so UIPanGestureRecognizer's target/action selector
420
430
  // dispatch resolves cleanly via Obj-C runtime.
421
431
 
432
+ private final class OverlayPassthroughWindow: UIWindow {
433
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
434
+ let hit = super.hitTest(point, with: event)
435
+ // Anything that bubbles up to the window itself or its empty
436
+ // root view means the touch missed our overlay subview — pass it
437
+ // through to whatever's below this window.
438
+ if hit === self || hit === self.rootViewController?.view {
439
+ return nil
440
+ }
441
+ return hit
442
+ }
443
+ }
444
+
422
445
  private final class Overlay: NSObject {
423
446
  static let shared = Overlay()
424
447
 
425
448
  private override init() { super.init() }
426
449
 
427
450
  private var label: UILabel?
451
+ private var overlayWindow: UIWindow?
428
452
  private var visible = false
429
453
 
430
454
  // `_lastSample` is written by the Sampler timer thread and read by the
@@ -484,11 +508,27 @@ private final class Overlay: NSObject {
484
508
 
485
509
  private func attach() {
486
510
  if label != nil { return }
487
- guard let window = currentKeyWindow() else {
488
- OneKeyLog.warn(kTag, "No key UIWindow available; overlay deferred")
511
+ guard let scene = currentForegroundScene() else {
512
+ OneKeyLog.warn(kTag, "No foreground UIWindowScene; overlay deferred")
489
513
  return
490
514
  }
491
515
 
516
+ // Dedicated host VC keeps the window's view hierarchy minimal —
517
+ // the empty UIView serves only as the parent for the label and
518
+ // as the hitTest sentinel checked by OverlayPassthroughWindow.
519
+ let host = UIViewController()
520
+ host.view.backgroundColor = .clear
521
+
522
+ let window = OverlayPassthroughWindow(windowScene: scene)
523
+ window.frame = scene.coordinateSpace.bounds
524
+ // .alert is 2000 on iOS; +1 puts the overlay above modal-presented
525
+ // controllers, system alerts and action sheets. Status bar is
526
+ // .statusBar (1000), which we stay below intentionally.
527
+ window.windowLevel = UIWindow.Level.alert + 1
528
+ window.backgroundColor = .clear
529
+ window.isHidden = false
530
+ window.rootViewController = host
531
+
492
532
  let lbl = UILabel(frame: CGRect(x: 30, y: 100, width: 170, height: 92))
493
533
  lbl.backgroundColor = UIColor.black.withAlphaComponent(0.7)
494
534
  lbl.layer.cornerRadius = 8
@@ -503,8 +543,9 @@ private final class Overlay: NSObject {
503
543
  let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
504
544
  lbl.addGestureRecognizer(pan)
505
545
 
506
- window.addSubview(lbl)
546
+ host.view.addSubview(lbl)
507
547
  label = lbl
548
+ overlayWindow = window
508
549
 
509
550
  if let s = lastSample {
510
551
  lbl.text = renderText(s)
@@ -514,6 +555,13 @@ private final class Overlay: NSObject {
514
555
  private func detach() {
515
556
  label?.removeFromSuperview()
516
557
  label = nil
558
+ // Hide before nilling so UIKit can release the window cleanly;
559
+ // assigning nil to rootViewController first avoids a fleeting
560
+ // warning when the window is torn down with a controller still
561
+ // attached.
562
+ overlayWindow?.isHidden = true
563
+ overlayWindow?.rootViewController = nil
564
+ overlayWindow = nil
517
565
  }
518
566
 
519
567
  @objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
@@ -541,29 +589,19 @@ private final class Overlay: NSObject {
541
589
  return "CPU: \(cpuStr)\nRAM: \(memStr)\nUI: \(uiStr)\nJS: \(jsStr)"
542
590
  }
543
591
 
544
- private func currentKeyWindow() -> UIWindow? {
545
- // iOS 15+ preferred path
546
- if #available(iOS 15.0, *) {
547
- for scene in UIApplication.shared.connectedScenes {
548
- guard
549
- let windowScene = scene as? UIWindowScene,
550
- windowScene.activationState == .foregroundActive
551
- else { continue }
552
- if let kw = windowScene.keyWindow {
553
- return kw
554
- }
555
- }
556
- }
557
- // Fallback: scan all connected scenes
592
+ /// Locate an active foreground `UIWindowScene` to host the overlay
593
+ /// window. Prefers `.foregroundActive`; falls back to the first
594
+ /// connected `UIWindowScene` so we still attach during transient
595
+ /// states like cold launch when no scene is `.foregroundActive` yet.
596
+ private func currentForegroundScene() -> UIWindowScene? {
597
+ var fallback: UIWindowScene?
558
598
  for scene in UIApplication.shared.connectedScenes {
559
- guard let windowScene = scene as? UIWindowScene else { continue }
560
- if let kw = windowScene.windows.first(where: { $0.isKeyWindow }) {
561
- return kw
562
- }
563
- if let any = windowScene.windows.first {
564
- return any
599
+ guard let ws = scene as? UIWindowScene else { continue }
600
+ if ws.activationState == .foregroundActive {
601
+ return ws
565
602
  }
603
+ if fallback == nil { fallback = ws }
566
604
  }
567
- return nil
605
+ return fallback
568
606
  }
569
607
  }
@@ -43,9 +43,13 @@ export interface ReactNativePerfStats extends HybridObject<{
43
43
  /**
44
44
  * Show the floating overlay (CPU + RAM) drawn natively.
45
45
  *
46
- * - Android: TextView attached to the current Activity via
47
- * `addContentView` no floating-window permission required.
48
- * - iOS: UILabel added to the key UIWindow.
46
+ * - Android: TextView attached via `WindowManager.addView` using the
47
+ * current Activity's window token (window type
48
+ * `TYPE_APPLICATION_ABOVE_SUB_PANEL`) no floating-window permission
49
+ * required, and the overlay tracks Activity lifecycle (re-attach on
50
+ * resume, detach on pause/destroy).
51
+ * - iOS: UILabel hosted on a dedicated passthrough UIWindow above the
52
+ * app's key window so it stays above modally-presented controllers.
49
53
  * - Draggable on both platforms.
50
54
  * - Idempotent. If the sampler is not running, the overlay shows "--"
51
55
  * until `start` is called.
@@ -1 +1 @@
1
- {"version":3,"file":"ReactNativePerfStats.nitro.d.ts","sourceRoot":"","sources":["../../../src/ReactNativePerfStats.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhC,+EAA+E;IAC/E,IAAI,IAAI,IAAI,CAAC;IAEb;;;;;;;;;OASG;IACH,WAAW,IAAI,IAAI,CAAC;IAEpB,gFAAgF;IAChF,WAAW,IAAI,IAAI,CAAC;IAEpB;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAE9B;;;;;;;;OAQG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC"}
1
+ {"version":3,"file":"ReactNativePerfStats.nitro.d.ts","sourceRoot":"","sources":["../../../src/ReactNativePerfStats.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAEhC,+EAA+E;IAC/E,IAAI,IAAI,IAAI,CAAC;IAEb;;;;;;;;;;;;;OAaG;IACH,WAAW,IAAI,IAAI,CAAC;IAEpB,gFAAgF;IAChF,WAAW,IAAI,IAAI,CAAC;IAEpB;;;;OAIG;IACH,MAAM,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IAE9B;;;;;;;;OAQG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-perf-stats",
3
- "version": "3.0.27",
3
+ "version": "3.0.29",
4
4
  "description": "react-native-perf-stats",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -45,9 +45,13 @@ export interface ReactNativePerfStats
45
45
  /**
46
46
  * Show the floating overlay (CPU + RAM) drawn natively.
47
47
  *
48
- * - Android: TextView attached to the current Activity via
49
- * `addContentView` no floating-window permission required.
50
- * - iOS: UILabel added to the key UIWindow.
48
+ * - Android: TextView attached via `WindowManager.addView` using the
49
+ * current Activity's window token (window type
50
+ * `TYPE_APPLICATION_ABOVE_SUB_PANEL`) no floating-window permission
51
+ * required, and the overlay tracks Activity lifecycle (re-attach on
52
+ * resume, detach on pause/destroy).
53
+ * - iOS: UILabel hosted on a dedicated passthrough UIWindow above the
54
+ * app's key window so it stays above modally-presented controllers.
51
55
  * - Draggable on both platforms.
52
56
  * - Idempotent. If the sampler is not running, the overlay shows "--"
53
57
  * until `start` is called.