我如何用字典支持一堆 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