我如何用字典支持一堆类属性?

时间:2015-03-20 01:59:54

标签: ios objective-c macos objective-c-runtime

我有一个类,它在字典中保存了很好定义键的属性。我想用一个类替换这个属性字典,让它称之为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)要求我有一个(属性名称)@" Foo" - > (变量名)Foo和2)导致两者属性被设置设置的字典值实际上我只想设置字典。

我知道我可以这样做:https://github.com/iosptl/ios6ptl/blob/master/ch28/Person/Person/Person.m 但那会1)仍然要求我有一个映射,2)要求我为每个属性都有一个@dynamic。

有更自动的方法吗?

由于

3 个答案:

答案 0 :(得分:1)

要使用动态生成的访问器方法,如您链接的Person代码所示,无需@dynamic,您可以在类的类别中声明属性,而不是类本身:

@interface AttributeSet : NSObject

// ... no properties here ...

@end

@interface AttributeSet (YourPropertiesCategoryName)

@property(strong) NSString *Foo;

...a ton more

@end

编译器将自动合成在类本身或类扩展中声明的属性(类似于没有类别名称的类别),但不适用于类别。

请注意,您不需要也不应该为该类别提供实施。 (如果你这样做,编译器会抱怨属性缺乏实现。它不会自动合成它们,但是你仍然需要使用@dynamic来消除警告。)

答案 1 :(得分:1)

经过一段时间后,我想我已经为您提出了相当可扩展的解决方案。您需要的只是使用以下帮助程序类创建对象,如下所示:

#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]);
}

注意:此实现远非完美,它确实使用了“可怕的”dlsym API,这意味着,如果您希望使用此类,则无法从可执行文件中删除符号。此外,如果将其提交到应用商店,可能会导致拒绝。如果您希望找到解决方法,还有其他方法可以自动确定要与字典一起使用的密钥。

此实现确实支持struct属性,以及弱,复制和原子属性。它将比在普通对象上设置属性要慢得多,因为这会通过objective-c的转发API(支持struct返回所需)。

希望这可以帮助你,我当然有很多乐趣。

<强> 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

答案 2 :(得分:0)

Rob Napier的例子是一个不错的选择;编译器会为你生成访问器,除非你不告诉它,以及你用@dynamic指令告诉它的方式。

另一种选择是自动代码生成:编写脚本为您的setter发出ObjC代码。

我能想到的第三个是overwriting the accessors during runtime。在您的班级+initialize中,您可以从运行时库中获取其属性列表,并使用class_replaceMethod()插入您自己的使用词典而非ivars的访问者。这将需要一些字符串修改来获取彼此的访问者名称和密钥。

这里有一个关于最后一个选项演示的要点:https://gist.github.com/woolsweater/4fb874b15449ee7fd7e8