应用内购买成功导致部分用户崩溃

Posted

技术标签:

【中文标题】应用内购买成功导致部分用户崩溃【英文标题】:Successful In App Purchase causes crash for some users 【发布时间】:2016-03-08 12:21:39 【问题描述】:

我有一个只有一个 IAP 的应用程序,它允许用户解锁完整的游戏。对于少数用户,他们成功购买了 IAP,但当他们点击继续时,游戏崩溃了。重新启动应用程序并尝试加载他们保存的游戏时,应用程序再次崩溃。删除并重新安装应用程序也没有任何区别。

这是一个崩溃报告:

Incident Identifier: B7B61633-1BE4-4AB2-99ED-A207B2E88525
CrashReporter Key:   2b01761b32c1d23c1adf755f83cc58464c9e7e77
Hardware Model:      iPhone5,2
Process:             MyApp [551]
Path:                /private/var/mobile/Containers/Bundle/Application/43D176E1-395E-4BF5-A0D5-3602068AADA6/MyApp.app/MyApp
Identifier:          com.BlahBlah.MyApp
Version:             5 (1.1)
Code Type:           ARM (Native)
Parent Process:      launchd [1]

Date/Time:           2016-03-02 02:10:42.42 +0000
Launch Time:         2016-03-02 02:10:27.27 +0000
OS Version:          ios 9.2.1 (13D15)
Report Version:      105

Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x00000000e7ffdefe
Triggered by Thread:  0

Breadcrumb Trail: (reverse chronological seconds)
14     GC Framework: startAuthenticationForExistingPrimaryPlayer


Global Trace Buffer (reverse chronological seconds):
13.238455    CFNetwork                  0x0000000023da3e45 TCP Conn 0x16ede9d0 SSL Handshake DONE
13.532519    CFNetwork                  0x0000000023da3d7f TCP Conn 0x16ede9d0 starting SSL negotiation
13.534444    CFNetwork                  0x0000000023e231a5 TCP Conn 0x16ede9d0 complete. fd: 11, err: 0
13.537454    CFNetwork                  0x0000000023e242a7 TCP Conn 0x16ede9d0 event 1. err: 0
13.643210    CFNetwork                  0x0000000023e24325 TCP Conn 0x16ede9d0 started
13.648224    CFNetwork                  0x0000000023da3e45 TCP Conn 0x16ed89f0 SSL Handshake DONE
13.982238    CFNetwork                  0x0000000023da3d7f TCP Conn 0x16ed89f0 starting SSL negotiation
13.982896    CFNetwork                  0x0000000023e231a5 TCP Conn 0x16ed89f0 complete. fd: 6, err: 0
13.984447    CFNetwork                  0x0000000023e242a7 TCP Conn 0x16ed89f0 event 1. err: 0

Thread 0 name:
Thread 0 Crashed:
0   MyApp                       0x002028ac _TFFC11MyApp9IAPHelper12paymentQueueFS0_FTCSo14SKPaymentQueue19updatedTransactionsGSaCSo20SKPaymentTransaction__T_U_FT_T_ + 7504 (IAPHelper.swift:0)
1   libdispatch.dylib               0x23447dd6 _dispatch_call_block_and_release + 10 (init.c:760)
2   libdispatch.dylib               0x234514e6 _dispatch_after_timer_callback + 66 (queue.c:3293)
3   libdispatch.dylib               0x23447dc2 _dispatch_client_callout + 22 (init.c:819)
4   libdispatch.dylib               0x2345a6d2 _dispatch_source_latch_and_call + 2042 (inline_internal.h:1063)
5   libdispatch.dylib               0x23449d16 _dispatch_source_invoke + 738 (source.c:755)
6   libdispatch.dylib               0x2344c1fe _dispatch_main_queue_callback_4CF + 394 (inline_internal.h:1043)
7   CoreFoundation                  0x2386cfc4 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 8 (CFRunLoop.c:1613)
8   CoreFoundation                  0x2386b4be __CFRunLoopRun + 1590 (CFRunLoop.c:2718)
9   CoreFoundation                  0x237bdbb8 CFRunLoopRunSpecific + 516 (CFRunLoop.c:2814)
10  CoreFoundation                  0x237bd9ac CFRunLoopRunInMode + 108 (CFRunLoop.c:2844)
11  GraphicsServices                0x24a37af8 GSEventRunModal + 160 (GSEvent.c:2245)
12  UIKit                           0x27aa9fb4 UIApplicationMain + 144 (UIApplication.m:3681)
13  MyApp                           0x001898f4 main + 180 (AppDelegate.swift:12)
14  libdyld.dylib                   0x23470872 start + 2 (start_glue.s:64)

