Added automatic PFObject subclass registration. (#967)

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.
This commit is contained in:
Richard Ross
2016-07-12 17:45:50 -07:00
committed by GitHub
parent 71553226c3
commit b5beb7c3ab
18 changed files with 149 additions and 145 deletions

View File

@@ -14,18 +14,12 @@
@interface PFObjectSubclassingController : NSObject
///--------------------------------------
#pragma mark - Init
///--------------------------------------
//TODO: (nlutsenko, richardross) Make it not terrible aka don't have singletons.
+ (instancetype)defaultController;
+ (void)clearDefaultController;
///--------------------------------------
#pragma mark - Registration
///--------------------------------------
- (void)scanForUnregisteredSubclasses:(BOOL)shouldSubscribe;
- (Class<PFSubclassing>)subclassForParseClassName:(NSString *)parseClassName;
- (void)registerSubclass:(Class<PFSubclassing>)kls;
- (void)unregisterSubclass:(Class<PFSubclassing>)kls;

View File

@@ -16,6 +16,8 @@
#import "PFAssert.h"
#import "PFMacros.h"
#import "PFObject.h"
#import "PFObject+Subclass.h"
#import "PFObjectSubclassInfo.h"
#import "PFPropertyInfo_Private.h"
#import "PFPropertyInfo_Runtime.h"
@@ -80,10 +82,9 @@ static NSNumber *PFNumberCreateSafe(const char *typeEncoding, const void *bytes)
dispatch_queue_t _registeredSubclassesAccessQueue;
NSMutableDictionary *_registeredSubclasses;
NSMutableDictionary *_unregisteredSubclasses;
id<NSObject> _bundleLoadedSubscriptionToken;
}
static PFObjectSubclassingController *defaultController_;
///--------------------------------------
#pragma mark - Init
///--------------------------------------
@@ -99,15 +100,10 @@ static PFObjectSubclassingController *defaultController_;
return self;
}
+ (instancetype)defaultController {
if (!defaultController_) {
defaultController_ = [[PFObjectSubclassingController alloc] init];
}
return defaultController_;
}
+ (void)clearDefaultController {
defaultController_ = nil;
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:_bundleLoadedSubscriptionToken
name:NSBundleDidLoadNotification
object:nil];
}
///--------------------------------------
@@ -122,6 +118,33 @@ static PFObjectSubclassingController *defaultController_;
return result;
}
- (void)scanForUnregisteredSubclasses:(BOOL)shouldSubscribe {
// NOTE: Potential race-condition here - if another thread dynamically loads a bundle, we may end up accidentally
// Skipping a bundle. Not entirely sure of the best solution to that here.
if (shouldSubscribe && _bundleLoadedSubscriptionToken == nil) {
@weakify(self);
_bundleLoadedSubscriptionToken = [[NSNotificationCenter defaultCenter] addObserverForName:NSBundleDidLoadNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
@strongify(self);
[self _registerSubclassesInBundle:note.object];
}];
}
NSArray *bundles = [[NSBundle allFrameworks] arrayByAddingObjectsFromArray:[NSBundle allBundles]];
for (NSBundle *bundle in bundles) {
// Skip bundles that aren't loaded yet.
if (!bundle.loaded || !bundle.executablePath) {
continue;
}
// Filter out any system bundles
if ([bundle.bundlePath hasPrefix:@"/System/"] || [bundle.bundlePath hasPrefix:@"/Library/"]) {
continue;
}
[self _registerSubclassesInBundle:bundle];
}
}
- (void)registerSubclass:(Class<PFSubclassing>)kls {
pf_sync_with_throw(_registeredSubclassesAccessQueue, ^{
[self _rawRegisterSubclass:kls];
@@ -315,4 +338,59 @@ static PFObjectSubclassingController *defaultController_;
_registeredSubclasses[[kls parseClassName]] = subclassInfo;
}
- (void)_registerSubclassesInBundle:(NSBundle *)bundle {
PFConsistencyAssert(bundle.loaded, @"Cannot register subclasses in a bundle that hasn't been loaded!");
dispatch_sync(_registeredSubclassesAccessQueue, ^{
Class pfObjectClass = [PFObject class];
// There are two different paths that we will need to check for the bundle, depending on the platform.
// - First, we need to check the raw executable path fom the bundle.
// This should be valid for most frameworks on macOS, and iOS/watchOS/tvOS simulators.
// - Second, we need to check the symlink resolved path - including /private/var on iOS.
// This should be valid for iOS, watchOS, and tvOS devices.
// In case there are other platforms that require checking multiple paths that we add support for,
// just use a simple array here.
char potentialPaths[2][PATH_MAX] = { };
strncpy(potentialPaths[0], bundle.executablePath.UTF8String, PATH_MAX);
realpath(potentialPaths[0], potentialPaths[1]);
const char **classNames = NULL;
unsigned bundleClassCount = 0;
for (int i = 0; i < sizeof(potentialPaths) / sizeof(*potentialPaths); i++) {
classNames = objc_copyClassNamesForImage(potentialPaths[i], &bundleClassCount);
if (bundleClassCount) {
break;
}
free(classNames);
classNames = NULL;
}
for (unsigned i = 0; i < bundleClassCount; i++) {
Class bundleClass = objc_getClass(classNames[i]);
// For obvious reasons, don't register the PFObject class.
if (bundleClass == pfObjectClass) {
continue;
}
// NOTE: Cannot use isSubclassOfClass here. Some classes may be part of a system bundle (even
// though we attempt to filter those out) that may be an internal class which doesn't inherit from NSObject.
// Scary, I know!
for (Class kls = bundleClass; kls != nil; kls = class_getSuperclass(kls)) {
if (kls == pfObjectClass) {
// Do class_conformsToProtocol as late in the checking as possible, as its SUUUPER slow.
// Behind the scenes this is a strcmp (lolwut?)
if (class_conformsToProtocol(bundleClass, @protocol(PFSubclassing)) &&
!class_conformsToProtocol(bundleClass, @protocol(PFSubclassingSkipAutomaticRegistration))) {
[self _rawRegisterSubclass:bundleClass];
}
break;
}
}
}
free(classNames);
});
}
@end

View File

@@ -36,6 +36,14 @@ NS_ASSUME_NONNULL_BEGIN
@end
@class PFObjectSubclassingController;
@protocol PFObjectSubclassingControllerProvider <NSObject>
@property (null_resettable, nonatomic, strong) PFObjectSubclassingController *objectSubclassingController;
@end
@class PFObjectBatchController;
@protocol PFObjectBatchController <NSObject>

View File

@@ -42,6 +42,7 @@ PFPersistenceControllerProvider>
<PFLocationManagerProvider,
PFDefaultACLControllerProvider,
PFObjectControllerProvider,
PFObjectSubclassingControllerProvider,
PFObjectBatchController,
PFObjectFilePersistenceControllerProvider,
PFPinningObjectStoreProvider,

View File

@@ -53,6 +53,7 @@
@synthesize cloudCodeController = _cloudCodeController;
@synthesize configController = _configController;
@synthesize objectController = _objectController;
@synthesize objectSubclassingController = _objectSubclassingController;
@synthesize objectBatchController = _objectBatchController;
@synthesize objectFilePersistenceController = _objectFilePersistenceController;
@synthesize objectLocalIdStore = _objectLocalIdStore;
@@ -235,6 +236,28 @@
});
}
///--------------------------------------
#pragma mark - ObjectSubclassingController
///--------------------------------------
- (PFObjectSubclassingController *)objectSubclassingController {
__block PFObjectSubclassingController *controller = nil;
dispatch_sync(_controllerAccessQueue, ^{
if (!_objectSubclassingController) {
_objectSubclassingController = [[PFObjectSubclassingController alloc] init];
[_objectSubclassingController scanForUnregisteredSubclasses:YES];
}
controller = _objectSubclassingController;
});
return controller;
}
- (void)setObjectSubclassingController:(PFObjectSubclassingController *)objectSubclassingController {
dispatch_sync(_controllerAccessQueue, ^{
_objectSubclassingController = objectSubclassingController;
});
}
///--------------------------------------
#pragma mark - ObjectBatchController
///--------------------------------------

