Introspección / Reflexión de Objective-C

79

¿Existe un método, función, API, forma comúnmente aceptada, etc., incorporada para volcar el contenido de un objeto instanciado en Objective-C, específicamente en el entorno Cocoa / Cocoa-Touch de Apple?

Quiero poder hacer algo como

MyType *the_thing = [[MyType alloc] init];
NSString *the_dump = [the_thing dump]; //pseudo code
NSLog("Dumped Contents: %@", the_dump);

y mostrar los nombres y valores de las variables de instancia del objeto, junto con cualquier método disponible para llamar en tiempo de ejecución. Idealmente en un formato fácil de leer.

Para los desarrolladores familiarizados con PHP, básicamente estoy buscando el equivalente de las funciones de reflexión ( var_dump(), get_class_methods()) y la API de reflexión de OO.

Alan Storm
fuente
Me hago la misma pregunta hay pocos días. gracias por esta pregunta.
Yannick Loriot
7
Sí, gran pregunta. Una de las mayores ventajas de ObjC sobre otros lenguajes similares es su increíble sistema de tiempo de ejecución dinámico que le permite hacer cosas increíbles como esta. Desafortunadamente, la gente rara vez lo usa en todo su potencial, así que felicitaciones por enseñarle a la comunidad SO con su pregunta.
rpj
He creado una biblioteca ligera para tratar la reflexión OSReflectionKit . Con esta biblioteca, puede simplemente llamar a [the_thing fullDescription].
Alexandre OS
GRAN Pregunta !! - ¿Tiene alguna idea de cómo establecer la propiedad real una vez que la haya encontrado usando Reflection? Tengo una pregunta aquí: stackoverflow.com/q/25538890/1735836
Patricia

Respuestas:

112

ACTUALIZACIÓN: Cualquiera que desee hacer este tipo de cosas puede querer consultar el contenedor ObjC de Mike Ash para el tiempo de ejecución de Objective-C .

Así es más o menos cómo lo haría:

#import <objc/runtime.h>

. . . 

-(void)dumpInfo
{
    Class clazz = [self class];
    u_int count;

    Ivar* ivars = class_copyIvarList(clazz, &count);
    NSMutableArray* ivarArray = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count ; i++)
    {
        const char* ivarName = ivar_getName(ivars[i]);
        [ivarArray addObject:[NSString  stringWithCString:ivarName encoding:NSUTF8StringEncoding]];
    }
    free(ivars);

    objc_property_t* properties = class_copyPropertyList(clazz, &count);
    NSMutableArray* propertyArray = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count ; i++)
    {
        const char* propertyName = property_getName(properties[i]);
        [propertyArray addObject:[NSString  stringWithCString:propertyName encoding:NSUTF8StringEncoding]];
    }
    free(properties);

    Method* methods = class_copyMethodList(clazz, &count);
    NSMutableArray* methodArray = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count ; i++)
    {
        SEL selector = method_getName(methods[i]);
        const char* methodName = sel_getName(selector);
        [methodArray addObject:[NSString  stringWithCString:methodName encoding:NSUTF8StringEncoding]];
    }
    free(methods);

    NSDictionary* classDump = [NSDictionary dictionaryWithObjectsAndKeys:
                               ivarArray, @"ivars",
                               propertyArray, @"properties",
                               methodArray, @"methods",
                               nil];

    NSLog(@"%@", classDump);
}

A partir de ahí, es fácil obtener los valores reales de las propiedades de una instancia, pero debe verificar si son tipos u objetos primitivos, así que fui demasiado perezoso para ponerlo. También puede optar por escanear la cadena de herencia para obtener todas las propiedades definidas en un objeto. Luego están los métodos definidos por categorías, y más ... Pero casi todo está disponible.

Aquí hay un extracto de lo que el código anterior volca para UILabel:

{
    ivars =     (
        "_size",
        "_text",
        "_color",
        "_highlightedColor",
        "_shadowColor",
        "_font",
        "_shadowOffset",
        "_minFontSize",
        "_actualFontSize",
        "_numberOfLines",
        "_lastLineBaseline",
        "_lineSpacing",
        "_textLabelFlags"
    );
    methods =     (
        rawSize,
        "setRawSize:",
        "drawContentsInRect:",
        "textRectForBounds:",
        "textSizeForWidth:",
        . . .
    );
    properties =     (
        text,
        font,
        textColor,
        shadowColor,
        shadowOffset,
        textAlignment,
        lineBreakMode,
        highlightedTextColor,
        highlighted,
        enabled,
        numberOfLines,
        adjustsFontSizeToFitWidth,
        minimumFontSize,
        baselineAdjustment,
        "_lastLineBaseline",
        lineSpacing,
        userInteractionEnabled
    );
}
Felixyz
fuente
6
+1 así es como lo haría yo (convertirlo en una NSObjectcategoría). Sin embargo, hay que free()tus ivars, propertiesy methodsmatrices, o de lo contrario les está goteando.
Dave DeLong
Woops, lo olvidé. Ponlos ahora. Gracias.
Felixyz
4
¿No podemos ver los valores de los ivars?
Samhan Salahuddin
2
... Y sus tipos. Noté que dijiste que se podía hacer, pero definitivamente pareces tener una mejor comprensión de esto que yo. ¿Sería capaz de actualizar esto en algún momento con el código para obtener los valores y tipos de los ivars y las propiedades?
GeneralMike
Con ARC, ¿sigue siendo necesario liberar ivars, propiedades y matrices de métodos, etc.?
Chuck Krutsinger
12

descriptionAparte del método (como .toString () en Java), no he oído hablar de uno que esté integrado, pero no sería demasiado difícil crear uno. Objective-C Runtime Reference tiene un montón de funciones que puede utilizar para obtener información sobre las variables de instancia, métodos, propiedades, etc. de un objeto.

Dave DeLong
fuente
+1 para la información, justo el tipo de cosas que estaba buscando. Dicho esto, esperaba una solución prediseñada, ya que soy lo suficientemente nuevo en el lenguaje como para que cualquier cosa que hackearía rápidamente podría pasar por alto algo importante del lenguaje.
Alan Storm
7
Si va a rechazar una respuesta, tenga la cortesía de dejar un comentario por qué.
Dave DeLong
8

Esto es lo que estoy usando actualmente para imprimir automáticamente las variables de clase, en una biblioteca para un eventual lanzamiento público: funciona volcando todas las propiedades de la clase de instancia hasta el final del árbol de herencia. Gracias a KVC, no necesita preocuparse si una propiedad es de tipo primitivo o no (para la mayoría de los tipos).

// Finds all properties of an object, and prints each one out as part of a string describing the class.
+ (NSString *) autoDescribe:(id)instance classType:(Class)classType
{
    NSUInteger count;
    objc_property_t *propList = class_copyPropertyList(classType, &count);
    NSMutableString *propPrint = [NSMutableString string];

    for ( int i = 0; i < count; i++ )
    {
        objc_property_t property = propList[i];

        const char *propName = property_getName(property);
        NSString *propNameString =[NSString stringWithCString:propName encoding:NSASCIIStringEncoding];

        if(propName) 
        {
            id value = [instance valueForKey:propNameString];
            [propPrint appendString:[NSString stringWithFormat:@"%@=%@ ; ", propNameString, value]];
        }
    }
    free(propList);


    // Now see if we need to map any superclasses as well.
    Class superClass = class_getSuperclass( classType );
    if ( superClass != nil && ! [superClass isEqual:[NSObject class]] )
    {
        NSString *superString = [self autoDescribe:instance classType:superClass];
        [propPrint appendString:superString];
    }

    return propPrint;
}

+ (NSString *) autoDescribe:(id)instance
{
    NSString *headerString = [NSString stringWithFormat:@"%@:%p:: ",[instance class], instance];
    return [headerString stringByAppendingString:[self autoDescribe:instance classType:[instance class]]];
}
Kendall Helmstetter Gelner
fuente
+1 muy útil, aunque solo responde la pregunta en parte. ¿Agregará un comentario aquí cuando publique la biblioteca?
Felixyz
@KendallHelmstetterGelner - ¡¡GRAN INFORMACIÓN !! ¿Tiene alguna idea de cómo establecer la propiedad real una vez que la haya encontrado usando Reflection? Tengo una pregunta aquí: stackoverflow.com/q/25538890/1735836
Patricia
@Lucy, cualquiera de las cadenas que obtenga usando esta técnica se puede usar en [myObject setValue: myValue forKey: propertyName]. La técnica que describí anteriormente (usando la lista de propiedades) es la reflexión ... también puede usar la matriz objc_property_t para llamar directamente a los métodos de propiedad, pero setValue: forKey: es más fácil de usar y funciona igual de bien.
Kendall Helmstetter Gelner
4