Thread 1 name:
Thread 1:
0   libsystem_kernel.dylib          0x23543320 kevent_qos + 24
1   libdispatch.dylib               0x2345794e _dispatch_mgr_invoke + 254 (source.c:2542)
2   libdispatch.dylib               0x23449a2e _dispatch_mgr_thread + 38 (source.c:2573)

Thread 2:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 3:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 4:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 5 name:
Thread 5:
0   libsystem_kernel.dylib          0x2352dbf8 mach_msg_trap + 20 (syscall_sw.h:105)
1   libsystem_kernel.dylib          0x2352d9f8 mach_msg + 40 (mach_msg.c:103)
2   CoreFoundation                  0x2386cf1c __CFRunLoopServiceMachPort + 136 (CFRunLoop.c:2345)
3   CoreFoundation                  0x2386b2a2 __CFRunLoopRun + 1050 (CFRunLoop.c:2607)
4   CoreFoundation                  0x237bdbb8 CFRunLoopRunSpecific + 516 (CFRunLoop.c:2814)
5   CoreFoundation                  0x237bd9ac CFRunLoopRunInMode + 108 (CFRunLoop.c:2844)
6   CFNetwork                       0x23e049e6 +[NSURLConnection(Loader) _resourceLoadLoop:] + 486 (NSURLConnection.mm:325)
7   Foundation                      0x240c632c __NSThread__start__ + 1144 (NSThread.m:1134)
8   libsystem_pthread.dylib         0x235e2c7e _pthread_body + 138 (pthread.c:656)
9   libsystem_pthread.dylib         0x235e2bf2 _pthread_start + 110 (pthread.c:692)
10  libsystem_pthread.dylib         0x235e0a08 thread_start + 8 (pthread_asm.s:162)

Thread 6 name:
Thread 6:
0   libsystem_kernel.dylib          0x23541f14 __select + 20
1   CoreFoundation                  0x238723c0 __CFSocketManager + 572 (CFSocket.c:2128)
2   libsystem_pthread.dylib         0x235e2c7e _pthread_body + 138 (pthread.c:656)
3   libsystem_pthread.dylib         0x235e2bf2 _pthread_start + 110 (pthread.c:692)
4   libsystem_pthread.dylib         0x235e0a08 thread_start + 8 (pthread_asm.s:162)

Thread 7:
0   libsystem_kernel.dylib          0x2354288c __workq_kernreturn + 8
1   libsystem_pthread.dylib         0x235e0e18 _pthread_wqthread + 1036 (pthread.c:1999)
2   libsystem_pthread.dylib         0x235e09fc start_wqthread + 8 (pthread_asm.s:147)

Thread 0 crashed with ARM Thread State (32-bit):
    r0: 0x00000000    r1: 0x00000000      r2: 0x39c940b0      r3: 0x00000000
    r4: 0x00000000    r5: 0x00631376      r6: 0x00000000      r7: 0x0040dcf4
    r8: 0x0064e984    r9: 0x00000000     r10: 0x00000001     r11: 0x16d54600
    ip: 0xf64d8965    sp: 0x0040db34      lr: 0x002011f0      pc: 0x002028ac
  cpsr: 0x60000010

