我如何用字典支持一堆 class 属性?
How would I back a bunch of class properties with a dictionary?
我有一个 class 将属性保存在字典中,其中键定义明确。我想用 class 替换这个属性字典,我们称它为 AttributeSet。哪里有定义的键:
extern NSString *const Foo;
我想拥有房产:
@interface AttributeSet : NSObject
@property(strong) NSString *Foo;
...a ton more
@end
实际上,出于向后兼容的原因,我希望 AttributeSet 对象在幕后使用字典。所以当这种情况发生时:
attributeSet.Foo = @"bar";
我真的希望这发生:
- (void)setFoo:(NSString *)foo {
self.attributes[Foo] = foo; //Foo is the extern variable Foo
}
但我不想为 所有 属性定义 getter 和 setter。
我知道我可以使用键值观察,但这将 1) 要求我有一个映射 (属性 name) @"Foo" --> (variable name) Foo and 2)导致 属性 被设置 和 字典值被设置,而实际上我只想设置字典.
我知道我可以做这样的事情:https://github.com/iosptl/ios6ptl/blob/master/ch28/Person/Person/Person.m
但这将 1) 仍然需要我有一个映射和 2) 需要我为每个 属性.
有一个@dynamic
有没有更自动的方法来做到这一点?
谢谢
要使用动态生成的访问器方法,如您链接的 Person
代码所示,无需 @dynamic
,您可以在 class 的类别中声明属性而不是 class 本身:
@interface AttributeSet : NSObject
// ... no properties here ...
@end
@interface AttributeSet (YourPropertiesCategoryName)
@property(strong) NSString *Foo;
...a ton more
@end
编译器将自动合成在 class 本身或 class 扩展(看起来像没有类别名称的类别)中声明的属性,但不是为类别。
请注意,您不需要也不应该为该类别提供实现。 (如果你这样做,编译器会抱怨缺少属性的实现。它不会自动合成它们,但你仍然需要使用 @dynamic
来消除警告。)
Rob Napier 的例子是一个不错的选择;编译器将为您生成访问器,除非您告诉它不要这样做,而您告诉它的方式是使用 @dynamic
指令。
另一个选项是自动代码生成:编写一个脚本来为您的 setter 发出 ObjC 代码。
我能想到的第三个是overwriting the accessors during runtime。在您的 class 的 +initialize
中,您可以从运行时库中获取其属性列表,并使用 class_replaceMethod()
插入您自己的访问器,这些访问器使用您的字典而不是 ivars。这将需要进行一些字符串处理以从彼此获取访问器名称和密钥。
这是最后一个选项的演示要点:https://gist.github.com/woolsweater/4fb874b15449ee7fd7e8
经过一段时间,我想我已经为您提出了相当可扩展的解决方案。它所需要的只是使用以下助手 class 简单地创建对象,如下所示:
#import "DictionaryBackedObject.h"
extern NSString *const foo;
NSString *const foo = @"Foo";
@interface Foo : NSObject
@property NSString *foo;
@end
@implementation Foo
@end
int main() {
Foo *object = [DictionaryBackedObject dictionaryBackedObjectOfType:[Foo class]
backingDictionary:@{ foo: @"Bar" }
mutable:NO];
NSLog(@"%@", [object foo]);
}
注意:这个实现远非完美,它确实使用了 'dreaded' dlsym
API,这意味着,如果您愿意,您不能从可执行文件中删除符号使用这个 class。此外,如果将其提交到应用商店,可能会导致拒绝。还有其他方法可以自动确定与字典一起使用的密钥,但是,如果您希望找到解决方法。
此实现确实支持结构属性,以及弱属性、复制属性和原子属性。它将比在普通对象上设置 属性 慢得多,因为这要经过 objective-c 的转发 API(需要支持结构 returns)。
希望这对你有所帮助,我确实从中获得了很多乐趣。
DictionaryBackedObject.h
@interface DictionaryBackedObject : NSObject
+(id) dictionaryBackedObjectOfType:(Class) kls backingDictionary:(NSDictionary *) dictionary mutable:(BOOL) isMutable;
@end
DictionaryBackedObject.m
#import "DictionaryBackedObject.h"
#include <stdalign.h>
#include <dlfcn.h>
@import ObjectiveC.runtime;
@import ObjectiveC.message;
__attribute__((noinline))
static SEL property_getGetterSelector(objc_property_t property) {
char *getter = property_copyAttributeValue(property, "G");
if (getter) {
SEL result = sel_registerName(getter);
free(getter);
return result;
}
return sel_registerName(property_getName(property));
}
__attribute__((noinline))
static SEL property_getSetterSelector(objc_property_t property) {
char *setter = property_copyAttributeValue(property, "S");
if (setter) {
SEL result = sel_registerName(setter);
free(setter);
return result;
}
char buffer[512];
char propertyName[512];
strncpy(propertyName, property_getName(property), 512);
propertyName[0] = toupper(propertyName[0]);
snprintf(buffer, 512, "set%s", propertyName);
return sel_registerName(buffer);
}
struct objc_property_attributes_t {
union {
struct {
int nonatomic : 1;
int copy : 1;
int weak : 1;
int strong : 1;
};
int memory_mode;
};
int is_readonly;
int is_dynamic;
};
static inline BOOL property_isAttributeNull(objc_property_t property, const char *attr) {
void *value = property_copyAttributeValue(property, attr);
BOOL results = value == NULL;
free(value);
return results;
}
static struct objc_property_attributes_t property_getPropertyAttributes(objc_property_t property) {
struct objc_property_attributes_t attrs;
attrs.nonatomic = !property_isAttributeNull(property, "N");
attrs.copy = !property_isAttributeNull(property, "C");
attrs.strong = attrs.copy || !property_isAttributeNull(property, "&");
attrs.weak = !property_isAttributeNull(property, "W");
attrs.is_readonly = !property_isAttributeNull(property, "R");
attrs.is_dynamic = !property_isAttributeNull(property, "D");
return attrs;
}
static objc_property_t class_getPropertyForSelector(Class kls, SEL cmd) {
#define VALID_PROPERTY(property) \
(property != NULL && (property_getGetterSelector(property) == cmd || property_getSetterSelector(property) == cmd))
const char *selName = sel_getName(cmd);
objc_property_t results = class_getProperty(kls, selName);
if (VALID_PROPERTY(results))
return results;
if (strstr(selName, "set") == selName) {
char lowercaseSel[512];
strncpy(lowercaseSel, strstr(selName, "set"), 512);
lowercaseSel[0] = tolower(lowercaseSel[0]);
results = class_getProperty(kls, lowercaseSel);
if (VALID_PROPERTY(results)) return results;
}
// Easy paths exhausted, go the 'hard' way of looping over all of the properties available
results = NULL;
unsigned propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(kls, &propertyCount);
for (unsigned propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++) {
if (VALID_PROPERTY(properties[propertyIndex])) {
results = properties[propertyIndex];
break;
}
}
free(properties);
return results;
#undef VALID_PROPERTY
}
@implementation DictionaryBackedObject
-(id) initWithDictionary:(NSDictionary *) dictionary mutable:(BOOL) isMutable {
return nil;
}
+(Class) dictionaryBackedSubclassOfClass:(Class) kls {
@synchronized (kls) {
NSString *className = [NSStringFromClass(kls) stringByAppendingFormat:@"_dictionaryBacked"];
Class subclass = Nil;
if ((subclass = NSClassFromString(className))) {
return subclass;
}
subclass = objc_allocateClassPair(kls, [className UTF8String], 0);
class_addIvar(subclass, "_backingDictionary", sizeof(NSDictionary *), _Alignof(NSDictionary *), @encode(NSDictionary *));
class_addIvar(subclass, "_backingDictionaryIsMutable", sizeof(NSNumber *), _Alignof(NSNumber *), @encode(NSNumber *));
unsigned propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(kls, &propertyCount);
for (unsigned i = 0; i < propertyCount; i++) {
objc_property_t property = properties[i];
char *type = property_copyAttributeValue(property, "T");
SEL getterSel = property_getGetterSelector(property);
SEL setterSel = property_getSetterSelector(property);
char getterTypeBuffer[512];
snprintf(getterTypeBuffer, 512, "%s@:", type);
char setterTypeBuffer[512];
snprintf(setterTypeBuffer, 512, "v@:%s", type);
NSUInteger typeSize;
NSUInteger typeAlignment;
NSGetSizeAndAlignment(type, &typeSize, &typeAlignment);
BOOL isStret = (typeSize * CHAR_BIT) > (WORD_BIT * 2);
class_addMethod(subclass, getterSel, isStret ? _objc_msgForward_stret : _objc_msgForward , getterTypeBuffer);
class_addMethod(subclass, setterSel, _objc_msgForward, setterTypeBuffer);
free(type);
}
free(properties);
Ivar backingDictionaryIvar = class_getInstanceVariable(subclass, "_backingDictionary");
Ivar backingDictionaryMutableIvar = class_getInstanceVariable(subclass, "_backingDictionaryIsMutable");
class_addMethod(subclass, @selector(forwardingTargetForSelector:), imp_implementationWithBlock(^id (id self) {
return nil;
}), "@@:");
class_addMethod(subclass, @selector(forwardInvocation:), imp_implementationWithBlock(^void (id self, NSInvocation *invocation) {
SEL _cmd = [invocation selector];
objc_property_t property = class_getPropertyForSelector([self class], _cmd);
if (property == NULL) {
[self doesNotRecognizeSelector:_cmd];
return;
}
BOOL isGetter = (_cmd == property_getGetterSelector(property));
struct objc_property_attributes_t attributes = property_getPropertyAttributes(property);
NSString *propertyType = (__bridge_transfer NSString *) CFStringCreateWithCStringNoCopy(
NULL, property_copyAttributeValue(property, "T"), kCFStringEncodingUTF8, NULL
);
NSUInteger propertySize;
NSGetSizeAndAlignment([propertyType UTF8String], &propertySize, NULL);
void *dlsymKey = dlsym(RTLD_MAIN_ONLY, property_getName(property));
id dictionaryKey = *(__unsafe_unretained id *) dlsymKey;
NSMutableDictionary *backingDictionary = object_getIvar(self, backingDictionaryIvar);
NSNumber *isMutable = object_getIvar(self, backingDictionaryMutableIvar);
// Performing synchronization on nil is a no-op, see objc_sync.mm:306.
@synchronized (attributes.nonatomic ? nil : self) {
if (isGetter) {
id value = backingDictionary[dictionaryKey];
if (attributes.strong) {
[invocation setReturnValue:&value];
} else if (attributes.weak) {
value = [value nonretainedObjectValue];
[invocation setReturnValue:&value];
} else {
void *buffer = alloca(propertySize);
[value getValue:buffer];
[invocation setReturnValue:buffer];
}
} else {
if ((attributes.is_readonly || ![isMutable boolValue])) {
[self doesNotRecognizeSelector:_cmd];
return;
}
id dictionaryValue = nil;
void *newValue = alloca(propertySize);
[invocation getArgument:newValue atIndex:2];
if (attributes.strong) {
dictionaryValue = (__bridge id) newValue;
if (attributes.copy) {
dictionaryValue = [dictionaryValue copy];
}
} else if (attributes.weak) {
dictionaryValue = [NSValue valueWithNonretainedObject:(__bridge id) newValue];
} else {
dictionaryValue = [NSValue valueWithBytes:newValue objCType:[propertyType UTF8String]];
}
if (dictionaryValue == nil) {
[backingDictionary removeObjectForKey:dictionaryKey];
} else {
[backingDictionary setObject:dictionaryValue forKey:dictionaryKey];
}
}
}
}), "v@:@");
class_addMethod(subclass, @selector(initWithDictionary:mutable:), imp_implementationWithBlock(^id (id self, NSDictionary *dictionary, BOOL mutable) {
object_setIvar(self, backingDictionaryIvar, dictionary);
object_setIvar(self, backingDictionaryMutableIvar, @(mutable));
return self;
}), "@@:@c");
objc_registerClassPair(subclass);
return subclass;
}
}
+(id) dictionaryBackedObjectOfType:(Class)kls backingDictionary:(NSDictionary *)dictionary mutable:(BOOL)isMutable {
Class subclass = [self dictionaryBackedSubclassOfClass:kls];
return [[subclass alloc] initWithDictionary:dictionary mutable:isMutable];
}
@end
我有一个 class 将属性保存在字典中,其中键定义明确。我想用 class 替换这个属性字典,我们称它为 AttributeSet。哪里有定义的键:
extern NSString *const Foo;
我想拥有房产:
@interface AttributeSet : NSObject
@property(strong) NSString *Foo;
...a ton more
@end
实际上,出于向后兼容的原因,我希望 AttributeSet 对象在幕后使用字典。所以当这种情况发生时:
attributeSet.Foo = @"bar";
我真的希望这发生:
- (void)setFoo:(NSString *)foo {
self.attributes[Foo] = foo; //Foo is the extern variable Foo
}
但我不想为 所有 属性定义 getter 和 setter。
我知道我可以使用键值观察,但这将 1) 要求我有一个映射 (属性 name) @"Foo" --> (variable name) Foo and 2)导致 属性 被设置 和 字典值被设置,而实际上我只想设置字典.
我知道我可以做这样的事情:https://github.com/iosptl/ios6ptl/blob/master/ch28/Person/Person/Person.m 但这将 1) 仍然需要我有一个映射和 2) 需要我为每个 属性.
有一个@dynamic有没有更自动的方法来做到这一点?
谢谢
要使用动态生成的访问器方法,如您链接的 Person
代码所示,无需 @dynamic
,您可以在 class 的类别中声明属性而不是 class 本身:
@interface AttributeSet : NSObject
// ... no properties here ...
@end
@interface AttributeSet (YourPropertiesCategoryName)
@property(strong) NSString *Foo;
...a ton more
@end
编译器将自动合成在 class 本身或 class 扩展(看起来像没有类别名称的类别)中声明的属性,但不是为类别。
请注意,您不需要也不应该为该类别提供实现。 (如果你这样做,编译器会抱怨缺少属性的实现。它不会自动合成它们,但你仍然需要使用 @dynamic
来消除警告。)
Rob Napier 的例子是一个不错的选择;编译器将为您生成访问器,除非您告诉它不要这样做,而您告诉它的方式是使用 @dynamic
指令。
另一个选项是自动代码生成:编写一个脚本来为您的 setter 发出 ObjC 代码。
我能想到的第三个是overwriting the accessors during runtime。在您的 class 的 +initialize
中,您可以从运行时库中获取其属性列表,并使用 class_replaceMethod()
插入您自己的访问器,这些访问器使用您的字典而不是 ivars。这将需要进行一些字符串处理以从彼此获取访问器名称和密钥。
这是最后一个选项的演示要点:https://gist.github.com/woolsweater/4fb874b15449ee7fd7e8
经过一段时间,我想我已经为您提出了相当可扩展的解决方案。它所需要的只是使用以下助手 class 简单地创建对象,如下所示:
#import "DictionaryBackedObject.h"
extern NSString *const foo;
NSString *const foo = @"Foo";
@interface Foo : NSObject
@property NSString *foo;
@end
@implementation Foo
@end
int main() {
Foo *object = [DictionaryBackedObject dictionaryBackedObjectOfType:[Foo class]
backingDictionary:@{ foo: @"Bar" }
mutable:NO];
NSLog(@"%@", [object foo]);
}
注意:这个实现远非完美,它确实使用了 'dreaded' dlsym
API,这意味着,如果您愿意,您不能从可执行文件中删除符号使用这个 class。此外,如果将其提交到应用商店,可能会导致拒绝。还有其他方法可以自动确定与字典一起使用的密钥,但是,如果您希望找到解决方法。
此实现确实支持结构属性,以及弱属性、复制属性和原子属性。它将比在普通对象上设置 属性 慢得多,因为这要经过 objective-c 的转发 API(需要支持结构 returns)。
希望这对你有所帮助,我确实从中获得了很多乐趣。
DictionaryBackedObject.h
@interface DictionaryBackedObject : NSObject
+(id) dictionaryBackedObjectOfType:(Class) kls backingDictionary:(NSDictionary *) dictionary mutable:(BOOL) isMutable;
@end
DictionaryBackedObject.m
#import "DictionaryBackedObject.h"
#include <stdalign.h>
#include <dlfcn.h>
@import ObjectiveC.runtime;
@import ObjectiveC.message;
__attribute__((noinline))
static SEL property_getGetterSelector(objc_property_t property) {
char *getter = property_copyAttributeValue(property, "G");
if (getter) {
SEL result = sel_registerName(getter);
free(getter);
return result;
}
return sel_registerName(property_getName(property));
}
__attribute__((noinline))
static SEL property_getSetterSelector(objc_property_t property) {
char *setter = property_copyAttributeValue(property, "S");
if (setter) {
SEL result = sel_registerName(setter);
free(setter);
return result;
}
char buffer[512];
char propertyName[512];
strncpy(propertyName, property_getName(property), 512);
propertyName[0] = toupper(propertyName[0]);
snprintf(buffer, 512, "set%s", propertyName);
return sel_registerName(buffer);
}
struct objc_property_attributes_t {
union {
struct {
int nonatomic : 1;
int copy : 1;
int weak : 1;
int strong : 1;
};
int memory_mode;
};
int is_readonly;
int is_dynamic;
};
static inline BOOL property_isAttributeNull(objc_property_t property, const char *attr) {
void *value = property_copyAttributeValue(property, attr);
BOOL results = value == NULL;
free(value);
return results;
}
static struct objc_property_attributes_t property_getPropertyAttributes(objc_property_t property) {
struct objc_property_attributes_t attrs;
attrs.nonatomic = !property_isAttributeNull(property, "N");
attrs.copy = !property_isAttributeNull(property, "C");
attrs.strong = attrs.copy || !property_isAttributeNull(property, "&");
attrs.weak = !property_isAttributeNull(property, "W");
attrs.is_readonly = !property_isAttributeNull(property, "R");
attrs.is_dynamic = !property_isAttributeNull(property, "D");
return attrs;
}
static objc_property_t class_getPropertyForSelector(Class kls, SEL cmd) {
#define VALID_PROPERTY(property) \
(property != NULL && (property_getGetterSelector(property) == cmd || property_getSetterSelector(property) == cmd))
const char *selName = sel_getName(cmd);
objc_property_t results = class_getProperty(kls, selName);
if (VALID_PROPERTY(results))
return results;
if (strstr(selName, "set") == selName) {
char lowercaseSel[512];
strncpy(lowercaseSel, strstr(selName, "set"), 512);
lowercaseSel[0] = tolower(lowercaseSel[0]);
results = class_getProperty(kls, lowercaseSel);
if (VALID_PROPERTY(results)) return results;
}
// Easy paths exhausted, go the 'hard' way of looping over all of the properties available
results = NULL;
unsigned propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(kls, &propertyCount);
for (unsigned propertyIndex = 0; propertyIndex < propertyCount; propertyIndex++) {
if (VALID_PROPERTY(properties[propertyIndex])) {
results = properties[propertyIndex];
break;
}
}
free(properties);
return results;
#undef VALID_PROPERTY
}
@implementation DictionaryBackedObject
-(id) initWithDictionary:(NSDictionary *) dictionary mutable:(BOOL) isMutable {
return nil;
}
+(Class) dictionaryBackedSubclassOfClass:(Class) kls {
@synchronized (kls) {
NSString *className = [NSStringFromClass(kls) stringByAppendingFormat:@"_dictionaryBacked"];
Class subclass = Nil;
if ((subclass = NSClassFromString(className))) {
return subclass;
}
subclass = objc_allocateClassPair(kls, [className UTF8String], 0);
class_addIvar(subclass, "_backingDictionary", sizeof(NSDictionary *), _Alignof(NSDictionary *), @encode(NSDictionary *));
class_addIvar(subclass, "_backingDictionaryIsMutable", sizeof(NSNumber *), _Alignof(NSNumber *), @encode(NSNumber *));
unsigned propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(kls, &propertyCount);
for (unsigned i = 0; i < propertyCount; i++) {
objc_property_t property = properties[i];
char *type = property_copyAttributeValue(property, "T");
SEL getterSel = property_getGetterSelector(property);
SEL setterSel = property_getSetterSelector(property);
char getterTypeBuffer[512];
snprintf(getterTypeBuffer, 512, "%s@:", type);
char setterTypeBuffer[512];
snprintf(setterTypeBuffer, 512, "v@:%s", type);
NSUInteger typeSize;
NSUInteger typeAlignment;
NSGetSizeAndAlignment(type, &typeSize, &typeAlignment);
BOOL isStret = (typeSize * CHAR_BIT) > (WORD_BIT * 2);
class_addMethod(subclass, getterSel, isStret ? _objc_msgForward_stret : _objc_msgForward , getterTypeBuffer);
class_addMethod(subclass, setterSel, _objc_msgForward, setterTypeBuffer);
free(type);
}
free(properties);
Ivar backingDictionaryIvar = class_getInstanceVariable(subclass, "_backingDictionary");
Ivar backingDictionaryMutableIvar = class_getInstanceVariable(subclass, "_backingDictionaryIsMutable");
class_addMethod(subclass, @selector(forwardingTargetForSelector:), imp_implementationWithBlock(^id (id self) {
return nil;
}), "@@:");
class_addMethod(subclass, @selector(forwardInvocation:), imp_implementationWithBlock(^void (id self, NSInvocation *invocation) {
SEL _cmd = [invocation selector];
objc_property_t property = class_getPropertyForSelector([self class], _cmd);
if (property == NULL) {
[self doesNotRecognizeSelector:_cmd];
return;
}
BOOL isGetter = (_cmd == property_getGetterSelector(property));
struct objc_property_attributes_t attributes = property_getPropertyAttributes(property);
NSString *propertyType = (__bridge_transfer NSString *) CFStringCreateWithCStringNoCopy(
NULL, property_copyAttributeValue(property, "T"), kCFStringEncodingUTF8, NULL
);
NSUInteger propertySize;
NSGetSizeAndAlignment([propertyType UTF8String], &propertySize, NULL);
void *dlsymKey = dlsym(RTLD_MAIN_ONLY, property_getName(property));
id dictionaryKey = *(__unsafe_unretained id *) dlsymKey;
NSMutableDictionary *backingDictionary = object_getIvar(self, backingDictionaryIvar);
NSNumber *isMutable = object_getIvar(self, backingDictionaryMutableIvar);
// Performing synchronization on nil is a no-op, see objc_sync.mm:306.
@synchronized (attributes.nonatomic ? nil : self) {
if (isGetter) {
id value = backingDictionary[dictionaryKey];
if (attributes.strong) {
[invocation setReturnValue:&value];
} else if (attributes.weak) {
value = [value nonretainedObjectValue];
[invocation setReturnValue:&value];
} else {
void *buffer = alloca(propertySize);
[value getValue:buffer];
[invocation setReturnValue:buffer];
}
} else {
if ((attributes.is_readonly || ![isMutable boolValue])) {
[self doesNotRecognizeSelector:_cmd];
return;
}
id dictionaryValue = nil;
void *newValue = alloca(propertySize);
[invocation getArgument:newValue atIndex:2];
if (attributes.strong) {
dictionaryValue = (__bridge id) newValue;
if (attributes.copy) {
dictionaryValue = [dictionaryValue copy];
}
} else if (attributes.weak) {
dictionaryValue = [NSValue valueWithNonretainedObject:(__bridge id) newValue];
} else {
dictionaryValue = [NSValue valueWithBytes:newValue objCType:[propertyType UTF8String]];
}
if (dictionaryValue == nil) {
[backingDictionary removeObjectForKey:dictionaryKey];
} else {
[backingDictionary setObject:dictionaryValue forKey:dictionaryKey];
}
}
}
}), "v@:@");
class_addMethod(subclass, @selector(initWithDictionary:mutable:), imp_implementationWithBlock(^id (id self, NSDictionary *dictionary, BOOL mutable) {
object_setIvar(self, backingDictionaryIvar, dictionary);
object_setIvar(self, backingDictionaryMutableIvar, @(mutable));
return self;
}), "@@:@c");
objc_registerClassPair(subclass);
return subclass;
}
}
+(id) dictionaryBackedObjectOfType:(Class)kls backingDictionary:(NSDictionary *)dictionary mutable:(BOOL)isMutable {
Class subclass = [self dictionaryBackedSubclassOfClass:kls];
return [[subclass alloc] initWithDictionary:dictionary mutable:isMutable];
}
@end