Implemented lazy parsing of method signatures to improve TTI

This commit is contained in:
Nick Lockwood 2015-08-11 08:33:28 -07:00
Родитель 57a6a02dff
Коммит a5e9f83a0a
5 изменённых файлов: 240 добавлений и 213 удалений

Просмотреть файл

@ -162,8 +162,8 @@
- (void)testFamilyStyleAndWeight - (void)testFamilyStyleAndWeight
{ {
{ {
UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-UltraLightItalic" size:14]; UIFont *expected = [UIFont fontWithName:@"HelveticaNeue-LightItalic" size:14];
UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Helvetica Neue", @"fontStyle": @"italic", @"fontWeight": @"100"}]; UIFont *result = [RCTConvert UIFont:@{@"fontFamily": @"Helvetica Neue", @"fontStyle": @"italic", @"fontWeight": @"300"}];
RCTAssertEqualFonts(expected, result); RCTAssertEqualFonts(expected, result);
} }
{ {

Просмотреть файл

@ -63,9 +63,11 @@ static BOOL RCTLogsError(void (^block)(void))
// Specifying an NSNumber param without nonnull isn't allowed // Specifying an NSNumber param without nonnull isn't allowed
XCTAssertTrue(RCTLogsError(^{ XCTAssertTrue(RCTLogsError(^{
NSString *methodName = @"doFooWithNumber:(NSNumber *)n"; NSString *methodName = @"doFooWithNumber:(NSNumber *)n";
(void)[[RCTModuleMethod alloc] initWithObjCMethodName:methodName RCTModuleMethod *method = [[RCTModuleMethod alloc] initWithObjCMethodName:methodName
JSMethodName:nil JSMethodName:nil
moduleClass:[self class]]; moduleClass:[self class]];
// Invoke method to trigger parsing
[method invokeWithBridge:nil module:self arguments:@[@1]];
})); }));
} }

Просмотреть файл

@ -88,7 +88,7 @@ RCT_NOT_IMPLEMENTED(-init);
[self.methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger idx, __unused BOOL *stop) { [self.methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger idx, __unused BOOL *stop) {
methodconfig[method.JSMethodName] = @{ methodconfig[method.JSMethodName] = @{
@"methodID": @(idx), @"methodID": @(idx),
@"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote", @"type": method.functionType == RCTFunctionTypePromise ? @"remoteAsync" : @"remote",
}; };
}]; }];
config[@"methods"] = [methodconfig copy]; config[@"methods"] = [methodconfig copy];

Просмотреть файл