View File

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

View File

@@ -2454,7 +2454,7 @@ static void PFObjectAssertValueIsKindOfValidClass(id object) {
}
+ (PFObjectSubclassingController *)subclassingController {
return [PFObjectSubclassingController defaultController];
return [Parse _currentManager].coreManager.objectSubclassingController;
}
@end

View File

@@ -80,22 +80,6 @@ static ParseClientConfiguration *currentParseConfiguration_;
currentParseManager_ = manager;
PFObjectSubclassingController *subclassingController = [PFObjectSubclassingController defaultController];
// Register built-in subclasses of PFObject so they get used.
// We're forced to register subclasses directly this way, in order to prevent a deadlock.
// If we ever switch to bundle scanning, this code can go away.
[subclassingController registerSubclass:[PFUser class]];
[subclassingController registerSubclass:[PFSession class]];
[subclassingController registerSubclass:[PFRole class]];
[subclassingController registerSubclass:[PFPin class]];
[subclassingController registerSubclass:[PFEventuallyPin class]];
#if !TARGET_OS_WATCH && !TARGET_OS_TV
[subclassingController registerSubclass:[PFInstallation class]];
#endif
#if TARGET_OS_IOS || TARGET_OS_TV
[subclassingController registerSubclass:[PFProduct class]];
#endif
#if TARGET_OS_IOS
[PFNetworkActivityIndicatorManager sharedManager].enabled = YES;
#endif

