Il modo migliore per serializzare un NSData in una stringa esadossimale


101

Sto cercando un modo piacevole per serializzare un oggetto NSData in una stringa esadecimale. L'idea è di serializzare il deviceToken utilizzato per la notifica prima di inviarlo al mio server.

Ho la seguente implementazione, ma penso che ci debba essere un modo più breve e più carino per farlo.

+ (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;
}

Risposte:


206

Questa è una categoria applicata a NSData che ho scritto. Restituisce una NSString esadecimale che rappresenta NSData, dove i dati possono essere di qualsiasi lunghezza. Restituisce una stringa vuota se NSData è vuoto.

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];

Questo è "probabilmente" meglio che chiamare [someData description]e poi eliminare gli spazi, <'s e>. Mettere a nudo i personaggi sembra troppo "hacky". Inoltre non sai mai se Apple cambierà la formattazione di NSData -descriptionin futuro.

NOTA: alcune persone mi hanno contattato per ottenere la licenza per il codice in questa risposta. Con la presente dedico il mio copyright nel codice che ho pubblicato in questa risposta al pubblico dominio.


4
Bello, ma due suggerimenti: (1) Penso che appendFormat sia più efficiente per dati di grandi dimensioni poiché evita di creare un NSString intermedio e (2)% x rappresenta un unsigned int piuttosto che un unsigned long, sebbene la differenza sia innocua.
svachalek

Non per essere un oppositore, poiché questa è una buona soluzione facile da usare ma la mia soluzione il 25 gennaio è molto più efficiente. Se stai cercando una risposta ottimizzata per le prestazioni, guarda quella risposta . Upvoting questa risposta come una soluzione piacevole e facile da capire.
NSProgrammer

5
Ho dovuto rimuovere il cast (lungo senza segno) e utilizzare @ "% 02hhx" come stringa di formato per far funzionare questo.
Anton,

1
Giusto, per developer.apple.com/library/ios/documentation/cocoa/conceptual/… il formato dovrebbe essere "%02lx"con quel cast, o cast (unsigned int), o rilasciare il cast e usare @"%02hhx":)
qix

1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];è molto meglio (impronta di memoria inferiore)
Marek R

31

Ecco un metodo di categoria NSData altamente ottimizzato per la generazione di una stringa esadecimale. Mentre la risposta di @Dave Gallagher è sufficiente per dimensioni relativamente piccole, le prestazioni della memoria e della CPU si deteriorano per grandi quantità di dati. L'ho profilato con un file da 2 MB sul mio iPhone 5. Il confronto temporale è stato di 0,05 contro 12 secondi. L'impronta di memoria è trascurabile con questo metodo mentre l'altro metodo ha aumentato l'heap 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 @"";
}

Bella @Peter - C'è comunque una soluzione ancora più veloce (non molto della tua) - appena sotto;)
Moose

1
@ Moose, fai riferimento più precisamente a quale risposta stai parlando: voti e nuove risposte possono influenzare il posizionamento delle risposte. [modifica: oh, fammi indovinare, intendi la tua risposta ...]
Cœur

1
Aggiunto controllo null malloc. Grazie @ Cœur.
Peter

17

L'utilizzo della proprietà description di NSData non dovrebbe essere considerato un meccanismo accettabile per la codifica HEX della stringa. Questa proprietà è solo descrittiva e può cambiare in qualsiasi momento. Come nota, pre-iOS, la proprietà di descrizione NSData non restituiva nemmeno i suoi dati in forma esadecimale.

Ci scusiamo per insistere sulla soluzione, ma è importante prendere l'energia per serializzarla senza dover ricorrere a un'API pensata per qualcos'altro oltre alla serializzazione dei dati.

@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

tuttavia, devi liberare (hexChars) prima del ritorno.
karim

3
@karim, non è corretto. Utilizzando initWithCharactersNoCopy: length: freeWhenDone: e avendo freeWhenDone SÌ, NSString prenderà il controllo di quel byte buffer. La chiamata gratuita (hexChars) provocherà un arresto anomalo. Il vantaggio qui è sostanziale poiché NSString non dovrà effettuare una chiamata memcpy costosa.
NSProgrammer

@NSProgrammer grazie. Non ho notato l'inizializzatore NSSting.
karim

La documentazione afferma che descriptionrestituisce una stringa con codifica esadecimale, quindi mi sembra ragionevole.
Non comune

non dovremmo controllare che il valore restituito da malloc sia potenzialmente nullo?
Cœur

9

Ecco un modo più veloce per eseguire la conversione:

BenchMark (tempo medio per una conversione di dati di 1024 byte ripetuta 100 volte):

Dave Gallagher: ~ 8.070 ms
NSProgrammer: ~ 0,077 ms
Peter: ~ 0,031 ms
Questo: ~ 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

1
Puoi vedere come ho implementato il controllo malloc qui ( _hexStringmetodo): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
Cœur

