skip to Main Content

I found this crash log in Firebase:
enter image description here

Crashed: com.apple.main-thread
0  libdispatch.dylib              0x4b20 dispatch_semaphore_signal + 8
1  GameCenterUI                   0x9ebd8 __56-[GKNotificationBannerViewController hideBannerQuickly:]_block_invoke_2 + 40
2  libdispatch.dylib              0x3f88 _dispatch_client_callout + 20
3  libdispatch.dylib              0x7418 _dispatch_continuation_pop + 504
4  libdispatch.dylib              0x1aa58 _dispatch_source_invoke + 1588
5  libdispatch.dylib              0x12748 _dispatch_main_queue_drain + 756
6  libdispatch.dylib              0x12444 _dispatch_main_queue_callback_4CF + 44
7  CoreFoundation                 0x9a6c8 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
8  CoreFoundation                 0x7c02c __CFRunLoopRun + 2036
9  CoreFoundation                 0x80eb0 CFRunLoopRunSpecific + 612
10 GraphicsServices               0x1368 GSEventRunModal + 164
11 UIKitCore                      0x3a1668 -[UIApplication _run] + 888
12 UIKitCore                      0x3a12cc UIApplicationMain + 340
13 libswiftUIKit.dylib            0x35308 UIApplicationMain(_:_:_:_:) + 104
14 BinMinesweeper                 0x7050 main + 4345163856 (AppSceneDelegate.swift:4345163856)
15 ???                            0x1e6d6c960 (Missing)

The above example came from iOS 16.3.1, iPhone XR. I can’t reproduce this crash. It happens pretty rarely (less than 1% of users).

I assume GKNotificationBannerViewController is the top banner "Welcome user_abc" presented when you launch the app.

The only time I interact with Game Center is when user click a leaderboard button, I show the game center VC. Here are some code:

import GameKit

public enum GameCenterUtil {
  
  private static var g_isEnabled: Bool = false
  
  public static func setupIfNeeded() {
    GKLocalPlayer.local.authenticateHandler = { loginVC, error in
      if loginVC != nil {
        // Nothing. Do not present login. 
        // User generally don't use game center. It's annoying.
        return
      }
      if error != nil {
        g_isEnabled = false
        return
      }
      g_isEnabled = true
    }
  }
  
  public static func reportScore(_ score: Int, category: String) {
    guard g_isEnabled else { return }
    GKLeaderboard.submitScore(score, context: 0, player: GKLocalPlayer.local, leaderboardIDs: [category]) { error in
      // nothing
    }
  }
  
  public static func presentLeaderboard(in vc: UIViewController) {
    // when disabled, this will be an alert.
    let gameCenterVC = GKGameCenterViewController()
    gameCenterVC.gameCenterDelegate = LeaderboardDismisser.shared
    vc.present(gameCenterVC, animated: true, completion: nil)
  }
}

final class LeaderboardDismisser: NSObject, GKGameCenterControllerDelegate {
  static let shared = LeaderboardDismisser()
  func gameCenterViewControllerDidFinish(_ gameCenterViewController: GKGameCenterViewController) {
    // This is required. When user is not logged in, the alert prompts up. If we don't dismiss it, the game vc will be not responding. 
    gameCenterViewController.dismiss(animated: true, completion: nil)
  }
}

Then in didFinishLaunching, i call GameCenterUtil.setupIfNeeded(), and in leaderboard button callback, I call GameCenterUtil.presentLeaderboard(in: vc).

Edit:
There is also a very similar crash, also unable to repro and happens rarely:

enter image description here

