Skip to content

Commit 56b37d2

Browse files
authored
Fix container instantiation timing, IID startup. (#4030)
* Fix container instantiation timing, IID startup. The container instantiation could happen at the wrong time, before all components have been added to the container. This fixes it, and also moves IID to use the proper instantiation timing instead of relying on the `configureWithApp:` call. This is part continuation of #3147, where I'll be fixing the rest of the SDKs in follow up PRs. * Typo * Further work on instantiation timing. This moves the instantiation of components to after the FirebaseApp is completely configured and assigned, as it should be. This also exposed a recursive issue with IID calling the Messaging singleton, which in turn called IID and instantiated multiple instances. A change was made to break the cycle: the Messaging `isAutoInitEnabled` call was changed to a static method vs an instance method, allowing IID to call it during initialization without instantiating the Messaging instance (since it doesn't have a direct dependency on it). * Revert unintentional formatting of entire FIRMessaging file. * Fix tests. * Format InstanceID.m * Remove unused import. * Fix ComponentContainer test. * Minor PR feedback.
1 parent 179dff2 commit 56b37d2

File tree

8 files changed

+101
-65
lines changed

8 files changed

+101
-65
lines changed

Example/Core/Tests/FIRComponentContainerTest.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ - (FIRComponentContainer *)containerWithRegistrants:(NSArray<Class> *)registrant
207207
FIRComponentContainer *container = [[FIRComponentContainer alloc] initWithApp:_hostApp
208208
registrants:allRegistrants];
209209
_hostApp.container = container;
210+
211+
// Instantiate all the components that were eagerly registered now that all other properties are
212+
// configured.
213+
[container instantiateEagerComponents];
214+
210215
return container;
211216
}
212217

Example/Messaging/Tests/FIRInstanceIDWithFCMTest.m

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ - (void)tearDown {
5858
}
5959

6060
- (void)testFCMAutoInitEnabled {
61-
NSString *const kFIRMessagingTestsAutoInit = @"com.messaging.test_autoInit";
62-
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingTestsAutoInit];
61+
// Use the standardUserDefaults since that's what IID expects and depends on.
62+
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
6363
FIRMessaging *messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults];
6464
id classMock = OCMClassMock([FIRMessaging class]);
65-
OCMStub([classMock messaging]).andReturn(messaging);
6665
OCMStub([_mockFirebaseApp isDataCollectionDefaultEnabled]).andReturn(YES);
6766
messaging.autoInitEnabled = YES;
6867
XCTAssertTrue(

Example/Messaging/Tests/FIRMessagingTest.m

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ @interface FIRMessaging ()
3737
@property(nonatomic, readwrite, strong) NSData *apnsTokenData;
3838
@property(nonatomic, readwrite, strong) FIRInstanceID *instanceID;
3939

40+
// Expose autoInitEnabled static method for IID.
41+
+ (BOOL)isAutoInitEnabledWithUserDefaults:(NSUserDefaults *)userDefaults;
42+
4043
// Direct Channel Methods
4144
- (void)updateAutomaticClientConnection;
4245
- (BOOL)shouldBeConnectedAutomatically;
@@ -129,6 +132,26 @@ - (void)testAutoInitEnableGlobalDefaultFalse {
129132
[bundleMock stopMocking];
130133
}
131134

135+
- (void)testAutoInitEnabledMatchesStaticMethod {
136+
// Flag is set to YES in user defaults.
137+
NSUserDefaults *defaults = self.messaging.messagingUserDefaults;
138+
[defaults setObject:@YES forKey:kFIRMessagingUserDefaultsKeyAutoInitEnabled];
139+
140+
XCTAssertTrue(self.messaging.isAutoInitEnabled);
141+
XCTAssertEqual(self.messaging.isAutoInitEnabled,
142+
[FIRMessaging isAutoInitEnabledWithUserDefaults:defaults]);
143+
}
144+
145+
- (void)testAutoInitDisabledMatchesStaticMethod {
146+
// Flag is set to NO in user defaults.
147+
NSUserDefaults *defaults = self.messaging.messagingUserDefaults;
148+
[defaults setObject:@NO forKey:kFIRMessagingUserDefaultsKeyAutoInitEnabled];
149+
150+
XCTAssertFalse(self.messaging.isAutoInitEnabled);
151+
XCTAssertEqual(self.messaging.isAutoInitEnabled,
152+
[FIRMessaging isAutoInitEnabledWithUserDefaults:defaults]);
153+
}
154+
132155
#pragma mark - Direct Channel Establishment Testing
133156

134157
#if TARGET_OS_IOS || TARGET_OS_TV

Firebase/Core/FIRApp.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ + (void)configureWithName:(NSString *)name options:(FIROptions *)options {
191191
}
192192

193193
[FIRApp addAppToAppDictionary:app];
194+
195+
// The FIRApp instance is ready to go, `sDefaultApp` is assigned, other SDKs are now ready to be
196+
// instantiated.
197+
[app.container instantiateEagerComponents];
194198
[FIRApp sendNotificationsToSDKs:app];
195199
}
196200
}