Grazie per il riferimento - A proposito, mi piace il "troppo lungo" - È vero, ma ora l'ho digitato, chiunque può copiare / incollare - Sto scherzando - L'ho generato - Lo sapevi già :) Hai ragione il lungo, stavo solo cercando di colpire ovunque posso vincere microsecondi! Divide le iterazioni del ciclo per 2. Ma ammetto che manca di eleganza. Ciao
Moose

8

Versione Swift funzionale

Una fodera:

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

Ecco in un modulo di estensione riutilizzabile e auto documentante:

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("")
    }
}

In alternativa, usa reduce("", combine: +)invece di joinWithSeparator("")essere visto come un maestro funzionale dai tuoi colleghi.


Modifica: ho cambiato String ($ 0, radix: 16) in String (formato: "% 02x", $ 0), perché i numeri di una cifra dovevano avere uno zero di riempimento


7

La risposta di Peter è stata trasferita su 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 "";
}

Swift 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 "";
}

4

Avevo bisogno di risolvere questo problema e ho trovato le risposte molto utili qui, ma mi preoccupo delle prestazioni. La maggior parte di queste risposte implica la copia dei dati in blocco da NSData, quindi ho scritto quanto segue per eseguire la conversione con un basso overhead:

@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;
}

Questo pre-alloca lo spazio nella stringa per l'intero risultato ed evita di copiare i contenuti NSData utilizzando enumerateByteRangesUsingBlock. La modifica della X in una x nella stringa di formato utilizzerà cifre esadecimali minuscole. Se non vuoi un separatore tra i byte puoi ridurre l'istruzione

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

fino a solo

[string appendFormat:@"%02X", byte];

2
Credo che l'indice per recuperare il valore del byte necessiti di un aggiustamento, perché NSRangeindica l'intervallo all'interno della NSDatarappresentazione più grande , non all'interno del buffer dei byte più piccoli (quel primo parametro del blocco fornito a enumerateByteRangesUsingBlock) che rappresenta una singola porzione contigua del più grande NSData. Pertanto, byteRange.lengthriflette la dimensione del buffer di byte, ma byteRange.locationè la posizione all'interno del più grande NSData. Quindi, si desidera utilizzare semplicemente offset, non byteRange.location + offset, per recuperare il byte.
Rob il

1
@ Rob Grazie, capisco cosa intendi e ho modificato il codice
John Stephen

1
Se modifichi l'affermazione verso il basso per usare solo il singolo appendFormat, probabilmente dovresti cambiare anche la self.length * 3aself.length * 2
T.Colligan

1

Avevo bisogno di una risposta che funzionasse per stringhe di lunghezza variabile, quindi ecco cosa ho fatto:

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

Funziona alla grande come estensione per la classe NSString.


1
e se Apple cambia il modo in cui rappresentano la descrizione?
Brenden

1
nel metodo di descrizione iOS13 restituisce un formato diverso.
nacho4d

1

Puoi sempre utilizzare [yourString uppercaseString] per scrivere lettere maiuscole nella descrizione dei dati


1

Un modo migliore per serializzare / deserializzare NSData in NSString è utilizzare il codificatore / decodificatore Google Toolbox per Mac Base64. Basta trascinare nella tua App Project i file GTMBase64.m, GTMBase64.he GTMDefines.h dal pacchetto Foundation e fare qualcosa di simile

/**
 * 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];

}

Guardando il codice sorgente sembra che la classe che lo fornisce ora sia GTMStringEncoding. Non l'ho provato, ma sembra un'ottima nuova soluzione a questa domanda.
sarfata

1
A partire da Mac OS X 10.6 / iOS 4.0, NSData esegue la codifica Base-64. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc

@jrc è vero, ma considera di codificare stringhe di lavoro reali in Base-64. Devi occuparti della codifica "sicura per il web", che non hai in iOS / MacOS, come in GTMBase64 # webSafeEncodeData. Inoltre potrebbe essere necessario aggiungere / rimuovere Base64 "padding", quindi hai anche questa opzione: GTMBase64 # stringByWebSafeEncodingData: (NSData *) data padded: (BOOL) padded;
loretoparisi

1

Ecco una soluzione 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
    }

}

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)

0

Modificare %08xin %08Xper ottenere caratteri maiuscoli.


6
questo sarebbe meglio come commento poiché non hai incluso alcun contesto. Sto solo dicendo
Brenden

0

Swift + Proprietà.

Preferisco avere una rappresentazione esadecimale come proprietà (la stessa di bytese descriptionproprietà):

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("")
    }
}

L'idea è presa in prestito da questa risposta


-4
[deviceToken description]

Dovrai rimuovere gli spazi.

Personalmente base64codifico il deviceToken, ma è una questione di gusti.


Questo non ottiene lo stesso risultato. descrizione restituisce: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Mentre sto cercando: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.