Najlepszy sposób na serializację NSData do ciągu szesnastkowego

101

Szukam przyjemnego kakaowego sposobu serializacji obiektu NSData do ciągu szesnastkowego. Chodzi o to, aby serializować deviceToken używany do powiadomienia przed wysłaniem go na mój serwer.

Mam następującą implementację, ale myślę, że musi być jakiś krótszy i przyjemniejszy sposób, aby to zrobić.

+ (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
źródło

Odpowiedzi:

206

Jest to kategoria dotycząca NSData, którą napisałem. Zwraca szesnastkowy NSString reprezentujący NSData, gdzie dane mogą mieć dowolną długość. Zwraca pusty ciąg, jeśli NSData jest pusty.

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

Stosowanie:

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

Jest to „prawdopodobnie” lepsze niż wywołanie, [someData description]a następnie usunięcie spacji, <i>. Usuwanie postaci wydaje się po prostu zbyt „hackowe”. Poza tym nigdy nie wiadomo, czy Apple zmieni -descriptionw przyszłości formatowanie NSData .

UWAGA: Wiele osób skontaktowało się ze mną w sprawie licencji na kod w tej odpowiedzi. Niniejszym dedykuję moje prawa autorskie do kodu, który zamieściłem w tej odpowiedzi, do domeny publicznej.

Dave
źródło
4
Ładnie, ale dwie sugestie: (1) Myślę, że appendFormat jest bardziej wydajny w przypadku dużych danych, ponieważ unika tworzenia pośredniego NSString, a (2)% x reprezentuje int unsigned zamiast unsigned long, chociaż różnica jest nieszkodliwa.
svachalek
Nie ma co krytykować, ponieważ jest to dobre rozwiązanie, które jest łatwe w użyciu, ale moje rozwiązanie z 25 stycznia jest znacznie bardziej wydajne. Jeśli szukasz odpowiedzi zoptymalizowanej pod kątem wydajności, zobacz tę odpowiedź . Głosowanie za tą odpowiedzią jako miłym, łatwym do zrozumienia rozwiązaniem.
NSProgrammer,
5
Musiałem usunąć (długi bez znaku) rzutowanie i użyć @ "% 02hhx" jako ciągu formatu, aby to zadziałało.
Anton
1
Zgadza się, według developer.apple.com/library/ios/documentation/cocoa/conceptual/ ... format powinien być "%02lx"z tym rzutowaniem, (unsigned int)@"%02hhx"
rzutem
1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];jest znacznie lepszy (mniejszy ślad pamięci)
Marek R
31

Oto wysoce zoptymalizowana metoda kategorii NSData do generowania ciągu szesnastkowego. Chociaż odpowiedź @Dave'a Gallaghera jest wystarczająca dla stosunkowo małego rozmiaru, wydajność pamięci i procesora pogarsza się w przypadku dużych ilości danych. Profilowałem to za pomocą pliku o wielkości 2 MB na moim iPhonie 5. Porównanie czasu wyniosło 0,05 do 12 sekund. Wykorzystanie pamięci jest znikome w przypadku tej metody, podczas gdy inna metoda zwiększyła stertę do 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 @"";
}
Piotr
źródło
Niezły @Peter - Jest jednak jeszcze szybsze (niewiele niż twoje) rozwiązanie - tuż poniżej;)
Łoś
2
@Moose, prosimy o dokładniejsze odniesienie się do odpowiedzi, o której Pan mówi: głosy i nowe odpowiedzi mogą wpłynąć na pozycjonowanie odpowiedzi. [edytuj: och, niech zgadnę, masz na myśli własną odpowiedź ...]
Cœur
1
Dodano sprawdzanie wartości zerowej malloc. Dzięki @ Cœur.
Peter
17

Używanie właściwości description NSData nie powinno być uważane za akceptowalny mechanizm kodowania HEX łańcucha. Ta właściwość służy wyłącznie do opisu i może ulec zmianie w dowolnym momencie. Uwaga: przed iOS właściwość opisu NSData nawet nie zwracała danych w postaci szesnastkowej.

Przepraszamy za nękanie rozwiązania, ale ważne jest, aby wziąć energię do serializacji bez wycofywania interfejsu API, który jest przeznaczony do czegoś innego niż serializacja danych.

@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
źródło
jednak musisz zwolnić (hexChars) przed powrotem.
karim
3
@karim, to nieprawda. Używając initWithCharactersNoCopy: length: freeWhenDone: i mając parametr freeWhenDone na YES, NSString przejmie kontrolę nad tym buforem bajtów. Połączenie bezpłatne (hexChars) spowoduje awarię. Korzyść jest tutaj znaczna, ponieważ NSString nie będzie musiał wykonywać drogiego wywołania memcpy.
NSProgrammer
@NSProgrammer thanks. Nie zauważyłem inicjatora NSSting.
karim
Dokumentacja stwierdza, że descriptionzwraca ciąg zakodowany szesnastkowo, więc wydaje mi się to rozsądne.
Niezbyt często
czy nie powinniśmy sprawdzić, czy wartość zwrócona przez malloc jest potencjalnie zerowa?
Coœur
10

Oto szybszy sposób wykonania konwersji:

BenchMark (średni czas konwersji danych 1024 bajty powtórzony 100 razy):

