Der beste Weg, um NSData in eine hexadeximale Zeichenfolge zu serialisieren


101

Ich suche nach einer Möglichkeit, ein NSData-Objekt in eine hexadezimale Zeichenfolge zu serialisieren. Die Idee ist, das für die Benachrichtigung verwendete deviceToken zu serialisieren, bevor es an meinen Server gesendet wird.

Ich habe die folgende Implementierung, aber ich denke, es muss einen kürzeren und schöneren Weg geben, dies zu tun.

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

Antworten:


206

Dies ist eine Kategorie, die auf NSData angewendet wird, die ich geschrieben habe. Es wird ein hexadezimaler NSString zurückgegeben, der die NSData darstellt, wobei die Daten eine beliebige Länge haben können. Gibt eine leere Zeichenfolge zurück, wenn NSData leer ist.

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

Verwendung:

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

Dies ist "wahrscheinlich" besser als das Aufrufen [someData description]und anschließende Entfernen der Leerzeichen, <und>. Das Strippen von Charakteren fühlt sich einfach zu "hackig" an. Außerdem wissen Sie nie, ob Apple die Formatierung von NSData -descriptionin Zukunft ändern wird.

HINWEIS: Ich wurde von Leuten bezüglich der Lizenzierung des Codes in dieser Antwort kontaktiert. Hiermit widme ich mein Urheberrecht an dem Code, den ich in dieser Antwort veröffentlicht habe, der Öffentlichkeit.


4
Schön, aber zwei Vorschläge: (1) Ich denke, appendFormat ist effizienter für große Datenmengen, da es die Erstellung eines Zwischen-NSStrings vermeidet und (2)% x eher ein vorzeichenloses int als ein vorzeichenloses long darstellt, obwohl der Unterschied harmlos ist.
Svachalek

Kein Neinsager zu sein, da dies eine gute Lösung ist, die einfach zu bedienen ist, aber meine Lösung am 25. Januar ist weitaus effizienter. Wenn Sie nach einer leistungsoptimierten Antwort suchen, lesen Sie diese Antwort . Diese Antwort als nette, leicht verständliche Lösung zu bewerten.
NSProgrammer

5
Ich musste die (vorzeichenlose lange) Besetzung entfernen und @ "% 02hhx" als Formatzeichenfolge verwenden, damit dies funktioniert.
Anton

1
Richtig, laut developer.apple.com/library/ios/documentation/cocoa/conceptual/… sollte das Format "%02lx"mit dieser Besetzung sein oder in die Besetzung gegossen (unsigned int)werden oder die Besetzung fallen lassen und verwenden @"%02hhx":)
qix

1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];ist viel besser (kleinerer Speicherbedarf)
Marek R

31

Hier ist eine hochoptimierte NSData-Kategoriemethode zum Generieren einer Hex-Zeichenfolge. Während die Antwort von @Dave Gallagher für eine relativ kleine Größe ausreicht, verschlechtern sich die Speicher- und CPU-Leistung für große Datenmengen. Ich habe dies mit einer 2-MB-Datei auf meinem iPhone 5 profiliert. Der Zeitvergleich betrug 0,05 gegenüber 12 Sekunden. Der Speicherbedarf ist bei dieser Methode vernachlässigbar, während bei der anderen Methode der Heap auf 70 MB vergrößert wurde!

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

Netter @Peter - Es gibt jedoch eine noch schnellere (nicht viel als Ihre) Lösung - direkt darunter;)
Elch

1
@Moose, bitte geben Sie genauer an, um welche Antwort es sich handelt: Stimmen und neue Antworten können die Positionierung der Antworten beeinflussen. [edit: oh, lass mich raten, du meinst deine eigene Antwort ...]
Cœur

1
Malloc Null Check hinzugefügt. Danke @ Cœur.
Peter

17

Die Verwendung der description-Eigenschaft von NSData sollte nicht als akzeptabler Mechanismus für die HEX-Codierung der Zeichenfolge angesehen werden. Diese Eigenschaft dient nur zur Beschreibung und kann jederzeit geändert werden. Vor iOS gab die NSData-Beschreibungseigenschaft ihre Daten nicht einmal in hexadezimaler Form zurück.

Es tut uns leid, dass Sie an der Lösung gearbeitet haben, aber es ist wichtig, dass Sie sich die Energie nehmen, um sie zu serialisieren, ohne eine API zu deaktivieren, die für etwas anderes als die Serialisierung von Daten gedacht ist.

@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

Sie müssen jedoch vor der Rückgabe (hexChars) freigeben.
Karim

3
@karim, das ist falsch. Wenn Sie initWithCharactersNoCopy: length: freeWhenDone: verwenden und freeWhenDone YES ist, übernimmt der NSString die Kontrolle über diesen Bytepuffer. Ein kostenloser Aufruf (hexChars) führt zu einem Absturz. Der Vorteil hierbei ist erheblich, da NSString keinen teuren Memcpy-Anruf tätigen muss.
NSProgrammer

@NSProgrammer danke. Ich habe den NSSting-Initialisierer nicht bemerkt.
Karim

In der Dokumentation heißt es, dass descriptioneine hexadezimal codierte Zeichenfolge zurückgegeben wird, was mir vernünftig erscheint.
Gelegentlich

sollten wir nicht prüfen, ob der von malloc zurückgegebene Wert möglicherweise null ist?
Cœur

9

Hier ist ein schnellerer Weg, um die Konvertierung durchzuführen:

BenchMark (mittlere Zeit für eine 100-mal wiederholte Datenkonvertierung von 1024 Byte):

