La mejor manera de serializar un NSData en una cadena hexadeximal

101

Estoy buscando una forma agradable de serializar un objeto NSData en una cadena hexadecimal. La idea es serializar el deviceToken usado para la notificación antes de enviarlo a mi servidor.

Tengo la siguiente implementación, pero creo que debe haber una forma más corta y agradable de hacerlo.

+ (NSString*) serializeDeviceToken:(NSData*) deviceToken
{
    NSMutableString *str = [NSMutableString stringWithCapacity:64];
    int length = [deviceToken length];
    char *bytes = malloc(sizeof(char) * length);

    [deviceToken getBytes:bytes length:length];

    for (int i = 0; i < length; i++)
    {
        [str appendFormat:@"%02.2hhX", bytes[i]];
    }
    free(bytes);

    return str;
}
sarfata
fuente

Respuestas:

206

Esta es una categoría aplicada a NSData que escribí. Devuelve un NSString hexadecimal que representa el NSData, donde los datos pueden tener cualquier longitud. Devuelve una cadena vacía si NSData está vacío.

NSData + Conversion.h

#import <Foundation/Foundation.h>

@interface NSData (NSData_Conversion)

#pragma mark - String Conversion
- (NSString *)hexadecimalString;

@end

NSData + Conversion.m

#import "NSData+Conversion.h"

@implementation NSData (NSData_Conversion)

#pragma mark - String Conversion
- (NSString *)hexadecimalString {
    /* Returns hexadecimal string of NSData. Empty string if data is empty.   */

    const unsigned char *dataBuffer = (const unsigned char *)[self bytes];

    if (!dataBuffer)
        return [NSString string];

    NSUInteger          dataLength  = [self length];
    NSMutableString     *hexString  = [NSMutableString stringWithCapacity:(dataLength * 2)];

    for (int i = 0; i < dataLength; ++i)
        [hexString appendString:[NSString stringWithFormat:@"%02lx", (unsigned long)dataBuffer[i]]];

    return [NSString stringWithString:hexString];
}

@end

Uso:

NSData *someData = ...;
NSString *someDataHexadecimalString = [someData hexadecimalString];

Esto es "probablemente" mejor que llamar [someData description]y luego quitar los espacios, <y>. Despojar a los personajes se siente demasiado "hacky". Además, nunca se sabe si Apple cambiará el formato de NSData -descriptionen el futuro.

NOTA: Algunas personas se comunicaron conmigo sobre la licencia del código en esta respuesta. Por la presente, dedico mis derechos de autor sobre el código que publiqué en esta respuesta al dominio público.

Dave
fuente
4
Bien, pero dos sugerencias: (1) Creo que appendFormat es más eficiente para datos grandes, ya que evita la creación de un NSString intermedio y (2)% x representa un unsigned int en lugar de unsigned long, aunque la diferencia es inofensiva.
svachalek
No quiero ser un detractor, ya que esta es una buena solución que es fácil de usar, pero mi solución del 25 de enero es mucho más eficiente. Si está buscando una respuesta optimizada para el rendimiento, consulte esa respuesta . Votar a favor esta respuesta como una solución agradable y fácil de entender.
NSProgrammer
5
Tuve que eliminar el elenco (largo sin firmar) y usar @ "% 02hhx" como cadena de formato para que esto funcione.
Anton
1
Correcto, según developer.apple.com/library/ios/documentation/cocoa/conceptual/… el formato debe ser "%02lx"con ese elenco, o lanzar a (unsigned int), o quitar el elenco y usar @"%02hhx":)
qix
1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];es mucho mejor (menor huella de memoria)
Marek R
31

Aquí hay un método de categoría NSData altamente optimizado para generar una cadena hexadecimal. Si bien la respuesta de @Dave Gallagher es suficiente para un tamaño relativamente pequeño, el rendimiento de la memoria y la CPU se deteriora para grandes cantidades de datos. Hice un perfil de esto con un archivo de 2 MB en mi iPhone 5. La comparación de tiempo fue 0.05 vs 12 segundos. La huella de memoria es insignificante con este método, mientras que el otro método aumentó el volumen a 70 MB.