@ -11,9 +11,9 @@
@class RCTBridge; @class RCTBridge;
typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) { typedef NS_ENUM(NSUInteger, RCTFunctionType) {
RCTJavaScriptFunctionKindNormal, RCTFunctionTypeNormal,
RCTJavaScriptFunctionKindAsync, RCTFunctionTypePromise,
}; };
typedef NS_ENUM(NSUInteger, RCTNullability) { typedef NS_ENUM(NSUInteger, RCTNullability) {
@ -35,7 +35,7 @@ typedef NS_ENUM(NSUInteger, RCTNullability) {
@property (nonatomic, copy, readonly) NSString *JSMethodName; @property (nonatomic, copy, readonly) NSString *JSMethodName;
@property (nonatomic, readonly) Class moduleClass; @property (nonatomic, readonly) Class moduleClass;
@property (nonatomic, readonly) SEL selector; @property (nonatomic, readonly) SEL selector;
@property (nonatomic, readonly) RCTJavaScriptFunctionKind functionKind; @property (nonatomic, readonly) RCTFunctionType functionType;
- (instancetype)initWithObjCMethodName:(NSString *)objCMethodName - (instancetype)initWithObjCMethodName:(NSString *)objCMethodName
JSMethodName:(NSString *)JSMethodName JSMethodName:(NSString *)JSMethodName

Просмотреть файл

@ -46,9 +46,10 @@ typedef void (^RCTArgumentBlock)(RCTBridge *, NSUInteger, id);
@implementation RCTModuleMethod @implementation RCTModuleMethod
{ {
Class _moduleClass; Class _moduleClass;
SEL _selector;
NSInvocation *_invocation; NSInvocation *_invocation;
NSArray *_argumentBlocks; NSArray *_argumentBlocks;
NSString *_objCMethodName;
SEL _selector;
} }
static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index, static void RCTLogArgumentError(RCTModuleMethod *method, NSUInteger index,
@ -117,13 +118,8 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments)
{ {
if ((self = [super init])) { if ((self = [super init])) {
NSArray *arguments;
RCTParseObjCMethodName(&objCMethodName, &arguments);
_moduleClass = moduleClass; _moduleClass = moduleClass;
_selector = NSSelectorFromString(objCMethodName); _objCMethodName = [objCMethodName copy];
RCTAssert(_selector, @"%@ is not a valid selector", objCMethodName);
_JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({ _JSMethodName = JSMethodName.length > 0 ? JSMethodName : ({
NSString *methodName = objCMethodName; NSString *methodName = objCMethodName;
NSRange colonRange = [methodName rangeOfString:@":"]; NSRange colonRange = [methodName rangeOfString:@":"];
@ -135,26 +131,139 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments)
methodName; methodName;
}); });
// Create method invocation if ([_objCMethodName rangeOfString:@"RCTPromise"].length) {
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector]; _functionType = RCTFunctionTypePromise;
RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", objCMethodName); } else {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; _functionType = RCTFunctionTypeNormal;
[invocation setSelector:_selector]; }
[invocation retainArguments]; }
_invocation = invocation;
// Process arguments return self;
NSUInteger numberOfArguments = methodSignature.numberOfArguments; }
NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];
- (void)processMethodSignature
{
NSArray *arguments;
NSString *objCMethodName = _objCMethodName;
RCTParseObjCMethodName(&objCMethodName, &arguments);
_selector = NSSelectorFromString(objCMethodName);
RCTAssert(_selector, @"%@ is not a valid selector", objCMethodName);
// Create method invocation
NSMethodSignature *methodSignature = [_moduleClass instanceMethodSignatureForSelector:_selector];
RCTAssert(methodSignature, @"%@ is not a recognized Objective-C method.", objCMethodName);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setSelector:_selector];
[invocation retainArguments];
_invocation = invocation;
// Process arguments
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
NSMutableArray *argumentBlocks = [[NSMutableArray alloc] initWithCapacity:numberOfArguments - 2];
#define RCT_ARG_BLOCK(_logic) \ #define RCT_ARG_BLOCK(_logic) \
[argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \ [argumentBlocks addObject:^(__unused RCTBridge *bridge, NSUInteger index, id json) { \
_logic \ _logic \
[invocation setArgument:&value atIndex:(index) + 2]; \ [invocation setArgument:&value atIndex:(index) + 2]; \
}]; }];
__weak RCTModuleMethod *weakSelf = self; __weak RCTModuleMethod *weakSelf = self;
void (^addBlockArgument)(void) = ^{ void (^addBlockArgument)(void) = ^{
RCT_ARG_BLOCK(
if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a function");
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing id value = (json ? ^(NSArray *args) {
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, args]];
} : ^(__unused NSArray *unused) {});
)
};
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *objcType = [methodSignature getArgumentTypeAtIndex:i];
BOOL isNullableType = NO;
RCTMethodArgument *argument = arguments[i - 2];
NSString *typeName = argument.type;
SEL selector = NSSelectorFromString([typeName stringByAppendingString:@":"]);
if ([RCTConvert respondsToSelector:selector]) {
switch (objcType[0]) {
#define RCT_CASE(_value, _type) \
case _value: { \
_type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
RCT_CASE(_C_CHR, char)
RCT_CASE(_C_UCHR, unsigned char)
RCT_CASE(_C_SHT, short)
RCT_CASE(_C_USHT, unsigned short)
RCT_CASE(_C_INT, int)
RCT_CASE(_C_UINT, unsigned int)
RCT_CASE(_C_LNG, long)
RCT_CASE(_C_ULNG, unsigned long)
RCT_CASE(_C_LNG_LNG, long long)
RCT_CASE(_C_ULNG_LNG, unsigned long long)
RCT_CASE(_C_FLT, float)
RCT_CASE(_C_DBL, double)
RCT_CASE(_C_BOOL, BOOL)
#define RCT_NULLABLE_CASE(_value, _type) \
case _value: { \
isNullableType = YES; \
_type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
RCT_NULLABLE_CASE(_C_SEL, SEL)
RCT_NULLABLE_CASE(_C_CHARPTR, const char *)
RCT_NULLABLE_CASE(_C_PTR, void *)
RCT_NULLABLE_CASE(_C_ID, id)
case _C_STRUCT_B: {
NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector];
NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature];
[typeInvocation setSelector:selector];
[typeInvocation setTarget:[RCTConvert class]];
[argumentBlocks addObject:
^(__unused RCTBridge *bridge, NSUInteger index, id json) {
void *returnValue = malloc(typeSignature.methodReturnLength);
[typeInvocation setArgument:&json atIndex:2];
[typeInvocation invoke];
[typeInvocation getReturnValue:returnValue];
[invocation setArgument:returnValue atIndex:index + 2];
free(returnValue);
}];
break;
}
default: {
static const char *blockType = @encode(typeof(^{}));
if (!strcmp(objcType, blockType)) {
addBlockArgument();
} else {
RCTLogError(@"Unsupported argument type '%@' in method %@.",
typeName, [self methodName]);
}
}
}
} else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) {
addBlockArgument();
} else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) {
RCT_ARG_BLOCK( RCT_ARG_BLOCK(
if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) { if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) {
@ -163,202 +272,115 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments)
} }
// Marked as autoreleasing, because NSInvocation doesn't retain arguments // Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing id value = (json ? ^(NSArray *args) { __autoreleasing id value = (json ? ^(NSError *error) {
[bridge _invokeAndProcessModule:@"BatchedBridge" [bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue" method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, args]]; arguments:@[json, @[RCTJSErrorFromNSError(error)]]];
} : ^(__unused NSArray *unused) {}); } : ^(__unused NSError *error) {});
) )
}; } else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
for (NSUInteger i = 2; i < numberOfArguments; i++) { @"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
const char *objcType = [methodSignature getArgumentTypeAtIndex:i]; _moduleClass, objCMethodName);
BOOL isNullableType = NO; RCT_ARG_BLOCK(
RCTMethodArgument *argument = arguments[i - 2]; if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
NSString *typeName = argument.type; RCTLogArgumentError(weakSelf, index, json, "should be a promise resolver function");
SEL selector = NSSelectorFromString([typeName stringByAppendingString:@":"]); return;
if ([RCTConvert respondsToSelector:selector]) {
switch (objcType[0]) {
#define RCT_CASE(_value, _type) \
case _value: { \
_type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
RCT_CASE(_C_CHR, char)
RCT_CASE(_C_UCHR, unsigned char)
RCT_CASE(_C_SHT, short)
RCT_CASE(_C_USHT, unsigned short)
RCT_CASE(_C_INT, int)
RCT_CASE(_C_UINT, unsigned int)
RCT_CASE(_C_LNG, long)
RCT_CASE(_C_ULNG, unsigned long)
RCT_CASE(_C_LNG_LNG, long long)
RCT_CASE(_C_ULNG_LNG, unsigned long long)
RCT_CASE(_C_FLT, float)
RCT_CASE(_C_DBL, double)
RCT_CASE(_C_BOOL, BOOL)
#define RCT_NULLABLE_CASE(_value, _type) \
case _value: { \
isNullableType = YES; \
_type (*convert)(id, SEL, id) = (typeof(convert))objc_msgSend; \
RCT_ARG_BLOCK( _type value = convert([RCTConvert class], selector, json); ) \
break; \
}
RCT_NULLABLE_CASE(_C_SEL, SEL)
RCT_NULLABLE_CASE(_C_CHARPTR, const char *)
RCT_NULLABLE_CASE(_C_PTR, void *)
RCT_NULLABLE_CASE(_C_ID, id)
case _C_STRUCT_B: {
NSMethodSignature *typeSignature = [RCTConvert methodSignatureForSelector:selector];
NSInvocation *typeInvocation = [NSInvocation invocationWithMethodSignature:typeSignature];
[typeInvocation setSelector:selector];
[typeInvocation setTarget:[RCTConvert class]];
[argumentBlocks addObject:
^(__unused RCTBridge *bridge, NSUInteger index, id json) {
void *returnValue = malloc(typeSignature.methodReturnLength);
[typeInvocation setArgument:&json atIndex:2];
[typeInvocation invoke];
[typeInvocation getReturnValue:returnValue];
[invocation setArgument:returnValue atIndex:index + 2];
free(returnValue);
}];
break;
}
default: {
static const char *blockType = @encode(typeof(^{}));
if (!strcmp(objcType, blockType)) {
addBlockArgument();
} else {
RCTLogError(@"Unsupported argument type '%@' in method %@.",
typeName, [self methodName]);
}
}
}
} else if ([typeName isEqualToString:@"RCTResponseSenderBlock"]) {
addBlockArgument();
} else if ([typeName isEqualToString:@"RCTResponseErrorBlock"]) {
RCT_ARG_BLOCK(
if (RCT_DEBUG && json && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a function");
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing id value = (json ? ^(NSError *error) {
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, @[RCTJSErrorFromNSError(error)]]];
} : ^(__unused NSError *error) {});
)
} else if ([typeName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
_moduleClass, objCMethodName);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a promise resolver function");
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing RCTPromiseResolveBlock value = (^(id result) {
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, result ? @[result] : @[]]];
});
)
_functionKind = RCTJavaScriptFunctionKindAsync;
} else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
_moduleClass, objCMethodName);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogArgumentError(weakSelf, index, json, "should be a promise rejecter function");
return;
}
// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) {
NSDictionary *errorJSON = RCTJSErrorFromNSError(error);
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, @[errorJSON]]];
});
)
_functionKind = RCTJavaScriptFunctionKindAsync;
} else {
// Unknown argument type
RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert"
" to support this type.", typeName, [self methodName]);
}
if (RCT_DEBUG) {
RCTNullability nullability = argument.nullability;
if (!isNullableType) {
if (nullability == RCTNullable) {
RCTLogArgumentError(weakSelf, i - 2, typeName, "is marked as "
"nullable, but is not a nullable type.");
}
nullability = RCTNonnullable;
} }
/** // Marked as autoreleasing, because NSInvocation doesn't retain arguments
* Special case - Numbers are not nullable in Android, so we __autoreleasing RCTPromiseResolveBlock value = (^(id result) {
* don't support this for now. In future we may allow it. [bridge _invokeAndProcessModule:@"BatchedBridge"
*/ method:@"invokeCallbackAndReturnFlushedQueue"
if ([typeName isEqualToString:@"NSNumber"]) { arguments:@[json, result ? @[result] : @[]]];
BOOL unspecified = (nullability == RCTNullabilityUnspecified); });
if (!argument.unused && (nullability == RCTNullable || unspecified)) { )
RCTLogArgumentError(weakSelf, i - 2, typeName, } else if ([typeName isEqualToString:@"RCTPromiseRejectBlock"]) {
[unspecified ? @"has unspecified nullability" : @"is marked as nullable" RCTAssert(i == numberOfArguments - 1,
stringByAppendingString: @" but React requires that all NSNumber " @"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
"arguments are explicitly marked as `nonnull` to ensure " _moduleClass, objCMethodName);
"compatibility with Android."].UTF8String); RCT_ARG_BLOCK(
} if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
nullability = RCTNonnullable; RCTLogArgumentError(weakSelf, index, json, "should be a promise rejecter function");
return;
} }
if (nullability == RCTNonnullable) { // Marked as autoreleasing, because NSInvocation doesn't retain arguments
RCTArgumentBlock oldBlock = argumentBlocks[i - 2]; __autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) {
argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSUInteger index, id json) { NSDictionary *errorJSON = RCTJSErrorFromNSError(error);
if (json == nil || json == (id)kCFNull) { [bridge _invokeAndProcessModule:@"BatchedBridge"
RCTLogArgumentError(weakSelf, index, typeName, "must not be null"); method:@"invokeCallbackAndReturnFlushedQueue"
id null = nil; arguments:@[json, @[errorJSON]]];
[invocation setArgument:&null atIndex:index + 2]; });
} else { )
oldBlock(bridge, index, json); } else {
}
}; // Unknown argument type
} RCTLogError(@"Unknown argument type '%@' in method %@. Extend RCTConvert"
} " to support this type.", typeName, [self methodName]);
} }
_argumentBlocks = [argumentBlocks copy]; if (RCT_DEBUG) {
RCTNullability nullability = argument.nullability;
if (!isNullableType) {
if (nullability == RCTNullable) {
RCTLogArgumentError(weakSelf, i - 2, typeName, "is marked as "
"nullable, but is not a nullable type.");
}
nullability = RCTNonnullable;
}
/**
* Special case - Numbers are not nullable in Android, so we
* don't support this for now. In future we may allow it.
*/
if ([typeName isEqualToString:@"NSNumber"]) {
BOOL unspecified = (nullability == RCTNullabilityUnspecified);
if (!argument.unused && (nullability == RCTNullable || unspecified)) {
RCTLogArgumentError(weakSelf, i - 2, typeName,
[unspecified ? @"has unspecified nullability" : @"is marked as nullable"
stringByAppendingString: @" but React requires that all NSNumber "
"arguments are explicitly marked as `nonnull` to ensure "
"compatibility with Android."].UTF8String);
}
nullability = RCTNonnullable;
}
if (nullability == RCTNonnullable) {
RCTArgumentBlock oldBlock = argumentBlocks[i - 2];
argumentBlocks[i - 2] = ^(RCTBridge *bridge, NSUInteger index, id json) {
if (json == nil || json == (id)kCFNull) {
RCTLogArgumentError(weakSelf, index, typeName, "must not be null");
id null = nil;
[invocation setArgument:&null atIndex:index + 2];
} else {
oldBlock(bridge, index, json);
}
};
}
}
} }
return self; _argumentBlocks = [argumentBlocks copy];
}
- (SEL)selector
{
if (_selector == NULL) {
[self processMethodSignature];
}
return _selector;
} }
- (void)invokeWithBridge:(RCTBridge *)bridge - (void)invokeWithBridge:(RCTBridge *)bridge
module:(id)module module:(id)module
arguments:(NSArray *)arguments arguments:(NSArray *)arguments
{ {
if (_argumentBlocks == nil) {
[self processMethodSignature];
}
if (RCT_DEBUG) { if (RCT_DEBUG) {
// Sanity check // Sanity check
@ -371,7 +393,7 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments)
NSInteger expectedCount = _argumentBlocks.count; NSInteger expectedCount = _argumentBlocks.count;
// Subtract the implicit Promise resolver and rejecter functions for implementations of async functions // Subtract the implicit Promise resolver and rejecter functions for implementations of async functions
if (_functionKind == RCTJavaScriptFunctionKindAsync) { if (_functionType == RCTFunctionTypePromise) {
actualCount -= 2; actualCount -= 2;
expectedCount -= 2; expectedCount -= 2;
} }
@ -398,6 +420,9 @@ void RCTParseObjCMethodName(NSString **objCMethodName, NSArray **arguments)
- (NSString *)methodName - (NSString *)methodName
{ {
if (_selector == NULL) {
[self processMethodSignature];
}
return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass, return [NSString stringWithFormat:@"-[%@ %@]", _moduleClass,
NSStringFromSelector(_selector)]; NSStringFromSelector(_selector)];
} }