Skip to content

Commit a491c1e

Browse files
Added automatic PFObject subclass registration.
This will scan all loaded code bundles for classes which inherit from `PFObject`, and register them upon Parse initialization. Still have opt-in support for manual-only registration, though it shouldn't be necessary for most cases.
1 parent 56cc254 commit a491c1e

17 files changed

+135
-125
lines changed

Parse/Internal/Object/Subclassing/PFObjectSubclassingController.h

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,12 @@
1414

1515
@interface PFObjectSubclassingController : NSObject
1616

17-
///--------------------------------------
18-
#pragma mark - Init
19-
///--------------------------------------
20-
21-
//TODO: (nlutsenko, richardross) Make it not terrible aka don't have singletons.
22-
+ (instancetype)defaultController;
23-
+ (void)clearDefaultController;
24-
2517
///--------------------------------------
2618
#pragma mark - Registration
2719
///--------------------------------------
2820

21+
- (void)scanForUnregisteredSubclasses:(BOOL)shouldSubscribe;
22+
2923
- (Class<PFSubclassing>)subclassForParseClassName:(NSString *)parseClassName;
3024
- (void)registerSubclass:(Class<PFSubclassing>)kls;
3125
- (void)unregisterSubclass:(Class<PFSubclassing>)kls;

Parse/Internal/Object/Subclassing/PFObjectSubclassingController.m

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#import "PFAssert.h"
1818
#import "PFMacros.h"
19+
#import "PFObject.h"
20+
#import "PFObject+Subclass.h"
1921
#import "PFObjectSubclassInfo.h"
2022
#import "PFPropertyInfo_Private.h"
2123
#import "PFPropertyInfo_Runtime.h"
@@ -80,10 +82,9 @@ @implementation PFObjectSubclassingController {
8082
dispatch_queue_t _registeredSubclassesAccessQueue;
8183
NSMutableDictionary *_registeredSubclasses;
8284
NSMutableDictionary *_unregisteredSubclasses;
85+
id<NSObject> _bundleLoadedSubscriptionToken;
8386
}
8487

85-
static PFObjectSubclassingController *defaultController_;
86-
8788
///--------------------------------------
8889
#pragma mark - Init
8990
///--------------------------------------
@@ -99,15 +100,10 @@ - (instancetype)init {
99100
return self;
100101
}
101102