- (NSString *) hexString
{
    NSUInteger bytesCount = self.length;
    if (bytesCount) {
        const char *hexChars = "0123456789ABCDEF";
        const unsigned char *dataBuffer = self.bytes;
        char *chars = malloc(sizeof(char) * (bytesCount * 2 + 1));       
        if (chars == NULL) {
            // malloc returns null if attempting to allocate more memory than the system can provide. Thanks Cœur
            [NSException raise:NSInternalInconsistencyException format:@"Failed to allocate more memory" arguments:nil];
            return nil;
        }
        char *s = chars;
        for (unsigned i = 0; i < bytesCount; ++i) {
            *s++ = hexChars[((*dataBuffer & 0xF0) >> 4)];
            *s++ = hexChars[(*dataBuffer & 0x0F)];
            dataBuffer++;
        }
        *s = '\0';
        NSString *hexString = [NSString stringWithUTF8String:chars];
        free(chars);
        return hexString;
    }
    return @"";
}
Pedro
fuente
Buen @Peter - Sin embargo, hay una solución aún más rápida (no mucho que la suya), justo debajo;)
Moose
1
@Moose, haga referencia con más precisión a qué respuesta está hablando: los votos y las nuevas respuestas pueden afectar el posicionamiento de las respuestas. [editar: oh, déjame adivinar, te refieres a tu propia respuesta ...]
Cœur
1
Se agregó una verificación nula de malloc. Gracias @ Cœur.
Peter
17

El uso de la propiedad de descripción de NSData no debe considerarse un mecanismo aceptable para la codificación HEX de la cadena. Esa propiedad es solo para descripción y puede cambiar en cualquier momento. Como nota, antes de iOS, la propiedad de descripción NSData ni siquiera devolvía sus datos en forma hexadecimal.

Lo siento por insistir en la solución, pero es importante tomarse la energía para serializarla sin tener que recurrir a una API que está destinada a algo más que a la serialización de datos.

@implementation NSData (Hex)

- (NSString*)hexString
{
    NSUInteger length = self.length;
    unichar* hexChars = (unichar*)malloc(sizeof(unichar) * (length*2));
    unsigned char* bytes = (unsigned char*)self.bytes;
    for (NSUInteger i = 0; i < length; i++) {
        unichar c = bytes[i] / 16;
        if (c < 10) {
            c += '0';
        } else {
            c += 'A' - 10;
        }
        hexChars[i*2] = c;

        c = bytes[i] % 16;
        if (c < 10) {
            c += '0';
        } else {
            c += 'A' - 10;
        }
        hexChars[i*2+1] = c;
    }
    NSString* retVal = [[NSString alloc] initWithCharactersNoCopy:hexChars length:length*2 freeWhenDone:YES];
    return [retVal autorelease];
}

@end
NSProgrammer
fuente
sin embargo, debe liberar (hexChars) antes de la devolución.
karim
3
@karim, eso es incorrecto. Al usar initWithCharactersNoCopy: length: freeWhenDone: y tener freeWhenDone en YES, NSString tomará el control de ese búfer de bytes. Llamar gratis (hexChars) resultará en un bloqueo. El beneficio aquí es sustancial ya que NSString no tendrá que realizar una costosa llamada a Memcpy.
NSProgrammer
@NSProgrammer gracias. No noté el inicializador NSSting.
karim
La documentación indica que descriptiondevuelve una cadena codificada en hexadecimal, por lo que me parece razonable.
Poco común
¿No deberíamos comprobar si el valor devuelto por malloc es potencialmente nulo?
Cœur
9

A continuación, se muestra una forma más rápida de realizar la conversión:

BenchMark (tiempo medio para una conversión de datos de 1024 bytes repetida 100 veces):

Dave Gallagher: ~ 8.070 ms
NS Programador: ~ 0.077 ms
Peter: ~ 0.031 ms
Este: ~ 0.017 ms

@implementation NSData (BytesExtras)

static char _NSData_BytesConversionString_[512] = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff";

-(NSString*)bytesString
{
    UInt16*  mapping = (UInt16*)_NSData_BytesConversionString_;
    register UInt16 len = self.length;
    char*    hexChars = (char*)malloc( sizeof(char) * (len*2) );

    // --- Coeur's contribution - a safe way to check the allocation
    if (hexChars == NULL) {
    // we directly raise an exception instead of using NSAssert to make sure assertion is not disabled as this is irrecoverable
        [NSException raise:@"NSInternalInconsistencyException" format:@"failed malloc" arguments:nil];
        return nil;
    }
    // ---

    register UInt16* dst = ((UInt16*)hexChars) + len-1;
    register unsigned char* src = (unsigned char*)self.bytes + len-1;

    while (len--) *dst-- = mapping[*src--];

    NSString* retVal = [[NSString alloc] initWithBytesNoCopy:hexChars length:self.length*2 encoding:NSASCIIStringEncoding freeWhenDone:YES];
#if (!__has_feature(objc_arc))
   return [retVal autorelease];
#else
    return retVal;
#endif
}

@end
Alce
fuente
1
Puede ver cómo implementé la verificación de malloc aquí ( _hexStringmétodo): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
Cœur
Gracias por la referencia - Por cierto, me gusta el 'demasiado extenso' - Eso es cierto, pero ahora lo escribí, cualquiera puede copiar / pegar - Estoy bromeando - Lo he generado - Ya lo sabías :) Tienes razón lo largo, ¡solo estaba tratando de golpear en todas partes donde puedo ganar microsegundos! Divide las iteraciones de bucle por 2. Pero admito que carece de elegancia. Bye
Moose
8

