diff --git a/CHANGELOG.md b/CHANGELOG.md index abda76b..4aca920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# Unreleased +- [fixed] `GULAppEnvironmentUtil` now computes `isAppStoreReceiptSandbox` and + `isFromAppStore` once per process and caches the results. Previously every + call read `-[NSBundle appStoreReceiptURL]`, which makes a synchronous XPC call + to the LaunchServices daemon and could block the calling thread, often the + main thread, for seconds. (firebase/firebase-ios-sdk#16726) + # 8.1.4 - [fixed] `GULMutableDictionary` now ignores a `nil` key (and a `nil` object in `setObject:forKey:`) instead of throwing on its internal queue, which crashed diff --git a/GoogleUtilities/Environment/GULAppEnvironmentUtil.m b/GoogleUtilities/Environment/GULAppEnvironmentUtil.m index 5823d24..2dc077f 100644 --- a/GoogleUtilities/Environment/GULAppEnvironmentUtil.m +++ b/GoogleUtilities/Environment/GULAppEnvironmentUtil.m @@ -64,14 +64,7 @@ static BOOL HasEmbeddedMobileProvision(void) { #endif } -+ (BOOL)isFromAppStore { - static dispatch_once_t isEncryptedOnce; - static BOOL isEncrypted = NO; - - dispatch_once(&isEncryptedOnce, ^{ - isEncrypted = IsAppEncrypted(); - }); - +static BOOL IsFromAppStore(void) { if ([GULAppEnvironmentUtil isSimulator]) { return NO; } @@ -93,10 +86,10 @@ static BOOL HasEmbeddedMobileProvision(void) { // the iTunesMetadata.plist outside of the sandbox will be rejected by Apple. // If the app does not contain the embedded.mobileprovision which is stripped out by Apple when // the app is submitted to store, then it is highly likely that it is from Apple Store. - return isEncrypted && !HasEmbeddedMobileProvision(); + return IsAppEncrypted() && !HasEmbeddedMobileProvision(); } -+ (BOOL)isAppStoreReceiptSandbox { +static BOOL IsAppStoreReceiptSandbox(void) { // Since checking the App Store's receipt URL can be memory intensive, check the option in the // Info.plist if developers opted out of this check. id enableSandboxCheck = @@ -111,6 +104,34 @@ static BOOL HasEmbeddedMobileProvision(void) { return [appStoreReceiptFileName isEqualToString:kFIRAIdentitySandboxReceiptFileName]; } ++ (BOOL)isFromAppStore { + // The inputs (install source and bundle contents) can't change while the app is running, so + // compute the result once. + static dispatch_once_t isFromAppStoreOnce; + static BOOL isFromAppStore = NO; + + dispatch_once(&isFromAppStoreOnce, ^{ + isFromAppStore = IsFromAppStore(); + }); + + return isFromAppStore; +} + ++ (BOOL)isAppStoreReceiptSandbox { + // `-[NSBundle appStoreReceiptURL]` makes a synchronous XPC call to the LaunchServices daemon, + // which can block the calling thread for seconds when the daemon is busy. The receipt can't + // change while the app is running, so compute the result once. + // See https://github.com/firebase/firebase-ios-sdk/issues/16726. + static dispatch_once_t isAppStoreReceiptSandboxOnce; + static BOOL isAppStoreReceiptSandbox = NO; + + dispatch_once(&isAppStoreReceiptSandboxOnce, ^{ + isAppStoreReceiptSandbox = IsAppStoreReceiptSandbox(); + }); + + return isAppStoreReceiptSandbox; +} + + (BOOL)isSimulator { #if TARGET_OS_SIMULATOR return YES; diff --git a/GoogleUtilities/Tests/Unit/Environment/GULAppEnvironmentUtilTest.m b/GoogleUtilities/Tests/Unit/Environment/GULAppEnvironmentUtilTest.m index 2a576c7..f49395e 100644 --- a/GoogleUtilities/Tests/Unit/Environment/GULAppEnvironmentUtilTest.m +++ b/GoogleUtilities/Tests/Unit/Environment/GULAppEnvironmentUtilTest.m @@ -20,6 +20,8 @@ #endif #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULAppEnvironmentUtil.h" +#import "GoogleUtilities/MethodSwizzler/Public/GoogleUtilities/GULSwizzler.h" +#import "GoogleUtilities/SwizzlerTestHelpers/Public/GoogleUtilities/GULSwizzler+Unswizzle.h" @interface GULAppEnvironmentUtil (Tests) + (void)setProcessInfoForTest:(NSProcessInfo *)processInfo; @@ -159,4 +161,35 @@ XCTAssertEqualObjects([GULAppEnvironmentUtil appleDevicePlatform], expectedPlatform); } +- (void)testAppStoreReceiptURLIsReadOnlyOnce { + // Warm up the cached values. + BOOL isAppStoreReceiptSandbox = [GULAppEnvironmentUtil isAppStoreReceiptSandbox]; + BOOL isFromAppStore = [GULAppEnvironmentUtil isFromAppStore]; + + __block NSInteger receiptURLReadCount = 0; + [GULSwizzler swizzleClass:[NSBundle class] + selector:@selector(appStoreReceiptURL) + isClassSelector:NO + withBlock:^NSURL *(id bundle) { + receiptURLReadCount++; + return nil; + }]; + + // Sanity check that reads of the receipt URL are intercepted. + XCTAssertNil([NSBundle mainBundle].appStoreReceiptURL); + XCTAssertEqual(receiptURLReadCount, 1); + receiptURLReadCount = 0; + + // `-[NSBundle appStoreReceiptURL]` makes a synchronous XPC call that can block for seconds, so + // later calls must return the cached values without reading it again. + XCTAssertEqual([GULAppEnvironmentUtil isAppStoreReceiptSandbox], isAppStoreReceiptSandbox); + XCTAssertEqual([GULAppEnvironmentUtil isFromAppStore], isFromAppStore); + + [GULSwizzler unswizzleClass:[NSBundle class] + selector:@selector(appStoreReceiptURL) + isClassSelector:NO]; + + XCTAssertEqual(receiptURLReadCount, 0); +} + @end