View File

@@ -42,7 +42,6 @@
- (void)tearDown {
[[Parse _currentManager] clearEventuallyQueue];
[Parse _clearCurrentManager];
[PFObjectSubclassingController clearDefaultController];
[super tearDown];
}

View File

@@ -13,10 +13,10 @@
#import "PFMacros.h"
#import "PFObjectPrivate.h"
#import "PFRole.h"
#import "PFTestCase.h"
#import "PFUnitTestCase.h"
#import "PFUserPrivate.h"
@interface ACLTests : PFTestCase
@interface ACLTests : PFUnitTestCase
@end
@@ -210,8 +210,6 @@
- (void)testACLRequiresObjectId {
[PFUser registerSubclass];
PFACL *acl = [PFACL ACL];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"

View File

@@ -105,22 +105,6 @@
@implementation ObjectSubclassPropertiesTests
///--------------------------------------
#pragma mark - XCTestCase
///--------------------------------------
- (void)setUp {
[super setUp];
[PFTestObject registerSubclass];
}
- (void)tearDown {
[PFObject unregisterSubclass:[PFTestObject class]];
[super tearDown];
}
///--------------------------------------
#pragma mark - Tests
///--------------------------------------

View File

@@ -17,7 +17,7 @@
#pragma mark - Helpers
///--------------------------------------
@interface TheFlash : PFObject<PFSubclassing> {
@interface TheFlash : PFObject<PFSubclassingSkipAutomaticRegistration> {
NSString *flashName;
}
@@ -59,7 +59,7 @@
@end
@interface ClassWithDirtyingConstructor : PFObject<PFSubclassing>
@interface ClassWithDirtyingConstructor : PFObject<PFSubclassingSkipAutomaticRegistration>
@end
@implementation ClassWithDirtyingConstructor
@@ -85,7 +85,7 @@
@implementation UtilityClass
@end
@interface DescendantOfUtility : UtilityClass<PFSubclassing>
@interface DescendantOfUtility : UtilityClass<PFSubclassingSkipAutomaticRegistration>
@end
@implementation DescendantOfUtility
@@ -94,7 +94,7 @@
}
@end
@interface StateClass : PFObject<PFSubclassing>
@interface StateClass : PFObject<PFSubclassing, PFSubclassingSkipAutomaticRegistration>
@property (nonatomic, copy) NSString *state;
@@ -120,17 +120,6 @@
@implementation ObjectSubclassTests
///--------------------------------------
#pragma mark - XCTestCase
///--------------------------------------
- (void)tearDown {
[PFObject unregisterSubclass:[TheFlash class]];
[PFObject unregisterSubclass:[BarryAllen class]];
[super tearDown];
}
///--------------------------------------
#pragma mark - Tests
///--------------------------------------
@@ -173,18 +162,6 @@
[DescendantOfUtility registerSubclass];
}
- (void)testSubclassRegistrationBeforeInitializingParse {
[[Parse _currentManager] clearEventuallyQueue];
[Parse _clearCurrentManager];
[TheFlash registerSubclass];
[Parse setApplicationId:@"a" clientKey:@"b"];
PFObject *theFlash = [PFObject objectWithClassName:@"Person"];
PFAssertIsKindOfClass(theFlash, [TheFlash class]);
}
- (void)testStateIsSubclassable {
[StateClass registerSubclass];
StateClass *stateClass = [StateClass object];

View File

@@ -16,13 +16,13 @@
#import "PFUnitTestCase.h"
#import "ParseUnitTests-Swift.h"
@interface TestSubclass : PFObject<PFSubclassing>
@interface TestSubclass : PFObject<PFSubclassingSkipAutomaticRegistration>
@end
@interface NotSubclass : PFObject<PFSubclassing>
@interface NotSubclass : PFObject<PFSubclassingSkipAutomaticRegistration>
@end
@interface PropertySubclass : PFObject<PFSubclassing> {
@interface PropertySubclass : PFObject<PFSubclassingSkipAutomaticRegistration> {
@public
id _ivarProperty;
}

View File

@@ -29,12 +29,11 @@
- (void)setUp {
[super setUp];
[PFUser registerSubclass];
_user = [PFUser user];
}
- (void)tearDown {
[PFObject unregisterSubclass:[PFUser class]];
_user = nil;
[super tearDown];
}

View File

@@ -8,33 +8,15 @@
*/
#import "PFPin.h"
#import "PFTestCase.h"
#import "PFUnitTestCase.h"
#import "Parse_Private.h"
@interface PinUnitTests : PFTestCase
@interface PinUnitTests : PFUnitTestCase
@end
@implementation PinUnitTests
///--------------------------------------
#pragma mark - XCTestCase
///--------------------------------------
- (void)setUp {
[super setUp];
[Parse enableLocalDatastore];
[Parse setApplicationId:@"a" clientKey:@"b"];
}
- (void)tearDown {
[[Parse _currentManager] clearEventuallyQueue];
[Parse _clearCurrentManager];
[super tearDown];
}
///--------------------------------------
#pragma mark - Tests
///--------------------------------------

View File

@@ -16,31 +16,15 @@
#import "PFObjectPrivate.h"
#import "PFRESTCommand.h"
#import "PFSessionController.h"
#import "PFTestCase.h"
#import "PFUnitTestCase.h"
#import "Parse_Private.h"
@interface SessionControllerTests : PFTestCase
@interface SessionControllerTests : PFUnitTestCase
@end
@implementation SessionControllerTests
///--------------------------------------
#pragma mark - XCTestCase
///--------------------------------------
- (void)setUp {
[super setUp];
[PFSession registerSubclass];
}
- (void)tearDown {
[PFObject unregisterSubclass:[PFSession class]];
[super tearDown];
}
///--------------------------------------
#pragma mark - Helpers
///--------------------------------------

View File

@@ -44,24 +44,12 @@
#pragma mark - Tests
///--------------------------------------
- (void)testSessionClassIsRegistered {
[[Parse _currentManager] clearEventuallyQueue];
[Parse _clearCurrentManager];
[PFObjectSubclassingController clearDefaultController];
[PFObject unregisterSubclass:[PFSession class]];
[Parse setApplicationId:@"a" clientKey:@"b"];
XCTAssertNotNil([PFSession query]);
}
- (void)testConstructorsClassNameValidation {
PFAssertThrowsInvalidArgumentException([[PFSession alloc] initWithClassName:@"yarrclass"],
@"Should throw an exception for invalid classname");
}
- (void)testSessionImmutableFieldsCannotBeChanged {
[PFSession registerSubclass];
PFSession *session = [PFSession object];
session[@"yolo"] = @"El Capitan!"; // Test for regular mutability
PFAssertThrowsInvalidArgumentException(session[@"sessionToken"] = @"a");
@@ -73,8 +61,6 @@
}
- (void)testSessionImmutableFieldsCannotBeDeleted {
[PFSession registerSubclass];
PFSession *session = [PFSession object];
[session removeObjectForKey:@"yolo"];// Test for regular mutability

View File

@@ -25,15 +25,11 @@
}
- (void)testImmutableFieldsCannotBeChanged {
[PFUser registerSubclass];
PFUser *user = [PFUser object];
PFAssertThrowsInvalidArgumentException(user[@"sessionToken"] = @"a");
}
- (void)testImmutableFieldsCannotBeDeleted {
[PFUser registerSubclass];
PFUser *user = [PFUser object];
PFAssertThrowsInvalidArgumentException([user removeObjectForKey:@"username"]);
PFAssertThrowsInvalidArgumentException([user removeObjectForKey:@"sessionToken"]);