Versión funcional Swift

Un trazador de líneas:

let hexString = UnsafeBufferPointer<UInt8>(start: UnsafePointer(data.bytes),
count: data.length).map { String(format: "%02x", $0) }.joinWithSeparator("")

Aquí está en un formulario de extensión reutilizable y autodocumentado:

extension NSData {
    func base16EncodedString(uppercase uppercase: Bool = false) -> String {
        let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(self.bytes),
                                                count: self.length)
        let hexFormat = uppercase ? "X" : "x"
        let formatString = "%02\(hexFormat)"
        let bytesAsHexStrings = buffer.map {
            String(format: formatString, $0)
        }
        return bytesAsHexStrings.joinWithSeparator("")
    }
}

Alternativamente, use en reduce("", combine: +)lugar de joinWithSeparator("")ser visto como un maestro funcional por sus compañeros.


Editar: cambié String ($ 0, base: 16) a String (formato: "% 02x", $ 0), porque se necesitaban números de un dígito para tener un cero de relleno

NiñoScript
fuente
7

La respuesta de Peter se transfirió a Swift.

func hexString(data:NSData)->String{
    if data.length > 0 {
        let  hexChars = Array("0123456789abcdef".utf8) as [UInt8];
        let buf = UnsafeBufferPointer<UInt8>(start: UnsafePointer(data.bytes), count: data.length);
        var output = [UInt8](count: data.length*2 + 1, repeatedValue: 0);
        var ix:Int = 0;
        for b in buf {
            let hi  = Int((b & 0xf0) >> 4);
            let low = Int(b & 0x0f);
            output[ix++] = hexChars[ hi];
            output[ix++] = hexChars[low];
        }
        let result = String.fromCString(UnsafePointer(output))!;
        return result;
    }
    return "";
}

swift3

func hexString()->String{
    if count > 0 {
        let hexChars = Array("0123456789abcdef".utf8) as [UInt8];
        return withUnsafeBytes({ (bytes:UnsafePointer<UInt8>) -> String in
            let buf = UnsafeBufferPointer<UInt8>(start: bytes, count: self.count);
            var output = [UInt8](repeating: 0, count: self.count*2 + 1);
            var ix:Int = 0;
            for b in buf {
                let hi  = Int((b & 0xf0) >> 4);
                let low = Int(b & 0x0f);
                output[ix] = hexChars[ hi];
                ix += 1;
                output[ix] = hexChars[low];
                ix += 1;
            }
            return String(cString: UnsafePointer(output));
        })
    }
    return "";
}

Rápido 5

func hexString()->String{
    if count > 0 {
        let hexChars = Array("0123456789abcdef".utf8) as [UInt8];
        return withUnsafeBytes{ bytes->String in
            var output = [UInt8](repeating: 0, count: bytes.count*2 + 1);
            var ix:Int = 0;
            for b in bytes {
                let hi  = Int((b & 0xf0) >> 4);
                let low = Int(b & 0x0f);
                output[ix] = hexChars[ hi];
                ix += 1;
                output[ix] = hexChars[low];
                ix += 1;
            }
            return String(cString: UnsafePointer(output));
        }
    }
    return "";
}
john07
fuente
4

Necesitaba resolver este problema y las respuestas aquí me resultaron muy útiles, pero me preocupa el rendimiento. La mayoría de estas respuestas implican copiar los datos a granel de NSData, así que escribí lo siguiente para hacer la conversión con poca sobrecarga:

@interface NSData (HexString)
@end

@implementation NSData (HexString)

- (NSString *)hexString {
    NSMutableString *string = [NSMutableString stringWithCapacity:self.length * 3];
    [self enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop){
        for (NSUInteger offset = 0; offset < byteRange.length; ++offset) {
            uint8_t byte = ((const uint8_t *)bytes)[offset];
            if (string.length == 0)
                [string appendFormat:@"%02X", byte];
            else
                [string appendFormat:@" %02X", byte];
        }
    }];
    return string;
}

Esto preasigna espacio en la cadena para todo el resultado y evita copiar el contenido de NSData utilizando enumerateByteRangesUsingBlock. Cambiar la X por una x en la cadena de formato usará dígitos hexadecimales en minúscula. Si no desea un separador entre los bytes, puede reducir la declaración

if (string.length == 0)
    [string appendFormat:@"%02X", byte];
else
    [string appendFormat:@" %02X", byte];

hasta solo