Firebase/Core/FIRComponentContainer.m

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ @interface FIRComponentContainer ()
3232
/// Cached instances of components that requested to be cached.
3333
@property(nonatomic, strong) NSMutableDictionary<NSString *, id> *cachedInstances;
3434

35+
/// Protocols of components that have requested to be eagerly instantiated.
36+
@property(nonatomic, strong, nullable) NSMutableArray<Protocol *> *eagerProtocolsToInstantiate;
37+
3538
@end
3639

3740
@implementation FIRComponentContainer
@@ -74,6 +77,9 @@ - (instancetype)initWithApp:(FIRApp *)app registrants:(NSMutableSet<Class> *)all
7477
}
7578

7679
- (void)populateComponentsFromRegisteredClasses:(NSSet<Class> *)classes forApp:(FIRApp *)app {
80+
// Keep track of any components that need to eagerly instantiate after all components are added.
81+
self.eagerProtocolsToInstantiate = [[NSMutableArray alloc] init];
82+
7783
// Loop through the verified component registrants and populate the components array.
7884
for (Class<FIRLibrary> klass in classes) {
7985
// Loop through all the components being registered and store them as appropriate.
@@ -92,24 +98,38 @@ - (void)populateComponentsFromRegisteredClasses:(NSSet<Class> *)classes forApp:(
9298
// Store the creation block for later usage.
9399
self.components[protocolName] = component.creationBlock;
94100

95-
// Instantiate the instance if it has requested to be instantiated.
101+
// Queue any protocols that should be eagerly instantiated. Don't instantiate them yet
102+
// because they could depend on other components that haven't been added to the components
103+
// array yet.
96104
BOOL shouldInstantiateEager =
97105
(component.instantiationTiming == FIRInstantiationTimingAlwaysEager);
98106
BOOL shouldInstantiateDefaultEager =
99107
(component.instantiationTiming == FIRInstantiationTimingEagerInDefaultApp &&
100108
[app isDefaultApp]);
101109
if (shouldInstantiateEager || shouldInstantiateDefaultEager) {
102-
@synchronized(self) {
103-
[self instantiateInstanceForProtocol:component.protocol
104-
withBlock:component.creationBlock];
105-
}
110+
[self.eagerProtocolsToInstantiate addObject:component.protocol];
106111
}
107112
}
108113
}
109114
}
110115

111116
#pragma mark - Instance Creation
112117

118+
- (void)instantiateEagerComponents {
119+
// After all components are registered, instantiate the ones that are requesting eager
120+
// instantiation.
121+
@synchronized(self) {
122+
for (Protocol *protocol in self.eagerProtocolsToInstantiate) {
123+
// Get an instance for the protocol, which will instantiate it since it couldn't have been
124+
// cached yet. Ignore the instance coming back since we don't need it.
125+
__unused id unusedInstance = [self instanceForProtocol:protocol];
126+
}
127+
128+
// All eager instantiation is complete, clear the stored property now.
129+
self.eagerProtocolsToInstantiate = nil;
130+
}
131+
}
132+
113133
/// Instantiate an instance of a class that conforms to the specified protocol.
114134
/// This will:
115135
/// - Call the block to create an instance if possible,

Firebase/Core/Private/FIRComponentContainerInternal.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ NS_ASSUME_NONNULL_BEGIN
3131
/// protocol wasn't registered, or if the instance couldn't instantiate for the provided app.
3232
- (nullable id)instanceForProtocol:(Protocol *)protocol NS_SWIFT_NAME(instance(for:));
3333

34+
/// Instantiates all the components that have registered as "eager" after initialization.
35+
- (void)instantiateEagerComponents;
36+
3437
/// Remove all of the cached instances stored and allow them to clean up after themselves.
3538
- (void)removeAllCachedInstances;
3639

Firebase/InstanceID/FIRInstanceID.m