Crashed: com.apple.GameKit.banner
0  libdispatch.dylib              0x4a60 dispatch_semaphore_wait + 8
1  GameCenterUI                   0x9abe4 __42+[GKNotificationBannerWindow enqueBanner:]_block_invoke_2 + 60
2  libdispatch.dylib              0x2320 _dispatch_call_block_and_release + 32
3  libdispatch.dylib              0x3eac _dispatch_client_callout + 20
4  libdispatch.dylib              0xb534 _dispatch_lane_serial_drain + 668
5  libdispatch.dylib              0xc0d8 _dispatch_lane_invoke + 436
6  libdispatch.dylib              0x16cdc _dispatch_workloop_worker_thread + 648
7  libsystem_pthread.dylib        0xddc _pthread_wqthread + 288
8  libsystem_pthread.dylib        0xb7c start_wqthread + 8

EDIT 2:

This is how I present/dismiss the game VC:

In button click, I simply call the present function:

    let button = MySpriteButton(texture: GAME_TEXTURE(.leaderboardBig))
    {
      PLAY_APP_SOUND(.click)
      GameCenterUtil.presentLeaderboard(in: vc)
    }

Then I auto dismiss it when user enter background. This is a workaround for a bug earlier (refer to the comment in the code):

// MySceneDelegate.swift
open func sceneDidEnterBackground(_ scene: UIScene) {

   // When we background the app with game center VC presented, 
   // Then a while later when we foreground the app, sometimes the game center VC becomes transparent for some reason, 
   // and covers the whole screen, making whole app not interactive
   // This could be a bug in game center
   // A naive solution is simply dismiss the game center VC when entering background.
   if
     let topVC = NAV_VC?.topViewController,
     let gameCenterVC = topVC.presentedViewController as? GKGameCenterViewController
   {
     gameCenterVC.dismiss(animated: false, completion: nil)
   }
}

Now I have a feeling that this workaround may be the reason. Because the doc (https://developer.apple.com/documentation/gamekit/gknotificationbanner) says:

If the game is in the foreground, the banner appears immediately. If the game is in the background, the banner appears when the game becomes active.

This sounds like a queue structure. And one of my crash log has GKNotificationBannerWindow enqueBanner:. The 2 sound related.

I think I still want to keep this workaround, but not sure how it caused the crash. I have tried to delay the dismiss for 1 seconds in my latest release, but it doesn’t fix the issue:

  // A helper function
  func fetchTopGameCenterVC() -> GKGameCenterViewController? {
    if
      let topVC = NAV_VC?.topViewController,
      let gameCenterVC = topVC.presentedViewController as? GKGameCenterViewController
    {
      return gameCenterVC
    }
    return nil
  }

  
// MySceneDelegate.swift

  open func sceneDidEnterBackground(_ scene: UIScene) {
    if let vcBeforeDelay = fetchTopGameCenterVC() {
      DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        if let vcAfterDelay = fetchTopGameCenterVC(),
           vcAfterDelay === vcBeforeDelay
        {
          vcAfterDelay.dismiss(animated: false, completion: nil)
        }
      }
    }
  }

2

Answers


  1. Maybe the reason for these crashes is a deadlock: In both cases, the crashed thread tries to get a semaphore, but cannot get it before the watchdog cancels the app.
    I don’t know GameCenterUI, but since it is UI related, maybe it can only be called on the main thread. However, your 2nd stack trace shows that it is called on another thread.
    So, please check if it is OK to call GameCenterUI on other threads.

    Login or Signup to reply.
  2. Based on the crash logs you have provided, it seems that this issue might be connected to the internal GameKit logic. Both crash logs mention the presence of dispatch_semaphore.

    As discussed in How to properly deallocate an object that contains a DispatchSemaphore, your workaround might break internal object lifecycle connected with dispatch_semaphore balancing.

    if
        let topVC = NAV_VC?.topViewController,
        let gameCenterVC = topVC.presentedViewController as? GKGameCenterViewController 
    {
        gameCenterVC.dismiss(animated: false, completion: nil)
    }
    

    I suggest you to try the A/B Testing in this case.
    Try to setup a user group with 10-15% of your users, disabling this auto-dismiss feature for them, based on results you can react correspondently.

    Firebase A/B Tutorial

    Kodeco (aka Raywenderlich) A/B Testing Tutorial

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search