1

一个客户给我带来了一个现有的应用程序,我们刚刚发布了一个新版本,包括用于崩溃报告的 Crittercism。

自发布以来,我们收到了大量的崩溃报告,如下所示。我相信崩溃是由 SKProductRequest 的代表过早发布引起的,所以我不是在寻找崩溃发生原因的答案——这已经在 StackOverflow 的其他地方得到了回答。

我的问题是我无法复制错误。我尝试了许多设备和不同版本的 iOS。从 Crittercism 来看,崩溃主要发生在最新的设备上,以及一系列 iPhone、iPod 和 iPad - 所以它不是一种特定类型的设备,但我仍然无法让它发生。我已经下载了精简版,然后从那里购买了完整版——一切都很完美。

因此,我的问题是,有没有人知道如何在我的设备上实现它以便我可以修复它?!

libobjc.A.dylib 0x37393f78 objc_msgSend + 15
StoreKit 0x37bc3a4f -[SKProductsRequest handleFinishResponse:returningError:] + 142
StoreKit 0x37bc4dc7 -[SKRequest _requestFinishedNotification:] + 210
Foundation 0x319624ff __57-[NSNotificationCenter addObserver:selector:name:object:]_block_invoke_0 + 18
CoreFoundation 0x31027547 ___CFXNotificationPost_block_invoke_0 + 70
CoreFoundation 0x30fb3097 _CFXNotificationPost + 1406
Foundation 0x318d63eb -[NSNotificationCenter postNotificationName:object:userInfo:] + 66
AppSupport 0x314eeba3 -[CPDistributedNotificationCenter deliverNotification:userInfo:] + 62
AppSupport 0x314f010b _CPDNDeliverNotification + 290
AppSupport 0x314ee99b _XDeliverNotification + 170
AppSupport 0x314e3b11 migHelperRecievePortCallout + 172
CoreFoundation 0x3102f523 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 38
CoreFoundation 0x3102f4c5 __CFRunLoopDoSource1 + 140
CoreFoundation 0x3102e313 __CFRunLoopRun + 1370
CoreFoundation 0x30fb14a5 CFRunLoopRunSpecific + 300
CoreFoundation 0x30fb136d CFRunLoopRunInMode + 104
GraphicsServices 0x3302b439 GSEventRunModal + 136
UIKit 0x30714cd5 UIApplicationMain + 1080
MyAppLite 0x000fc7c3 main (main.m:13)

罪魁祸首肯定在某个地方,但我仍然无法让它在我的设备或模拟器中崩溃:

#import "InAppPurchaseViewController.h"

#define INDICATOR_Y 150.0f
#define INDICATOR_MOVE_Y 300.0f
#define PRODUCT_LABEL_Y 150.0f
#define PURCHASE_BUTTON_Y 190.0f
#define RESTORE_BUTTON_Y 240.0f

@interface InAppPurchaseViewController (Private)
- (void)updateUIToDefaultState;
@end

@implementation InAppPurchaseViewController

- (void) dealloc {
    [productLabel release];
    [purchaseButton release];
    [indicatorView release];
    [super dealloc];
}

