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

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.


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

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

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

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


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

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

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.


1
co jeśli Apple zmieni sposób, w jaki przedstawiają opis?
— Brenden,

1
w metodzie opisu iOS13 zwraca inny format.
— nacho4d


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

}

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
    }

}

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

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


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


-4
[deviceToken description]

Musisz usunąć spacje.

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


To nie daje tego samego wyniku. opis zwraca: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Podczas gdy szukam: 2CF56D5D2FAB0A47 .... 7738CE777E791759
— sarfata
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.