Lines changed: 26 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#import <FirebaseCore/FIRLibrary.h>
2323
#import <FirebaseCore/FIROptions.h>
2424
#import <GoogleUtilities/GULAppEnvironmentUtil.h>
25+
#import <GoogleUtilities/GULUserDefaults.h>
2526
#import "FIRInstanceID+Private.h"
2627
#import "FIRInstanceIDAuthService.h"
2728
#import "FIRInstanceIDCheckinPreferences.h"
@@ -67,8 +68,8 @@
6768
static NSString *const kAPSEnvironmentDevelopmentValue = @"development";
6869
/// FIRMessaging selector that returns the current FIRMessaging auto init
6970
/// enabled flag.
70-
static NSString *const kFIRInstanceIDFCMSelectorAutoInitEnabled = @"isAutoInitEnabled";
71-
static NSString *const kFIRInstanceIDFCMSelectorInstance = @"messaging";
71+
static NSString *const kFIRInstanceIDFCMSelectorAutoInitEnabled =
72+
@"isAutoInitEnabledWithUserDefaults:";
7273

7374
static NSString *const kFIRInstanceIDAPNSTokenType = @"APNSTokenType";
7475
static NSString *const kFIRIIDAppReadyToConfigureSDKNotification =
@@ -77,8 +78,6 @@
7778
static NSString *const kFIRIIDErrorDomain = @"com.firebase.instanceid";
7879
static NSString *const kFIRIIDServiceInstanceID = @"InstanceID";
7980

80-
static NSInteger const kFIRIIDErrorCodeInstanceIDFailed = -121;
81-
8281
typedef void (^FIRInstanceIDKeyPairHandler)(FIRInstanceIDKeyPair *keyPair, NSError *error);
8382