#pragma mark -
#pragma mark UIViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.title = @"...";
    self.view.backgroundColor = [UIColor blackColor];
    self.tableView.backgroundColor = [UIColor blackColor];
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    self.tableView.scrollEnabled = NO;
    self.tableView.indicatorStyle = UIScrollViewIndicatorStyleWhite;

    UIImageView *headerImageView = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"upgrade-header.png"]] autorelease];
    UIView *headerView = [[[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.bounds.size.width, headerImageView.image.size.height + 200.0)] autorelease];
    [headerView addSubview:headerImageView];

    indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
    indicatorView.hidesWhenStopped = YES;
    [indicatorView startAnimating];
    [headerView addSubview:indicatorView];

    productLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 300.0f, 0.0f)];
    productLabel.text = @"...";
    productLabel.hidden = YES;
    productLabel.font = [UIFont systemFontOfSize:18.0f];
    productLabel.textColor = [UIColor whiteColor];
    productLabel.backgroundColor = [UIColor blackColor];
    [productLabel sizeToFit];
    [headerView addSubview:productLabel];

    purchaseButton = [[UIButton buttonWithType:UIButtonTypeCustom] retain];
    purchaseButton.titleLabel.font = [UIFont boldSystemFontOfSize:20.0f];
    UIImage *bgImage = [UIImage imageNamed:@"btn_purchase.png"];
    UIImage *buttonImage = [bgImage stretchableImageWithLeftCapWidth:(bgImage.size.width / 2.0f) - 1 topCapHeight:0.0f];
    [purchaseButton setBackgroundImage:buttonImage forState:UIControlStateNormal];
    [purchaseButton setTitle:@"Purchase" forState:UIControlStateNormal];
    [purchaseButton addTarget:self action:@selector(purchaseClicked:) forControlEvents:UIControlEventTouchUpInside];
    purchaseButton.frame = CGRectMake(0.0f, 0.0f, 300.0f, buttonImage.size.height);
    purchaseButton.hidden = YES;
    [self.view addSubview:purchaseButton];

    restoreButton = [[UIButton buttonWithType:UIButtonTypeCustom] retain];
    restoreButton.enabled = NO;
    restoreButton.titleLabel.font = [UIFont boldSystemFontOfSize:18.0f];
    [restoreButton setBackgroundImage:buttonImage forState:UIControlStateNormal];
    [restoreButton setTitle:@"Restore Purchases" forState:UIControlStateNormal];
    [restoreButton addTarget:self action:@selector(restoreClicked:) forControlEvents:UIControlEventTouchUpInside];
    restoreButton.frame = CGRectMake(0.0f, 0.0f, 300.0f, buttonImage.size.height);
    [self.view addSubview:restoreButton];

    headerImageView.center = headerView.center;
    headerImageView.frame = CGRectMake(headerImageView.frame.origin.x, 0.0f, headerImageView.frame.size.width, headerImageView.frame.size.height);

    indicatorView.center = headerView.center;
    indicatorView.frame = CGRectMake(indicatorView.frame.origin.x, INDICATOR_Y, indicatorView.frame.size.width, indicatorView.frame.size.height);

    productLabel.center = headerView.center;
    productLabel.frame = CGRectMake(productLabel.frame.origin.x, PRODUCT_LABEL_Y, productLabel.frame.size.width, productLabel.frame.size.height);

    purchaseButton.center = headerView.center;
    purchaseButton.frame = CGRectMake(purchaseButton.frame.origin.x, PURCHASE_BUTTON_Y, purchaseButton.frame.size.width, purchaseButton.frame.size.height);

    restoreButton.center = headerView.center;
    restoreButton.frame = CGRectMake(restoreButton.frame.origin.x, RESTORE_BUTTON_Y, restoreButton.frame.size.width, restoreButton.frame.size.height);

    self.tableView.tableHeaderView = headerView;

    [self performSelectorInBackground:@selector(retrieveProductDetails:) withObject:nil];
}

#pragma mark -
#pragma mark AppStoreServiceDelegate

- (void)productDetailsRequestSucceededWithResponse:(SKProductsResponse *)response {
    if (response.products.count == 1) {
        [self performSelectorOnMainThread:@selector(productDetailsRetrieved:) withObject:[response.products objectAtIndex:0] waitUntilDone:NO];    
    } else {
        NSString *message = @"Unable to retrieve product details: No valid product to purchase. Please contact support.";
        [self performSelectorOnMainThread:@selector(appStoreRequestFailed:) withObject:message waitUntilDone:NO];
    }
}

- (void)productDetailsRequestFailedWithError:(NSError *)error {
    NSString *message = [NSString stringWithFormat:@"Unable to retrieve product details: %@", [error localizedDescription]];
    [self performSelectorOnMainThread:@selector(appStoreRequestFailed:) withObject:message waitUntilDone:NO];
}