[string appendFormat:@"%02X", byte];
Juan Esteban
fuente
2
Creo que el índice para recuperar el valor del byte necesita un ajuste, porque NSRangeindica el rango dentro de la NSDatarepresentación más grande , no dentro del búfer de bytes más pequeño (ese primer parámetro del bloque suministrado enumerateByteRangesUsingBlock) que representa una sola porción contigua del más grande NSData. Por lo tanto, byteRange.lengthrefleja el tamaño del búfer de bytes, pero byteRange.locationes la ubicación dentro del mayor NSData. Por lo tanto, desea usar simplemente offset, no byteRange.location + offset, para recuperar el byte.
Rob
1
@Rob Gracias, veo lo que quieres decir y he ajustado el código
John Stephen
1
Si modifica la declaración hacia abajo para usar el single appendFormat, probablemente también debería cambiar self.length * 3aself.length * 2
T. Colligan
1

Necesitaba una respuesta que funcionara para cadenas de longitud variable, así que esto es lo que hice:

+ (NSString *)stringWithHexFromData:(NSData *)data
{
    NSString *result = [[data description] stringByReplacingOccurrencesOfString:@" " withString:@""];
    result = [result substringWithRange:NSMakeRange(1, [result length] - 2)];
    return result;
}

Funciona muy bien como una extensión para la clase NSString.

BadPirate
fuente
1
¿Qué pasa si Apple cambia la forma en que representan la descripción?
Brenden
1
en el método de descripción de iOS13 devuelve un formato diferente.
nacho4d
1

Siempre puede usar [yourString uppercaseString] para poner letras en mayúscula en la descripción de los datos

Rostyslav Bachyk
fuente
1

Una mejor manera de serializar / deserializar NSData en NSString es usar el codificador / decodificador Google Toolbox para Mac Base64. Simplemente arrastre a su proyecto de aplicación los archivos GTMBase64.m, GTMBase64.he GTMDefines.h del paquete Foundation y haga algo como

/**
 * Serialize NSData to Base64 encoded NSString
 */
-(void) serialize:(NSData*)data {

    self.encodedData = [GTMBase64 stringByEncodingData:data];

}

/**
 * Deserialize Base64 NSString to NSData
 */
-(NSData*) deserialize {

    return [GTMBase64 decodeString:self.encodedData];

}
Loretoparisi
fuente
Mirando el código fuente , parece que la clase que lo proporciona ahora es GTMStringEncoding. No lo he probado, pero parece una gran nueva solución a esta pregunta.
sarfata
1
A partir de Mac OS X 10.6 / iOS 4.0, NSData realiza codificación Base-64. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc
@jrc eso es cierto, pero considere codificar cadenas de trabajo reales en Base-64. Tienes que lidiar con la codificación "segura para la web", que no tienes en iOS / MacOS, como en GTMBase64 # webSafeEncodeData. También es posible que deba agregar / eliminar el "relleno" de Base64, por lo que también tiene esta opción: GTMBase64 # stringByWebSafeEncodingData: (NSData *) data padded: (BOOL) padded;
loretoparisi
1

Aquí hay una solución usando Swift 3

extension Data {

    public var hexadecimalString : String {
        var str = ""
        enumerateBytes { buffer, index, stop in
            for byte in buffer {
                str.append(String(format:"%02x",byte))
            }
        }
        return str
    }

}

extension NSData {

    public var hexadecimalString : String {
        return (self as Data).hexadecimalString
    }

}
Alex
fuente
0
@implementation NSData (Extn)

- (NSString *)description
{
    NSMutableString *str = [[NSMutableString alloc] init];
    const char *bytes = self.bytes;
    for (int i = 0; i < [self length]; i++) {
        [str appendFormat:@"%02hhX ", bytes[i]];
    }
    return [str autorelease];
}

@end

Now you can call NSLog(@"hex value: %@", data)
Ramesh
fuente
0

Cambie %08xa %08Xpara obtener caracteres en mayúscula.

Dan Reese
fuente
6
esto sería mejor como comentario ya que no incluyó ningún contexto. Solo digo
Brenden
0

Swift + Propiedad.

Prefiero tener una representación hexadecimal como propiedad (lo mismo que bytesy descriptionpropiedades):

extension NSData {

    var hexString: String {

        let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(self.bytes), count: self.length)
        return buffer.map { String(format: "%02x", $0) }.joinWithSeparator("")
    }

    var heXString: String {

        let buffer = UnsafeBufferPointer<UInt8>(start: UnsafePointer(self.bytes), count: self.length)
        return buffer.map { String(format: "%02X", $0) }.joinWithSeparator("")
    }
}

La idea se toma prestada de esta respuesta

Avt
fuente
-4
[deviceToken description]

Deberá eliminar los espacios.

Personalmente base64codifico el deviceToken, pero es cuestión de gustos.

Eddie
fuente
Esto no obtiene el mismo resultado. descripción devuelve: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Mientras estoy buscando: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata