【问题标题】:How would I back a bunch of class properties with a dictionary?我如何用字典支持一堆类属性?
【发布时间】:2015-03-20 01:59:54
【问题描述】:

我有一个类在字典中保存属性,其中键定义明确。我想用一个类来替换这个属性字典,我们称之为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) 要求我拥有 (property name) @"Foo" --> (variable name) Foo 和 2) 的映射 设置的属性设置的字典值,而实际上我只想设置字典。

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

有没有更自动的方法来做到这一点?

谢谢

【问题讨论】:

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


【解决方案1】:

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

@interface AttributeSet : NSObject

// ... no properties here ...

@end

@interface AttributeSet (YourPropertiesCategoryName)

@property(strong) NSString *Foo;

...a ton more

@end

编译器将自动合成在类本身或类扩展中声明的属性(看起来像没有类别名称的类别),但不是针对类别。

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

【讨论】:

    【解决方案2】:

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

    #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,也就是说,如果你想使用这个类,你不能从可执行文件中删除你的符号。此外,如果将其提交到应用商店,可能会导致拒绝。但是,如果您希望找到解决方法,还有其他方法可以自动确定要与字典一起使用的键。

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

    希望这对您有所帮助,我确实玩得很开心。

    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
    

    【讨论】:

    • /me 提供秘密 wacko-overkill-runtime-hacking 俱乐部敬礼
    【解决方案3】:

    Rob Napier 的例子是一个不错的选择;编译器将为您生成访问器,除非您告诉它不要这样做,而您告诉它的方式是使用 @dynamic 指令。

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

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

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

    【讨论】:

    • 我最终按照您的建议编写了一个脚本来执行此操作。谢谢!
    • 酷,很高兴我能帮上忙。
    猜你喜欢
    • 2020-10-04
    • 2019-09-12
    • 2022-11-19
    • 1970-01-01
    • 2020-07-21
    • 2013-01-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多