- (void)transactionSucceededForProductId:(NSString *)productId {
    [self performSelectorOnMainThread:@selector(purchaseCompleted:) withObject:productId waitUntilDone:NO];
}

- (void)transactionFailedWithReason:(NSString *)reason {
    NSString *message = [NSString stringWithFormat:@"Sorry, your purchase could not be completed: %@", reason];
    [self performSelectorOnMainThread:@selector(appStoreRequestFailed:) withObject:message waitUntilDone:NO];
}

- (void)transactionCancelled {
    [self performSelectorOnMainThread:@selector(updateUIToDefaultState) withObject:nil waitUntilDone:NO];
}

#pragma mark -
#pragma mark UIAlertViewDelegate (purchase failed)
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    [self.navigationController popViewControllerAnimated:YES];
}

#pragma mark -
#pragma mark Private button callbacks

- (void)purchaseClicked:(UIButton *)clicked {
    clicked.enabled = NO;
    [indicatorView startAnimating];
    indicatorView.frame = CGRectMake(indicatorView.frame.origin.x, INDICATOR_MOVE_Y, indicatorView.frame.size.width, indicatorView.frame.size.height);
    [self performSelectorInBackground:@selector(purchaseProduct:) withObject:nil];
}

- (void)restoreClicked:(UIButton *)clicked {
    clicked.enabled = NO;
    [indicatorView startAnimating];
    indicatorView.frame = CGRectMake(indicatorView.frame.origin.x, INDICATOR_MOVE_Y, indicatorView.frame.size.width, indicatorView.frame.size.height);
    [self performSelectorInBackground:@selector(restoreProducts:) withObject:nil];
}

#pragma mark -
#pragma mark Private

- (void)showPurchaseDetailsWithName:(NSString *)productName price:(NSString *)price {
    self.title = productName;
    productLabel.text = [NSString stringWithFormat:@"%@, %@", productName, price];
    [productLabel sizeToFit];
    productLabel.center = self.tableView.tableHeaderView.center;
    [self updateUIToDefaultState];
}

- (void)productDetailsRetrieved:(SKProduct *)productDetails {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [indicatorView stopAnimating];
    [self showPurchaseDetailsWithName:productDetails.localizedTitle price:[[AppStoreService sharedAppStoreService] formatPrice:productDetails]];
    [pool drain];
}

- (void)purchaseCompleted:(NSString *)productId  {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [indicatorView stopAnimating];
    [[AppStoreService sharedAppStoreService] setPurchasedFullEdition:YES];
    [self.navigationController popViewControllerAnimated:YES];
    [pool drain];
}

- (void)purchaseProduct:(id)ignored {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[AppStoreService sharedAppStoreService] purchaseProducts:[NSSet setWithObject:[[AppStoreService sharedAppStoreService] inAppProductIdentifierForEdition]] notifyingDelegate:self];
    [pool drain];
}

- (void)restoreProducts:(id)ignored {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[AppStoreService sharedAppStoreService] retoreCompletedTransactionsNotifyingDelegate:self];
    [pool drain];
}

- (void)appStoreRequestFailed:(NSString *)reason {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[[[UIAlertView alloc] initWithTitle:@"Purchase Error" message:reason delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease] show];
    [self updateUIToDefaultState];
    [pool drain];
}

- (void)retrieveProductDetails:(id)ignored {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [[AppStoreService sharedAppStoreService] requestDetailsOfProducts:[NSSet setWithObject:[[AppStoreService sharedAppStoreService] inAppProductIdentifierForEdition]] notifyingDelegate:self];
    [pool drain];
}

- (void)updateUIToDefaultState {
    [indicatorView stopAnimating];
    productLabel.hidden = NO;
    purchaseButton.hidden = NO;
    restoreButton.enabled = YES;
    purchaseButton.enabled = YES;
}

@end

这是 AppStoreService.m