102-
+ (instancetype)defaultController {
103-
if (!defaultController_) {
104-
defaultController_ = [[PFObjectSubclassingController alloc] init];
105-
}
106-
return defaultController_;
107-
}
108-
109-
+ (void)clearDefaultController {
110-
defaultController_ = nil;
103+
- (void)dealloc {
104+
[[NSNotificationCenter defaultCenter] removeObserver:_bundleLoadedSubscriptionToken
105+
name:NSBundleDidLoadNotification
106+
object:nil];
111107
}
112108

113109
///--------------------------------------
@@ -122,6 +118,33 @@ + (void)clearDefaultController {
122118
return result;
123119
}
124120

121+
- (void)scanForUnregisteredSubclasses:(BOOL)shouldSubscribe {
122+
// NOTE: Potential race-condition here - if another thread dynamically loads a bundle, we may end up accidentally
123+
// Skipping a bundle. Not entirely sure of the best solution to that here.
124+
if (shouldSubscribe && _bundleLoadedSubscriptionToken == nil) {
125+
@weakify(self);
126+
_bundleLoadedSubscriptionToken = [[NSNotificationCenter defaultCenter] addObserverForName:NSBundleDidLoadNotification
127+
object:nil
128+
queue:nil
129+
usingBlock:^(NSNotification *note) {
130+
@strongify(self);
131+
[self _registerSubclassesInBundle:note.object];
132+
}];
133+
}
134+
NSArray *bundles = [[NSBundle allFrameworks] arrayByAddingObjectsFromArray:[NSBundle allBundles]];
135+
for (NSBundle *bundle in bundles) {
136+
// Skip bundles that aren't loaded yet.
137+
if (!bundle.loaded || !bundle.executablePath) {
138+
continue;
139+
}
140+
// Filter out any system bundles
141+
if ([bundle.bundlePath hasPrefix:@"/System/"] || [bundle.bundlePath hasPrefix:@"/Library/"]) {
142+
continue;
143+
}
144+
[self _registerSubclassesInBundle:bundle];
145+
}
146+
}
147+
125148
- (void)registerSubclass:(Class<PFSubclassing>)kls {
126149
pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{
127150
[self _rawRegisterSubclass:kls];
@@ -315,4 +338,47 @@ - (void)_rawRegisterSubclass:(Class)kls {
315338
_registeredSubclasses[[kls parseClassName]] = subclassInfo;
316339
}
317340

341+
- (void)_registerSubclassesInBundle:(NSBundle *)bundle {
342+
PFConsistencyAssert(bundle.loaded, @"Cannot register subclasses in a bundle that hasn't been loaded!");
343+
dispatch_sync(_registeredSubclassesAccessQueue, ^{
344+
Class pfObjectClass = [PFObject class];
345+
unsigned bundleClassCount = 0;
346+
347+
// NSBundle's executablePath does not resolve symlinks (sadface). Unfortunately, objc_copyClassNamesForImage()
348+
// Only takes absolute paths, as it does a direct string compare. This causes issues when using a framework
349+
// which uses the `Versions/XXX` format, vs. just having the binary be at the root of the `.framework` bundle.
350+
// However, we cannot use stringByResolvingSymlinksInPath for some reason here. On iOS, it never resolves the first,
351+
// symlink in the path, e.g. /var to /private/var.
352+
// Luckily, the POSIX function `realpath` will expand that symlink, and give us the path we need.
353+
char pathBuf[PATH_MAX + 1] = { 0 };
354+
if (!realpath(bundle.executablePath.UTF8String, pathBuf)) {
355+
return;
356+
}
357+
358+
const char **classNames = objc_copyClassNamesForImage(pathBuf, &bundleClassCount);
359+
for (unsigned i = 0; i < bundleClassCount; i++) {
360+
Class bundleClass = objc_getClass(classNames[i]);
361+
// For obvious reasons, don't register the PFObject class.
362+
if (bundleClass == pfObjectClass) {
363+
continue;
364+
}
365+
// NOTE: (richardross) Cannot use isSubclassOfClass here. Some classes may be part of a system bundle (even
366+
// though we attempt to filter those out) that may be an internal class which doesn't inherit from NSObject.
367+
// Scary, I know!
368+
for (Class kls = bundleClass; kls != nil; kls = class_getSuperclass(kls)) {
369+
if (kls == pfObjectClass) {
370+
// Do class_conformsToProtocol as late in the checking as possible, as its SUUUPER slow.
371+
// Behind the scenes this is a strcmp (lolwut?)
372+
if (class_conformsToProtocol(bundleClass, @protocol(PFSubclassing)) &&
373+
!class_conformsToProtocol(bundleClass, @protocol(PFSubclassingSkipAutomaticRegistration))) {
374+
[self _rawRegisterSubclass:bundleClass];
375+
}
376+
break;
377+
}
378+
}
379+
}
380+
free(classNames);
381+
});
382+
}
383+
318384
@end

Parse/Internal/PFCoreDataProvider.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ NS_ASSUME_NONNULL_BEGIN
3636

3737
@end
3838

39+
@class PFObjectSubclassingController;
40+
41+
@protocol PFObjectSubclassingControllerProvider <NSObject>
42+
43+
@property (null_resettable, nonatomic, strong) PFObjectSubclassingController *objectSubclassingController;
44+
45+
@end
46+
3947
@class PFObjectBatchController;
4048

4149
@protocol PFObjectBatchController <NSObject>

Parse/Internal/PFCoreManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ PFPersistenceControllerProvider>
4242
<PFLocationManagerProvider,
4343
PFDefaultACLControllerProvider,
4444
PFObjectControllerProvider,
45+
PFObjectSubclassingControllerProvider,
4546
PFObjectBatchController,
4647
PFObjectFilePersistenceControllerProvider,
4748
PFPinningObjectStoreProvider,

Parse/Internal/PFCoreManager.m

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ @implementation PFCoreManager
5353
@synthesize cloudCodeController = _cloudCodeController;
5454
@synthesize configController = _configController;
5555
@synthesize objectController = _objectController;
56+
@synthesize objectSubclassingController = _objectSubclassingController;
5657
@synthesize objectBatchController = _objectBatchController;
5758
@synthesize objectFilePersistenceController = _objectFilePersistenceController;
5859
@synthesize objectLocalIdStore = _objectLocalIdStore;
@@ -235,6 +236,28 @@ - (void)setObjectController:(PFObjectController *)controller {
235236
});
236237
}
237238

239+
///--------------------------------------
240+
#pragma mark - ObjectSubclassingController
241+
///--------------------------------------
242+
243+
- (PFObjectSubclassingController *)objectSubclassingController {
244+
__block PFObjectSubclassingController *controller = nil;
245+
dispatch_sync(_controllerAccessQueue, ^{
246+
if (!_objectSubclassingController) {
247+
_objectSubclassingController = [[PFObjectSubclassingController alloc] init];
248+
[_objectSubclassingController scanForUnregisteredSubclasses:YES];
249+
}
250+
controller = _objectSubclassingController;
251+
});
252+
return controller;
253+
}
254+
255+
- (void)setObjectSubclassingController:(PFObjectSubclassingController *)objectSubclassingController {
256+
dispatch_sync(_controllerAccessQueue, ^{
257+
_objectSubclassingController = objectSubclassingController;
258+
});
259+
}
260+
238261
///--------------------------------------
239262
#pragma mark - ObjectBatchController
240263
///--------------------------------------

Parse/PFObject+Subclass.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,15 @@ NS_ASSUME_NONNULL_BEGIN
132132

133133
@end
134134

135+
/*!
136+
This protocol exists ONLY so that, if you absolutely need it, you can perform manual subclass registration
137+
via `[Subclass registerSubclass]`. Note that any calls to `registerSubclass` must happen after parse has been
138+
initialized already. This should only ever be needed in the scenario where you may be dynamically creation new
139+
Objective-C classes for parse objects, or you are doing conditional subclass registration (e.g. only register class A
140+
if config setting 'foo' is defined, otherwise register B).
141+
*/
142+
@protocol PFSubclassingSkipAutomaticRegistration <PFSubclassing>
143+
144+
@end
145+
135146
NS_ASSUME_NONNULL_END

Parse/PFObject.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2454,7 +2454,7 @@ + (PFCurrentUserController *)currentUserController {
24542454
}
24552455

24562456
+ (PFObjectSubclassingController *)subclassingController {
2457-
return [PFObjectSubclassingController defaultController];
2457+
return [Parse _currentManager].coreManager.objectSubclassingController;
24582458
}
24592459

24602460
@end

Parse/Parse.m

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -80,22 +80,6 @@ + (void)initializeWithConfiguration:(ParseClientConfiguration *)configuration {
8080

8181
currentParseManager_ = manager;
8282

83-
PFObjectSubclassingController *subclassingController = [PFObjectSubclassingController defaultController];
84-
// Register built-in subclasses of PFObject so they get used.
85-
// We're forced to register subclasses directly this way, in order to prevent a deadlock.
86-
// If we ever switch to bundle scanning, this code can go away.
87-
[subclassingController registerSubclass:[PFUser class]];
88-
[subclassingController registerSubclass:[PFSession class]];
89-
[subclassingController registerSubclass:[PFRole class]];
90-
[subclassingController registerSubclass:[PFPin class]];
91-
[subclassingController registerSubclass:[PFEventuallyPin class]];
92-
#if !TARGET_OS_WATCH && !TARGET_OS_TV
93-
[subclassingController registerSubclass:[PFInstallation class]];
94-
#endif
95-
#if TARGET_OS_IOS || TARGET_OS_TV
96-
[subclassingController registerSubclass:[PFProduct class]];
97-
#endif
98-
9983
#if TARGET_OS_IOS
10084
[PFNetworkActivityIndicatorManager sharedManager].enabled = YES;
10185
#endif

Tests/Other/TestCases/UnitTestCase/PFUnitTestCase.m

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ - (void)setUp {
4242
- (void)tearDown {
4343
[[Parse _currentManager] clearEventuallyQueue];
4444
[Parse _clearCurrentManager];
45-
[PFObjectSubclassingController clearDefaultController];
4645

4746
[super tearDown];
4847
}

Tests/Unit/ACLTests.m

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
#import "PFMacros.h"
1414
#import "PFObjectPrivate.h"
1515
#import "PFRole.h"
16-
#import "PFTestCase.h"
16+
#import "PFUnitTestCase.h"
1717
#import "PFUserPrivate.h"
1818

19-
@interface ACLTests : PFTestCase
19+
@interface ACLTests : PFUnitTestCase
2020

2121
@end
2222

@@ -210,8 +210,6 @@ - (void)testUnsharedCopy {
210210

211211

212212
- (void)testACLRequiresObjectId {
213-
[PFUser registerSubclass];
214-
215213
PFACL *acl = [PFACL ACL];
216214
#pragma clang diagnostic push
217215
#pragma clang diagnostic ignored "-Wnonnull"

Tests/Unit/ObjectSubclassPropertiesTests.m

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,6 @@ @interface ObjectSubclassPropertiesTests : PFUnitTestCase
105105

106106
@implementation ObjectSubclassPropertiesTests
107107

108-
///--------------------------------------
109-
#pragma mark - XCTestCase
110-
///--------------------------------------
111-
112-
- (void)setUp {
113-
[super setUp];
114-
115-
[PFTestObject registerSubclass];
116-
}
117-
118-
- (void)tearDown {
119-
[PFObject unregisterSubclass:[PFTestObject class]];
120-
121-
[super tearDown];
122-
}
123-
124108
///--------------------------------------
125109
#pragma mark - Tests
126110
///--------------------------------------

Tests/Unit/ObjectSubclassTests.m

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
#pragma mark - Helpers
1818
///--------------------------------------
1919

20-
@interface TheFlash : PFObject<PFSubclassing> {
20+
@interface TheFlash : PFObject<PFSubclassingSkipAutomaticRegistration> {
2121
NSString *flashName;
2222
}
2323

@@ -59,7 +59,7 @@ + (NSString *)parseClassName {
5959

6060
@end
6161

62-
@interface ClassWithDirtyingConstructor : PFObject<PFSubclassing>
62+
@interface ClassWithDirtyingConstructor : PFObject<PFSubclassingSkipAutomaticRegistration>
6363
@end
6464

6565
@implementation ClassWithDirtyingConstructor
@@ -85,7 +85,7 @@ @interface UtilityClass : PFObject
8585
@implementation UtilityClass
8686
@end
8787

88-
@interface DescendantOfUtility : UtilityClass<PFSubclassing>
88+
@interface DescendantOfUtility : UtilityClass<PFSubclassingSkipAutomaticRegistration>
8989
@end
9090

9191
@implementation DescendantOfUtility
@@ -94,7 +94,7 @@ + (NSString *)parseClassName {
9494
}
9595
@end
9696

97-
@interface StateClass : PFObject<PFSubclassing>
97+
@interface StateClass : PFObject<PFSubclassing, PFSubclassingSkipAutomaticRegistration>
9898

9999
@property (nonatomic, copy) NSString *state;
100100

@@ -120,17 +120,6 @@ @interface ObjectSubclassTests : PFUnitTestCase
120120

121121
@implementation ObjectSubclassTests
122122

123-
///--------------------------------------
124-
#pragma mark - XCTestCase
125-
///--------------------------------------
126-
127-
- (void)tearDown {
128-
[PFObject unregisterSubclass:[TheFlash class]];
129-
[PFObject unregisterSubclass:[BarryAllen class]];
130-
131-
[super tearDown];
132-
}
133-
134123
///--------------------------------------
135124
#pragma mark - Tests
136125
///--------------------------------------
@@ -173,18 +162,6 @@ - (void)testSubclassesCanInheritUtilityClassesWithoutParseClassName {
173162
[DescendantOfUtility registerSubclass];
174163
}
175164

176-
- (void)testSubclassRegistrationBeforeInitializingParse {
177-
[[Parse _currentManager] clearEventuallyQueue];
178-
[Parse _clearCurrentManager];
179-
180-
[TheFlash registerSubclass];
181-
182-
[Parse setApplicationId:@"a" clientKey:@"b"];
183-
184-
PFObject *theFlash = [PFObject objectWithClassName:@"Person"];
185-
PFAssertIsKindOfClass(theFlash, [TheFlash class]);
186-
}
187-
188165
- (void)testStateIsSubclassable {
189166
[StateClass registerSubclass];
190167
StateClass *stateClass = [StateClass object];

Tests/Unit/ObjectSubclassingControllerTests.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
#import "PFUnitTestCase.h"
1717
#import "ParseUnitTests-Swift.h"
1818

19-
@interface TestSubclass : PFObject<PFSubclassing>
19+
@interface TestSubclass : PFObject<PFSubclassingSkipAutomaticRegistration>
2020
@end
2121

22-
@interface NotSubclass : PFObject<PFSubclassing>
22+
@interface NotSubclass : PFObject<PFSubclassingSkipAutomaticRegistration>
2323
@end
2424

25-
@interface PropertySubclass : PFObject<PFSubclassing> {
25+
@interface PropertySubclass : PFObject<PFSubclassingSkipAutomaticRegistration> {
2626
@public
2727
id _ivarProperty;
2828
}

0 commit comments

Comments
 (0)