Home > Blockchain >  ObjectC-Why can't I get the properties correctly using the class_copyPropertyList function?
ObjectC-Why can't I get the properties correctly using the class_copyPropertyList function?

Time:03-02

  • macOS 11.5.2
  • Xcode 13.2.1
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <iostream>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class clazz = NSClassFromString(@"NSString");
        uint32_t count = 0;
        objc_property_t* properties = class_copyPropertyList(clazz, &count);
        for (uint32_t i = 0; i < count; i  ){
            const char* name = property_getName(properties[i]);
            std::cout << name << std::endl;
        }
        free(properties);
    }
    return 0;
}

I will take some snippets of the output:

hash
superclass
description
debugDescription
hash
superclass
description
debugDescription
vertexID
sha224
NS_isSourceOver
hash
superclass
description
debugDescription
...

From the output, we can find that properties such as hash, description, superclass, etc. will appear repeatedly several times, while some properties (such as UTF8String) do not appear in the result list. How should I get the list of properties correctly? I would appreciate it.

CodePudding user response:

The reason you're not seeing UTF8String come up as a property is that it's not declared as a property in the main declaration of NSString, but rather in a category. On macOS 12.2.1/Xcode 13.2.1, the declaration of NSString boils down to this:

@interface NSString : NSObject <NSCopying, NSMutableCopying, NSSecureCoding>
@property (readonly) NSUInteger length;
- (unichar)characterAtIndex:(NSUInteger)index;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@end

All other properties and methods on NSString are declared in categories immediately afterwards:

@interface NSString (NSStringExtensionMethods)

#pragma mark *** Substrings ***

/* To avoid breaking up character sequences such as Emoji, you can do:
    [str substringFromIndex:[str rangeOfComposedCharacterSequenceAtIndex:index].location]
    [str substringToIndex:NSMaxRange([str rangeOfComposedCharacterSequenceAtIndex:index])]
    [str substringWithRange:[str rangeOfComposedCharacterSequencesForRange:range]
*/
- (NSString *)substringFromIndex:(NSUInteger)from;
- (NSString *)substringToIndex:(NSUInteger)to;

// ...

@property (nullable, readonly) const char *UTF8String NS_RETURNS_INNER_POINTER; // Convenience to return null-terminated UTF8 representation

// ...

@end

When a property is declared in a category on a type like this, it doesn't get emitted as an actual Obj-C property because categories can only add methods to classes, and not instance variables. When a category declares a property on a type, it must be backed by a method and not a traditional property.

You can see this with a custom class, too — on my machine,

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface MyClass: NSObject
@property (nullable, readonly) const char *direct_UTF8String NS_RETURNS_INNER_POINTER;
@end

@interface MyClass (Extensions)
@property (nullable, readonly) const char *category_UTF8String NS_RETURNS_INNER_POINTER;
@end

@implementation MyClass
- (const char *)direct_UTF8String {
    return "Hello, world!";
}

- (const char *)category_UTF8String {
    return "Hi there!";
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class clazz = NSClassFromString(@"MyClass");
        
        printf("%s properties:\n", class_getName(clazz));
        uint32_t count = 0;
        objc_property_t* properties = class_copyPropertyList(clazz, &count);
        for (uint32_t i = 0; i < count; i  ){
            printf("%s\n", property_getName(properties[i]));
        }
        
        free(properties);
        
        puts("-----------------------------------------------");
        
        printf("%s methods:\n", class_getName(clazz));
        Method *methods = class_copyMethodList(clazz, &count);
        for (uint32_t i = 0; i < count; i  ) {
            SEL name = method_getName(methods[i]);
            printf("%s\n", sel_getName(name));
        }
        
        free(methods);
    }
    
    return 0;
}

outputs

MyClass properties:
direct_UTF8String
-----------------------------------------------
MyClass methods:
direct_UTF8String
category_UTF8String

If you remove the actual implementations of the *UTF8String methods from the class, the property remains declared, but the category method disappears (because it doesn't actually have a synthesized implementation because of how categories work):

MyClass properties:
direct_UTF8String
-----------------------------------------------
MyClass methods:
direct_UTF8String

As for how to adjust to this: it depends on what purpose you're trying to fetch properties for, and why you might need UTF8String specifically.

CodePudding user response:

NSString declares in its interface it implements methods, but it does not actually implement them, that is why when you print at runtime a list of the its methods it does not print what you expect. The methods are implemented by other private classes, and when you initialize a new instance of NSString, instead of getting an instance of NSString you get an instance of that private class that have the actual implementation.

You can see that by printing the class type of a string, the following prints NSCFString or NSTaggedPointerString, not NSString:

NSString* aString = [NSString stringWithFormat: @"something"];
NSLog(@"%@", [aString class]);

And this prints __NSCFConstantString:

NSLog(@"%@", [@"a constant string" class]);

This pattern is called a class cluster pattern. If you modify to dump the methods of the NSCFString you will get a "redactedDescription", it seems you are prevented to query these classes.

  • Related