Hice un par de ajustes en el código de Kendall para imprimir valores de propiedad, lo que me resultó muy útil. Lo definí como un método de instancia en lugar de un método de clase, ya que así lo llama la recursividad de superclase. También agregué manejo de excepciones para propiedades que no cumplen con KVO y agregué saltos de línea a la salida para facilitar la lectura (y la diferencia):

-(NSString *) autoDescribe:(id)instance classType:(Class)classType
{
    NSUInteger count;
    objc_property_t *propList = class_copyPropertyList(classType, &count);
    NSMutableString *propPrint = [NSMutableString string];

    for ( int i = 0; i < count; i++ )
    {
        objc_property_t property = propList[i];

        const char *propName = property_getName(property);
        NSString *propNameString =[NSString stringWithCString:propName encoding:NSASCIIStringEncoding];

        if(propName) 
        {
         @try {
            id value = [instance valueForKey:propNameString];
            [propPrint appendString:[NSString stringWithFormat:@"%@=%@\n", propNameString, value]];
         }
         @catch (NSException *exception) {
            [propPrint appendString:[NSString stringWithFormat:@"Can't get value for property %@ through KVO\n", propNameString]];
         }
        }
    }
    free(propList);


    // Now see if we need to map any superclasses as well.
    Class superClass = class_getSuperclass( classType );
    if ( superClass != nil && ! [superClass isEqual:[NSObject class]] )
    {
        NSString *superString = [self autoDescribe:instance classType:superClass];
        [propPrint appendString:superString];
    }

    return propPrint;
}
Christopher Pickslay
fuente
EXCELENTE INFORMACIÓN !!! - ¿Tiene alguna idea de cómo establecer la propiedad real una vez que la haya encontrado usando Reflection? Tengo una pregunta aquí: stackoverflow.com/q/25538890/1735836
Patricia
3

Honestamente, la herramienta adecuada para este trabajo es el depurador de Xcode. Tiene toda esta información fácilmente accesible de forma visual. Tómate el tiempo para aprender a usarlo, es una herramienta realmente poderosa.

Más información:

Usando el depurador

Guía de depuración de Xcode obsoleta : archivada por Apple

Acerca de la depuración con Xcode : archivado por Apple

Acerca de LLDB y depuración : archivado por Apple

Depuración con GDB : archivado por Apple

Guía de depuración de SpriteKit : archivada por Apple

Temas de programación de depuración para Core Foundation : archivado por Apple

Colin Barrett
fuente
4
+1 para obtener buena información, pero a veces es más rápido volcar el estado de un objeto en dos puntos y diferenciar la salida de lo que comienza en el estado de un programa en un solo punto en el tiempo.
Alan Storm
1

He hecho cocoapod con esto, https://github.com/neoneye/autodescribe

Modifiqué el código de Christopher Pickslay y lo convertí en una categoría en NSObject y también le agregué una prueba unitaria. Así es como se usa:

@interface TestPerson : NSObject

@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSNumber *age;

@end

@implementation TestPerson

// empty

@end

@implementation NSObject_AutoDescribeTests

-(void)test0 {
    TestPerson *person = [TestPerson new];
    person.firstName = @"John";
    person.lastName = @"Doe";
    person.age = [NSNumber numberWithFloat:33.33];
    NSString *actual = [person autoDescribe];
    NSString *expected = @"firstName=John\nlastName=Doe\nage=33.33";
    STAssertEqualObjects(actual, expected, nil);
}

@end
neoneye
fuente
0

Estoy confundido con Introspección y Refección antes, así que obtenga información a continuación.

La introspección es la capacidad que tiene el objeto de verificar qué tipo es, o el protocolo al que se conforma, o el selector al que puede responder. La API de objc como isKindOfClass/ isMemberOfClass/ conformsToProtocol/ respondsToSelectoretc.

La capacidad de reflexión va más allá de la introspección, no solo puede obtener información del objeto, sino que también puede operar metadatos, propiedades y funciones del objeto. como object_setClasspuede modificar el tipo de objeto.

Jone
fuente