它崩溃的方法是:

public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) 
  let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
  dispatch_after(delayTime, dispatch_get_main_queue()) 

    for transaction in transactions 
      switch (transaction.transactionState) 
      case .Purchased:
        self.completeTransaction(transaction)
        break
      case .Failed:
        self.failedTransaction(transaction)
        break
      case .Restored:
        self.restoreTransaction(transaction)
        break
      case .Deferred:
        break
      case .Purchasing:
        break
      
    
  

我使用了一个 IAPHelper.swift 类,该类取自 Ray Wenderlich 的 IAP 教程:http://www.raywenderlich.com/105365/in-app-purchases-tutorial-getting-started

为了提供尽可能多的信息,我将在这个类下面列出完整的内容以及来自 UnlockGameViewController 的相关代码,从中可以看到解锁游戏的选项:

//  IAPHelper.swift

import StoreKit

// ** NSNotifications - for sending messages to handle in UnlockGameVC ** //

/// Notification that is generated when a product is purchased.
public let IAPHelperProductPurchasedNotification = "IAPHelperProductPurchasedNotification"

/// Notification that is generated when a transaction fails.
public let IAPHelperTransactionFailedNotification = "IAPHelperTransactionFailedNotification"

/// Notification that is generated when cannot retrieve IAPs from iTunes.
public let IAPHelperConnectionErrorNotification = "IAPHelperConnectionErrorNotification"

/// Notification that is generated when we need to stop the spinner.
public let IAPHelperStopSpinnerNotification = "IAPHelperStopSpinnerNotification"

/// Product identifiers are unique strings registered on the app store.
public typealias ProductIdentifier = String

/// Completion handler called when products are fetched.
public typealias RequestProductsCompletionHandler = (success: Bool, products: [SKProduct]) -> ()


/// A Helper class for In-App-Purchases, it can fetch products, tell you if a product has been purchased,
/// purchase products, and restore purchases.  Uses NSUserDefaults to cache if a product has been purchased.
public class IAPHelper : NSObject  

  /// MARK: - User facing API

  /// Initialize the helper.  Pass in the set of ProductIdentifiers supported by the app.
  public init(productIdentifiers: Set<ProductIdentifier>) 
    self.productIdentifiers = productIdentifiers

    for productIdentifier in productIdentifiers 
      let purchased = NSUserDefaults.standardUserDefaults().boolForKey(productIdentifier)
      if purchased 
        purchasedProductIdentifiers.insert(productIdentifier)
        print("Previously purchased: \(productIdentifier)")
       else 
        print("Not purchased: \(productIdentifier)")
      
    

    super.init()

    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
  

  /// Gets the list of SKProducts from the Apple server and calls the handler with the list of products.
  public func requestProductsWithCompletionHandler(handler: RequestProductsCompletionHandler) 
    completionHandler = handler
    productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
    productsRequest?.delegate = self
    productsRequest?.start()
  

  /// Initiates purchase of a product.
  public func purchaseProduct(product: SKProduct) 
    print("Buying \(product.productIdentifier)...")
    let payment = SKPayment(product: product)
    SKPaymentQueue.defaultQueue().addPayment(payment)
  

  /// Given the product identifier, returns true if that product has been purchased.
  public func isProductPurchased(productIdentifier: ProductIdentifier) -> Bool 
    return purchasedProductIdentifiers.contains(productIdentifier)
  

  /// If the state of whether purchases have been made is lost (e.g. the
  /// user deletes and reinstalls the app) this will recover the purchases.
  public func restoreCompletedTransactions() 
    SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
    print("Restoring...")
  

  public func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) 
    print("Restore queue finished.")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperStopSpinnerNotification, object: nil)
  

  public func paymentQueue(queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: NSError) 
    print("Restore queue failed.")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperConnectionErrorNotification, object: nil)
  

  public class func canMakePayments() -> Bool 
    return SKPaymentQueue.canMakePayments()
  

  /// MARK: - Private Properties

  // Used to keep track of the possible products and which ones have been purchased.
  private let productIdentifiers: Set<ProductIdentifier>
  private var purchasedProductIdentifiers = Set<ProductIdentifier>()

  // Used by SKProductsRequestDelegate
  private var productsRequest: SKProductsRequest?
  private var completionHandler: RequestProductsCompletionHandler?