Dave Gallagher: ~
8,070 ms NSP Programmer: ~ 0,077 ms
Peter: ~ 0,031 ms
Ten: ~ 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
Łoś
źródło
1
Możesz zobaczyć, jak zaimplementowałem test malloc tutaj ( _hexStringmetoda): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/ ...
Cœur
Dzięki za referencje - przy okazji lubię „zbyt długie” - To prawda, ale teraz wpisałem, każdy może kopiować / wklejać - żartuję - wygenerowałem - już wiesz :) Masz rację po prostu próbowałem trafić wszędzie, gdzie mogę wygrać mikrosekundy! Dzieli iteracje pętli przez 2. Ale przyznaję, że brakuje mu elegancji. Do widzenia
Łoś
8

Funkcjonalna wersja Swift

Jedna wkładka:

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

Oto rozszerzenie do wielokrotnego użytku, samodokumentujące się:

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

Alternatywnie, użyj reduce("", combine: +)zamiast joinWithSeparator("")być postrzeganym przez swoich rówieśników jako wzorzec funkcjonalny.


Edycja: Zmieniłem String (0 $, podstawa: 16) na String (format: "% 02x", $ 0), ponieważ liczby jednocyfrowe potrzebne do wypełnienia zerem

NiñoScript
źródło
7

Odpowiedź Petera została przeniesiona do Szybkiego

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

Szybki 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
źródło
4

Musiałem rozwiązać ten problem i znalazłem tutaj bardzo przydatne odpowiedzi, ale martwię się o wydajność. Większość z tych odpowiedzi obejmuje zbiorcze kopiowanie danych z NSData, więc napisałem co następuje, aby wykonać konwersję z niskim narzutem:

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

To wstępnie przydziela miejsce w ciągu dla całego wyniku i zapobiega kopiowaniu zawartości NSData przy użyciu enumerateByteRangesUsingBlock. Zmiana X na x w ciągu formatu spowoduje użycie małych cyfr szesnastkowych. Jeśli nie chcesz separatora między bajtami, możesz zmniejszyć instrukcję

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

do sprawiedliwego

[string appendFormat:@"%02X", byte];
John Stephen
źródło
2
Uważam, że indeks do pobierania wartości bajtu wymaga korekty, ponieważ NSRangewskazuje zakres w większej NSDatareprezentacji, a nie w buforze o mniejszych bajtach (ten pierwszy parametr bloku dostarczonego do enumerateByteRangesUsingBlock), który reprezentuje pojedynczą ciągłą część większego NSData. W ten sposób byteRange.lengthodzwierciedla rozmiar bufora bajtów, ale byteRange.locationjest to lokalizacja w większym NSData. Dlatego chcesz użyć po prostu offset, a nie byteRange.location + offset, aby pobrać bajt.
Rob
1
@Rob Dzięki, rozumiem, co masz na myśli i dostosowałem kod
John Stephen
1
Jeśli zmodyfikować oświadczenie w dół po prostu użyć pojedynczy appendFormatnależy prawdopodobnie również zmianę self.length * 3doself.length * 2
T. Colligan
1

Potrzebowałem odpowiedzi, która działałaby dla ciągów o zmiennej długości, więc oto co zrobiłem:

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

Świetnie sprawdza się jako rozszerzenie klasy NSString.

BadPirate
źródło
1
co jeśli Apple zmieni sposób, w jaki przedstawiają opis?
Brenden,
1
w metodzie opisu iOS13 zwraca inny format.
nacho4d
1

Zawsze możesz użyć [yourString uppercaseString], aby użyć wielkich liter w opisie danych

Rostyslav Bachyk
źródło
1

Lepszym sposobem serializacji / deserializacji NSData do NSString jest użycie programu Google Toolbox for Mac Base64 kodera / dekodera. Po prostu przeciągnij do projektu aplikacji pliki GTMBase64.m, GTMBase64.he GTMDefines.h z pakietu Foundation i wykonaj coś w rodzaju

/**
 * 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
źródło
Patrząc na kod źródłowy wydaje się, że klasa udostępniająca, którą jest teraz GTMStringEncoding. Nie próbowałem tego, ale wygląda na to, że jest to świetne nowe rozwiązanie tego pytania.
sarfata
1
Począwszy od Mac OS X 10.6 / iOS 4.0, NSData obsługuje kodowanie Base-64. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc
@jrc to prawda, ale rozważ kodowanie prawdziwych ciągów roboczych w Base-64. Musisz radzić sobie z kodowaniem „bezpiecznym w sieci”, którego nie masz w iOS / MacOS, jak w GTMBase64 # webSafeEncodeData. Konieczne może być również dodanie / usunięcie "wypełnienia" Base64, więc masz również tę opcję: GTMBase64 # stringByWebSafeEncodingData: (NSData *) dopełnione dane: (BOOL) dopełnione;
loretoparisi
1

Oto rozwiązanie wykorzystujące 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
źródło
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
źródło
0

Zmień %08xna, %08Xaby uzyskać duże znaki.

Dan Reese
źródło
6
byłoby to lepsze jako komentarz, ponieważ nie podałeś żadnego kontekstu. Tylko mówię
Brenden,
0

Swift + Property.

Wolę mieć reprezentację szesnastkową jako właściwość (taką samą jak bytesi descriptionwłaściwości):

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

Pomysł jest zapożyczony z tej odpowiedzi

Avt
źródło
-4
[deviceToken description]

Musisz usunąć spacje.

Osobiście base64koduję deviceToken, ale to kwestia gustu.

Eddie
źródło
To nie daje tego samego wyniku. opis zwraca: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Podczas gdy szukam: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata