mirror of
https://github.com/tappollo/Parse-SDK-iOS-OSX.git
synced 2026-04-29 13:25:37 +08:00
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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -42,6 +42,7 @@ PFPersistenceControllerProvider>
|
||||
<PFLocationManagerProvider,
|
||||
PFDefaultACLControllerProvider,
|
||||
PFObjectControllerProvider,
|
||||
PFObjectSubclassingControllerProvider,
|
||||
PFObjectBatchController,
|
||||
PFObjectFilePersistenceControllerProvider,
|
||||
PFPinningObjectStoreProvider,
|
||||
|
||||
@@ -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
|
||||
///--------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2454,7 +2454,7 @@ static void PFObjectAssertValueIsKindOfValidClass(id object) {
|
||||
}
|
||||
|
||||
+ (PFObjectSubclassingController *)subclassingController {
|
||||
return [PFObjectSubclassingController defaultController];
|
||||
return [Parse _currentManager].coreManager.objectSubclassingController;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
- (void)tearDown {
|
||||
[[Parse _currentManager] clearEventuallyQueue];
|
||||
[Parse _clearCurrentManager];
|
||||
[PFObjectSubclassingController clearDefaultController];
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
///--------------------------------------
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -29,12 +29,11 @@
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
[PFUser registerSubclass];
|
||||
_user = [PFUser user];
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[PFObject unregisterSubclass:[PFUser class]];
|
||||
_user = nil;
|
||||
|
||||
[super tearDown];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
///--------------------------------------
|
||||
|
||||
@@ -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
|
||||
///--------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
Reference in New Issue
Block a user