//  AppStoreService.m
    #import "AppStoreService.h"
    #import "SynthesizeSingleton.h"
    #import "DataService.h"
    #import "ConfigService.h"

    #pragma mark -
    #pragma mark Private internal app store delegates

    @implementation AppStoreProductRequestDelegate

    @synthesize delegate = _delegate;

    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
        //NSLog(@"Got response... %@", response);
        [request release];
        if (self.delegate) {
            [self.delegate productDetailsRequestSucceededWithResponse:response];
        }
    }

    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
        //NSLog(@"Got error... %@", error);
        [request release];
        if (self.delegate) {
            [self.delegate productDetailsRequestFailedWithError:error];
        }
    }

    @end

    @implementation AppStoreTransactionObserver

    @synthesize delegate = _delegate;

    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
        for (SKPaymentTransaction *transaction in transactions) {
            switch (transaction.transactionState) {
                case SKPaymentTransactionStatePurchased: { [self completeTransaction:transaction]; break; }
                case SKPaymentTransactionStateFailed: { [self failedTransaction:transaction]; break; }
                case SKPaymentTransactionStateRestored: {[self restoreTransaction:transaction]; break; }
                default: break;
            }
        }
    }

    - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
        if (self.delegate) {
            [self.delegate transactionFailedWithReason:[NSString stringWithFormat:@"Purchase failed: %@", [error localizedDescription]]];
        }
    }

    - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
        //NSLog(@"paymentQueueRestoreCompletedTransactionsFinished:");
    }

    - (void)failedTransaction:(SKPaymentTransaction *)transaction {
        //NSLog(@"failedTransaction: %@", transaction);
        if (self.delegate) {
            if (transaction.error.code == SKErrorPaymentCancelled) {
                [self.delegate transactionCancelled];
            } else {
                [self.delegate transactionFailedWithReason:[NSString stringWithFormat:@"Purchase failed: %@", [transaction.error localizedDescription]]];
            }
        }
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }

    - (void)restoreTransaction:(SKPaymentTransaction *)transaction {
        //NSLog(@"restoreTransaction: %@", transaction);
        if (self.delegate) {
            [self.delegate transactionSucceededForProductId:transaction.originalTransaction.payment.productIdentifier];
        }
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }

    - (void)completeTransaction:(SKPaymentTransaction *)transaction {
        //NSLog(@"completeTransaction: %@", transaction);
        if (self.delegate) {
            [self.delegate transactionSucceededForProductId:transaction.payment.productIdentifier];
        }
        [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    }

    @end

    #pragma mark -
    #pragma mark AppStoreService

    @implementation AppStoreService

    SYNTHESIZE_SINGLETON_FOR_CLASS(AppStoreService);

    static NSString *kLPHasPurchasedFullEdition = @"kLPHasPurchasedFullEdition";

    - (AppStoreService *)init {
        if (self = [super init]) {
            productDetailsDelegate = [[AppStoreProductRequestDelegate alloc] init];
            appStoreObserver = [[AppStoreTransactionObserver alloc] init];
            [[SKPaymentQueue defaultQueue] addTransactionObserver:appStoreObserver];
        }
        return self;
    }

    - (void) dealloc {
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:appStoreObserver];
        [productDetailsDelegate release];
        [appStoreObserver release];
        [super dealloc];
    }

    - (BOOL)hasPurchasedFullEdition {
        return [[NSUserDefaults standardUserDefaults] boolForKey:kLPHasPurchasedFullEdition];
    }

    - (void)setPurchasedFullEdition:(BOOL)purchased {
        //NSLog(@"Purchased? %d", purchased);
        [[NSUserDefaults standardUserDefaults] setBool:purchased forKey:kLPHasPurchasedFullEdition];
        [[ConfigService sharedConfigService] synchronizeConfig];
    }

    - (NSString *)inAppProductIdentifierForEdition {
        if ([[DataService sharedDataService] isLiteEdition]) {
            if ([DataService sharedDataService].isLanguageEdition) {
                // Note. Remove the "-" from language codes, e.g. Brazillian Portugese pt-br, as in-app purchase IDs cannot contain a hyphen.
                NSString *fixedCode = [[DataService sharedDataService].languageCode stringByReplacingOccurrencesOfString:@"-" withString:@""];
                return [NSString stringWithFormat:@"com.myBrokenApp.%@.AllCategories", fixedCode];
            } else {
                return @"com.myBrokenApp.AllCategories";
            }
        } else {
            @throw [NSException exceptionWithName:@"InvalidOperation" reason:@"An in app purchase product ID was requested for a non-lite version" userInfo:[NSDictionary dictionary]];
        }
    }

    - (NSString *)purchasedCategoryIdsWhereClause {
        // flirting & essentials for lite builds
        return ([DataService sharedDataService].isLiteEdition && ![self hasPurchasedFullEdition]) ? @"and c.category_id in (1,19)" : @" ";
    }

    - (NSString *)formatPrice:(SKProduct *)product {
        NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
        [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
        [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
        [numberFormatter setLocale:product.priceLocale];
        NSString *currency = [numberFormatter stringFromNumber:product.price];
        [numberFormatter release];
        return currency;
    }

    - (void)requestDetailsOfProducts:(NSSet *)products notifyingDelegate:(id<AppStoreServiceDelegate>)delegate {
        //NSLog(@"Retrieving details of products: %@", products);
        SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:products];
        productDetailsDelegate.delegate = delegate;
        request.delegate = productDetailsDelegate;
        //NSLog(@"Starting request...");
        [request start];
    }

    - (void)purchaseProducts:(NSSet *)products notifyingDelegate:(id<AppStoreServiceDelegate>)delegate {
        //NSLog(@"Making in app purchase for products: %@", products);
        if ([SKPaymentQueue canMakePayments]) {
            appStoreObserver.delegate = delegate;
            for (NSString *productId in products) {
                [[SKPaymentQueue defaultQueue] addPayment:[SKPayment paymentWithProductIdentifier:productId]];
            }

        } else {
            [delegate transactionFailedWithReason:@"You are not permitted to make purchases."];
        }
    }

    - (void)retoreCompletedTransactionsNotifyingDelegate:(id<AppStoreServiceDelegate>)delegate {
        //NSLog(@"Restoring in-app purchases...");
        if ([SKPaymentQueue canMakePayments]) {
            appStoreObserver.delegate = delegate;
            [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
        } else {
            [delegate transactionFailedWithReason:@"You are not permitted to make purchases."];
        }
    }

    @end
4

2 回答 2

1

Whisk,你在这里发布了一些非常有问题的代码。一方面,您使用 SYNTHESIZE_SINGLETON_FOR_CLASS() 这意味着您的 AppStoreService dealloc 方法将永远不会被调用,因此您的应用程序中存在内存泄漏。此外,productsRequest 中对 [request release] 的调用看起来确实很奇怪。但是,您在使用 SKProductsRequest 时遇到的具体问题似乎与一个委托有关,该委托指向您在 SKProductsRequest 调用回调之前释放的对象之一。这表明当充当委托的对象调用其 dealloc 方法时,您需要将委托设置为 nil。至于如何重现该问题,这听起来很棘手,因为您在代码中设置了非常奇怪的对象生命周期。我建议您使用自动回归测试工具,但我不确定由于您使用单例,它甚至可以检查您的应用程序中的对象生命周期。看单身人士有什么不好?

于 2013-06-19T22:59:52.117 回答
1

我认为您的理论“过早发布 SKProductRequest 的代表”可能是正确的。您是否尝试过在购买过程中退出/终止应用程序?(如果沙盒服务器速度太快(哈哈!那一天),请尝试在事务完成之前插入一些断点并手动终止应用程序。)当您的应用程序再次启动并最终向 StoreKit 注册一个对象作为事务时委托,它将从以前的运行中获得开放的事务,这发现了几个设计错误并帮助我们修复了围绕 StoreKit 事务完成的一类错误。

于 2013-06-19T23:50:00.340 回答