8483
/**
@@ -639,41 +638,41 @@ + (void)load {
639638
+ (nonnull NSArray<FIRComponent *> *)componentsToRegister {
640639
FIRComponentCreationBlock creationBlock =
641640
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
641+
// InstanceID only works with the default app.
642+
if (!container.app.isDefaultApp) {
643+
// Only configure for the default FIRApp.
644+
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeFIRApp002,
645+
@"Firebase Instance ID only works with the default app.");
646+
return nil;
647+
}
648+
642649
// Ensure it's cached so it returns the same instance every time instanceID is called.
643650
*isCacheable = YES;
644651
FIRInstanceID *instanceID = [[FIRInstanceID alloc] initPrivately];
645652
[instanceID start];
653+
[instanceID configureInstanceIDWithOptions:container.app.options];
646654
return instanceID;
647655
};
648656
FIRComponent *instanceIDProvider =
649657
[FIRComponent componentWithProtocol:@protocol(FIRInstanceIDInstanceProvider)
650-
instantiationTiming:FIRInstantiationTimingLazy
658+
instantiationTiming:FIRInstantiationTimingEagerInDefaultApp
651659
dependencies:@[]
652660
creationBlock:creationBlock];
653661
return @[ instanceIDProvider ];
654662
}
655663

656-
+ (void)configureWithApp:(FIRApp *)app {
657-
if (!app.isDefaultApp) {
658-
// Only configure for the default FIRApp.
659-
FIRInstanceIDLoggerDebug(kFIRInstanceIDMessageCodeFIRApp002,
660-
@"Firebase Instance ID only works with the default app.");
661-
return;
662-
}
663-
[[FIRInstanceID instanceID] configureInstanceIDWithOptions:app.options app:app];
664-
}
665-
666-
- (void)configureInstanceIDWithOptions:(FIROptions *)options app:(FIRApp *)firApp {
664+
- (void)configureInstanceIDWithOptions:(FIROptions *)options {
667665
NSString *GCMSenderID = options.GCMSenderID;
668666
if (!GCMSenderID.length) {
669667
FIRInstanceIDLoggerError(kFIRInstanceIDMessageCodeFIRApp000,
670668
@"Firebase not set up correctly, nil or empty senderID.");
671-
[FIRInstanceID exitWithReason:@"GCM_SENDER_ID must not be nil or empty." forFirebaseApp:firApp];
672-
return;
669+
[NSException raise:kFIRIIDErrorDomain
670+
format:@"Could not configure Firebase InstanceID. GCMSenderID must not be nil or "
671+
@"empty."];
673672
}
674673

675674
self.fcmSenderID = GCMSenderID;
676-
self.firebaseAppID = firApp.options.googleAppID;
675+
self.firebaseAppID = options.googleAppID;
677676

678677
// FCM generates a FCM token during app start for sending push notification to device.
679678
// This is not needed for app extension.
@@ -682,26 +681,6 @@ - (void)configureInstanceIDWithOptions:(FIROptions *)options app:(FIRApp *)firAp
682681
}
683682
}
684683

685-
+ (NSError *)configureErrorWithReason:(nonnull NSString *)reason {
686-
NSString *description =
687-
[NSString stringWithFormat:@"Configuration failed for service %@.", kFIRIIDServiceInstanceID];
688-
if (!reason.length) {
689-
reason = @"Unknown reason";
690-
}
691-
692-
NSDictionary *userInfo =
693-
@{NSLocalizedDescriptionKey : description, NSLocalizedFailureReasonErrorKey : reason};
694-
695-
return [NSError errorWithDomain:kFIRIIDErrorDomain
696-
code:kFIRIIDErrorCodeInstanceIDFailed
697-
userInfo:userInfo];
698-
}
699-
700-
+ (void)exitWithReason:(nonnull NSString *)reason forFirebaseApp:(FIRApp *)firebaseApp {
701-
[NSException raise:kFIRIIDErrorDomain
702-
format:@"Could not configure Firebase InstanceID. %@", reason];
703-
}
704-
705684
// This is used to start any operations when we receive FirebaseSDK setup notification
706685
// from FIRCore.
707686
- (void)didCompleteConfigure {
@@ -738,29 +717,20 @@ - (BOOL)isFCMAutoInitEnabled {
738717
return NO;
739718
}
740719

741-
// Messaging doesn't have the singleton method, auto init should be enabled since FCM exists.
742-
SEL instanceSelector = NSSelectorFromString(kFIRInstanceIDFCMSelectorInstance);
743-
if (![messagingClass respondsToSelector:instanceSelector]) {
744-
return YES;
745-
}
746-
747-
// Get FIRMessaging shared instance.
748-
IMP messagingInstanceIMP = [messagingClass methodForSelector:instanceSelector];
749-
id (*getMessagingInstance)(id, SEL) = (void *)messagingInstanceIMP;
750-
id messagingInstance = getMessagingInstance(messagingClass, instanceSelector);
751-
752-
// Messaging doesn't have the property, auto init should be enabled since FCM exists.
720+
// Messaging doesn't have the class method, auto init should be enabled since FCM exists.
753721
SEL autoInitSelector = NSSelectorFromString(kFIRInstanceIDFCMSelectorAutoInitEnabled);
754-
if (![messagingInstance respondsToSelector:autoInitSelector]) {
722+
if (![messagingClass respondsToSelector:autoInitSelector]) {
755723
return YES;
756724
}
757725

758-
// Get autoInitEnabled method.
759-
IMP isAutoInitEnabledIMP = [messagingInstance methodForSelector:autoInitSelector];
760-
BOOL (*isAutoInitEnabled)(id, SEL) = (BOOL(*)(id, SEL))isAutoInitEnabledIMP;
726+
// Get the autoInitEnabled class method.
727+
IMP isAutoInitEnabledIMP = [messagingClass methodForSelector:autoInitSelector];
728+
BOOL(*isAutoInitEnabled)
729+
(Class, SEL, GULUserDefaults *) = (BOOL(*)(id, SEL, GULUserDefaults *))isAutoInitEnabledIMP;
761730

762731
// Check FCM's isAutoInitEnabled property.
763-
return isAutoInitEnabled(messagingInstance, autoInitSelector);
732+
return isAutoInitEnabled(messagingClass, autoInitSelector,
733+
[GULUserDefaults standardUserDefaults]);
764734
}
765735

766736
// Actually makes InstanceID instantiate both the IID and Token-related subsystems.

Firebase/Messaging/FIRMessaging.m

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,9 +541,21 @@ - (void)setAPNSToken:(NSData *)apnsToken type:(FIRMessagingAPNSTokenType)type {
541541
#pragma mark - FCM
542542

543543
- (BOOL)isAutoInitEnabled {
544+
// Defer to the class method since we're just reading from regular userDefaults and we need to
545+
// read this from IID without instantiating the Messaging singleton.
546+
return [[self class] isAutoInitEnabledWithUserDefaults:_messagingUserDefaults];
547+
}
548+
549+
/// Checks if Messaging auto-init is enabled in the user defaults instance passed in. This is
550+
/// exposed as a class property for IID to fetch the property without instantiating an instance of
551+
/// Messaging. Since Messaging can only be used with the default FIRApp, we can have one point of
552+
/// entry without context of which FIRApp instance is being used.
553+
/// ** THIS METHOD IS DEPENDED ON INTERNALLY BY IID USING REFLECTION. PLEASE DO NOT CHANGE THE
554+
/// SIGNATURE, AS IT WOULD BREAK AUTOINIT FUNCTIONALITY WITHIN IID. **
555+
+ (BOOL)isAutoInitEnabledWithUserDefaults:(GULUserDefaults *)userDefaults {
544556
// Check storage
545557
id isAutoInitEnabledObject =
546-
[_messagingUserDefaults objectForKey:kFIRMessagingUserDefaultsKeyAutoInitEnabled];
558+
[userDefaults objectForKey:kFIRMessagingUserDefaultsKeyAutoInitEnabled];
547559
if (isAutoInitEnabledObject) {
548560
return [isAutoInitEnabledObject boolValue];
549561
}

0 commit comments

Comments
 (0)