// MARK: - SKProductsRequestDelegate

extension IAPHelper: SKProductsRequestDelegate 
  public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) 
    print("Loaded list of products...")
    let products = response.products 
    completionHandler?(success: true, products: products)
    clearRequest()

    // debug printing
    for p in products 
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    
  

  public func request(request: SKRequest, didFailWithError error: NSError) 
    print("Failed to load list of products.")
    print("Error: \(error)")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperConnectionErrorNotification, object: nil)
    clearRequest()
  

  private func clearRequest() 
    productsRequest = nil
    completionHandler = nil
  


extension IAPHelper: SKPaymentTransactionObserver 
  public func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) 
    let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
    dispatch_after(delayTime, dispatch_get_main_queue()) 

      for transaction in transactions 
        switch (transaction.transactionState) 
        case .Purchased:
          print("case .Purchased:")
          self.completeTransaction(transaction)
          break
        case .Failed:
          print("case .Failed:")
          self.failedTransaction(transaction)
          break
        case .Restored:
          print("case .Restored:")
          self.restoreTransaction(transaction)
          break
        case .Deferred:
          print("case .Deferred:")
          break
        case .Purchasing:
          print("case .Purchasing:")
          break
        
      
    
  

  private func completeTransaction(transaction: SKPaymentTransaction) 
    print("completeTransaction...")
    provideContentForProductIdentifier(transaction.payment.productIdentifier)
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  

  private func restoreTransaction(transaction: SKPaymentTransaction) 
    let productIdentifier = transaction.originalTransaction!.payment.productIdentifier
    print("restoreTransaction... \(productIdentifier)")
    provideContentForProductIdentifier(productIdentifier)
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  

  // Helper: Saves the fact that the product has been purchased and posts a notification.
  private func provideContentForProductIdentifier(productIdentifier: String) 
    purchasedProductIdentifiers.insert(productIdentifier)
    NSUserDefaults.standardUserDefaults().setBool(true, forKey: productIdentifier)
    NSUserDefaults.standardUserDefaults().synchronize()
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperProductPurchasedNotification, object: productIdentifier)
  

  private func failedTransaction(transaction: SKPaymentTransaction) 
    print("failedTransaction...")
    NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperStopSpinnerNotification, object: nil)
    if transaction.error!.code != SKErrorPaymentCancelled 
      print("Transaction error: \(transaction.error!.localizedDescription)")
      NSNotificationCenter.defaultCenter().postNotificationName(IAPHelperTransactionFailedNotification, object: nil)
    
    SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  


来自 UnlockGameViewController 的通知方法:

// MARK: - NSNotification methods

// When a product is purchased, this notification fires
func productPurchased(notification: NSNotification) 
  let productIdentifier = notification.object as! String
  for (index, product) in products.enumerate() 
    if product.productIdentifier == productIdentifier 
      // Only one IAP so we can assume this is the Unlock Full Game IAP
      activitySpinnerStop()

      agent.gameUnlocked = true  // sets the bool that the game has now been unlocked

      if openedFromMain == true 
        showAlertWith(Localization("GameUnlockedAlertTitle"), message: Localization("GameUnlockedAlertMessage"))
        noThanksButton.setTitle("Return to Main Menu", forState: UIControlState.Normal)
       else 
        showAlertWith(Localization("GameUnlockedAlertTitle"), message: Localization("GameUnlockedAlertMessage2"))
      
    
  


// When a transaction fails, this notification fires
func transactionFailed(notification: NSNotification) 
  activitySpinnerStop()
  showAlertWith(Localization("TransactionFailedAlertTitle"), message: Localization("TransactionFailedAlertMessage"))


// When we cannot connect to iTunes to retrieve the IAPs, this notification fires
func cannotConnect(notification: NSNotification) 
  activitySpinnerStop()
  showAlertWith(Localization("NoConnectionAlertTitle"), message: Localization("NoConnectionAlertMessage"))

确实有两个问题:1)为什么它首先会崩溃? 2)为什么他们不能删除应用程序,重新安装和恢复?特别是在付款完成时。

老实说,如果我的用户能够毫无问题地删除、重新安装和恢复,他们可以忍受初始崩溃。一位用户报告说,在他的 iPhone 崩溃后,他在 iPad 上下载了游戏,并且能够很好地恢复交易。所以这让我相信这可能是使用 NSUserDefaults 保存在崩溃设备上的损坏数据的问题?类似于这个问题的东西: iOS - strange crash on in App purchase restore function

这是我的第一个应用程序,我对调试的经验确实很少。如果解决方案本身没有出现,我们将非常欢迎您提供一些有关如何调试此问题的指导。我不知道如何在我的设备上重现一个似乎只有在实时应用上进行 IAP 时才会发生的问题。

【问题讨论】:

你为什么使用dispatch_after?这种事情总是让我的蜘蛛侠感觉刺痛 这个问题我已经有一段时间了,之前在这里询问过。这被建议作为答案,公平地说,它大大减少了崩溃的数量:***.com/questions/34289204/… 可能是 Game Center 身份验证挂起并导致整个应用程序崩溃? "Breadcrumb Trail: (倒序秒数) 14 GC Framework: startAuthenticationForExistingPrimaryPlayer" 【参考方案1】:

我无法告诉你出了什么问题...所有接缝都还好,只是一点评论...也许在您的一种方法中增加一个“守卫”就可以了。 这是你的方法:

public func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) 
    print("Loaded list of products...")
    let products = response.products 
    completionHandler?(success: true, products: products)
    clearRequest()

    // debug printing
    for p in products 
      print("Found product: \(p.productIdentifier) \(p.localizedTitle) \(p.price.floatValue)")
    
  

Apple 建议检查 response.products 是否不为零 if ([response.products count] &gt; 0)... https://developer.apple.com/library/ios/technotes/tn2387/_index.html

所以..有这样的东西会更好..

    func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) 
        if response.products.count != 0 
        .....
        
    

您还可以进行额外检查..如果响应中不是invalidProductIdentifiers

。这种情况更有可能发生在您创建的真实应用程序中 并不时删除 IAP 产品,而您的应用要求 不再存在的产品 ID... 更多内容:http://www.appcoda.com/in-app-purchase-tutorial/

所以..在上面的方法中你也可以添加

func productsRequest(request: SKProductsRequest!, didReceiveResponse response: SKProductsResponse!) 
    ...

    if response.invalidProductIdentifiers.count != 0 
        println(response.invalidProductIdentifiers.description)
     //do something
    

【讨论】:

谢谢托尼,这些链接非常有用。似乎我没有实施您链接的 Apple 文档中详述的几个最佳实践。我会仔细检查并确保我的应用程序符合要求,如果它解决了问题,我会将这个答案标记为正确。

以上是关于应用内购买成功导致部分用户崩溃的主要内容,如果未能解决你的问题,请参考以下文章

应用内购买错误现已修复,但对于以前受影响的用户仍会崩溃

用于自动续订订阅通知的应用内购买

iTunes Connect 中缺少应用内购买部分

tableView reloadData 导致应用在删除部分后崩溃

显示单元部分明智地导致 iOS 应用程序崩溃

iOS 7:应用内购买收据验证和验证