2016-03-08 3 views
1

У меня есть приложение с одним 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.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 на ИПД: 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 в реальном приложении.

+0

Почему вы используете dispatch_after? Такого рода вещи всегда накладывают мое спокойное чувство покалывания – Paulw11

+0

У меня была эта проблема, и я спросил об этом раньше. Это было предложено в качестве ответа, и, чтобы быть справедливым, это резко сократило количество аварий: http://stackoverflow.com/questions/34289204/in-app-purchase-causes-occasional-crash – Eatton

+0

Не может быть, что аутентификация Game Center висит и разбивает вам все приложение? «Тропа кроссвордов: (обратные хронологические секунды) 14 GC Framework: startAuthenticationForExistingPrimaryPlayer« – smoothBlue

ответ

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] > 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, а ваше приложение запрашивает идентификаторы продуктов, которые больше не существуют ... Подробнее здесь: 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 
    } 
} 
+0

Спасибо, Тони, эти ссылки были очень полезны. Кажется, я не внедряю пару лучших практик, подробно описанных в документе Apple, который вы связали. Я пройду через него и удостоверяюсь, что мое приложение соответствует требованиям и пометит этот ответ как правильный, если он решает проблему. – Eatton

Смежные вопросы