Dave Gallagher: ~ 8,070 ms
NSProgrammer: ~ 0,077 ms
Peter: ~ 0,031 ms
This One: ~ 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
Sie können sehen, wie ich den Malloc-Check implementiert habe ( _hexStringMethode): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
Cœur

Vielen Dank für den Hinweis - Übrigens mag ich das 'zu lang' - Das stimmt, aber jetzt habe ich es getippt, jeder kann es kopieren / einfügen - ich scherze - ich habe es generiert - du wusstest es schon :) Du bist richtig So lange habe ich nur versucht, überall zu treffen, wo ich Mikrosekunden gewinnen kann! Es teilt die Schleifeniterationen durch 2. Aber ich gebe zu, dass es an Eleganz mangelt. Tschüss
Elch

8

Funktionale Swift-Version

Einzeiler:

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

Hier ist eine wiederverwendbare und selbstdokumentierende Erweiterungsform:

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

Alternativ können Sie verwenden, reduce("", combine: +)anstatt joinWithSeparator("")von Ihren Kollegen als funktionaler Master angesehen zu werden.


Bearbeiten: Ich habe String ($ 0, Radix: 16) in String (Format: "% 02x", $ 0) geändert, da einstellige Zahlen eine Auffüll-Null benötigen


7

Peters Antwort wurde auf Swift übertragen

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

Ich musste dieses Problem lösen und fand die Antworten hier sehr nützlich, aber ich mache mir Sorgen um die Leistung. Die meisten dieser Antworten beinhalten das Kopieren der Daten in großen Mengen aus NSData. Daher habe ich Folgendes geschrieben, um die Konvertierung mit geringem Overhead durchzuführen:

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

Dadurch wird vorab Speicherplatz in der Zeichenfolge für das gesamte Ergebnis zugewiesen, und es wird vermieden, dass der NSData-Inhalt mithilfe von enumerateByteRangesUsingBlock jemals kopiert wird. Wenn Sie das X in ein x in der Formatzeichenfolge ändern, werden Hex-Ziffern in Kleinbuchstaben verwendet. Wenn Sie kein Trennzeichen zwischen den Bytes möchten, können Sie die Anweisung reduzieren

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

auf nur

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

2
Ich glaube, der Index zum Abrufen des Bytewerts muss angepasst werden, da er den NSRangeBereich innerhalb der größeren NSDataDarstellung angibt , nicht innerhalb des kleineren Bytepuffers (des ersten Parameters des Blocks, an den geliefert wird enumerateByteRangesUsingBlock), der einen einzelnen zusammenhängenden Teil des größeren darstellt NSData. Somit byteRange.lengthspiegelt sich die Größe des Bytepuffers wider, aber das byteRange.locationist der Ort innerhalb des größeren NSData. Sie möchten also einfach das Byte abrufen offset, nicht byteRange.location + offset.
Rob

1
@ Rob Danke, ich verstehe was du meinst und habe den Code angepasst
John Stephen

1
Wenn Sie die Anweisung nach unten ändern, um nur die Single zu verwenden appendFormat, sollten Sie wahrscheinlich auch die self.length * 3zuself.length * 2
T. Colligan

1

Ich brauchte eine Antwort, die für Zeichenfolgen mit variabler Länge funktioniert. Deshalb habe ich Folgendes getan:

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

Funktioniert hervorragend als Erweiterung für die NSString-Klasse.


1
Was ist, wenn Apple die Darstellung der Beschreibung ändert?
Brenden

1
In iOS13 gibt die Beschreibungsmethode ein anderes Format zurück.
Nacho4D

1

Sie können jederzeit [yourString uppercaseString] verwenden, um Buchstaben in der Datenbeschreibung groß zu schreiben


1

Eine bessere Möglichkeit, NSData in NSString zu serialisieren / deserialisieren, ist die Verwendung der Google Toolbox für Mac Base64-Encoder / -Decoder. Ziehen Sie einfach die Dateien GTMBase64.m, GTMBase64.he GTMDefines.h aus dem Paket Foundation in Ihr App-Projekt und tun Sie so etwas

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

}

Wenn man sich den Quellcode ansieht, scheint es, dass die Klasse, die das jetzt bereitstellt, GTMStringEncoding ist. Ich habe es nicht ausprobiert, aber es sieht nach einer großartigen neuen Lösung für diese Frage aus.
Sarfata

1
Ab Mac OS X 10.6 / iOS 4.0 führt NSData die Base-64-Codierung durch. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc

@jrc das ist wahr, aber erwägen Sie, echte Arbeitszeichenfolgen in Base-64 zu codieren. Sie müssen sich dann mit "web sicherer" Codierung befassen, die Sie in iOS / MacOS nicht haben, wie in GTMBase64 # webSafeEncodeData. Möglicherweise müssen Sie auch Base64 "padding" hinzufügen / entfernen, damit Sie auch diese Option haben: GTMBase64 # stringByWebSafeEncodingData: (NSData *) Daten aufgefüllt: (BOOL) aufgefüllt;
Loretoparisi

1

Hier ist eine Lösung mit 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

Wechseln Sie %08xzu %08X, um Großbuchstaben zu erhalten.


6
Dies wäre besser als Kommentar, da Sie keinen Kontext angegeben haben. Sag einfach '
Brenden

0

Swift + Property.

Ich bevorzuge eine hexadezimale Darstellung als Eigenschaft (die gleichen wie bytesund descriptionEigenschaften):

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

Aus dieser Antwort ist eine Idee entlehnt


-4
[deviceToken description]

Sie müssen die Leerzeichen entfernen.

Persönlich base64verschlüssele ich das deviceToken, aber es ist Geschmackssache.


Dies führt nicht zum gleichen Ergebnis. Beschreibung gibt zurück: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Während ich suche: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.