Лучший способ сериализовать NSData в шестнадцатеричную строку

101

Я ищу приятный какао способ сериализации объекта NSData в шестнадцатеричную строку. Идея состоит в том, чтобы сериализовать deviceToken, используемый для уведомления, перед его отправкой на мой сервер.

У меня есть следующая реализация, но я думаю, что должен быть какой-то более короткий и приятный способ сделать это.

+ (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;
}
сарфата
источник

Ответы:

206

Это категория, применяемая к написанным мной NSData. Он возвращает шестнадцатеричное NSString, представляющее NSData, где данные могут быть любой длины. Возвращает пустую строку, если NSData пусто.

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

Использование:

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

Это «вероятно» лучше, чем вызов [someData description]с последующим удалением пробелов, <и>. Удаление персонажей кажется слишком "хакерским". Кроме того, вы никогда не знаете, изменит ли Apple форматирование NSData -descriptionв будущем.

ПРИМЕЧАНИЕ. Люди обращались ко мне по поводу лицензирования кода в этом ответе. Настоящим я посвящаю свои авторские права на код, опубликованный в этом ответе, в общественное достояние.

Дэйв
источник
4
Хорошо, но два предложения: (1) Я думаю, что appendFormat более эффективен для больших данных, так как он позволяет избежать создания промежуточного NSString и (2)% x представляет собой unsigned int, а не unsigned long, хотя разница безвредна.
свачалек 09
Не скептически: это хорошее решение, простое в использовании, но мое решение от 25 января гораздо более эффективно. Если вы ищете ответ с оптимизацией производительности, посмотрите этот ответ . Проголосовать за этот ответ как за красивое и легкое для понимания решение.
NSProgrammer
5
Мне пришлось удалить (беззнаковое длинное) приведение и использовать @ "% 02hhx" в качестве строки формата, чтобы это сработало.
Антон
1
Правильно, согласно developer.apple.com/library/ios/documentation/cocoa/conceptual/… формат должен быть "%02lx"с этим приведением, или приведен к (unsigned int), или отброшен и использован @"%02hhx":)
qix
1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];намного лучше (меньший объем памяти)
Marek R
31

Вот высоко оптимизированный метод категории NSData для генерации шестнадцатеричной строки. Хотя ответа @Dave Gallagher достаточно для относительно небольшого размера, производительность памяти и процессора ухудшается для больших объемов данных. Я профилировал это с помощью файла размером 2 МБ на своем iPhone 5. Сравнение времени было 0,05 против 12 секунд. Объем памяти при использовании этого метода незначителен, в то время как другой метод увеличил размер кучи до 70 МБ!

- (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 @"";
}
Питер
источник
Хороший @Peter - Однако есть еще более быстрое (не намного, чем у вас) решение - чуть ниже;)
Moose
1
@Moose, укажите точнее, о каком ответе вы говорите: голоса и новые ответы могут повлиять на расположение ответов. [править: о, позвольте мне угадать, вы имеете в виду ваш собственный ответ ...]
Cœur
1
Добавлена ​​проверка нулевого значения malloc. Спасибо, @ Cœur.
Питер
17

Использование свойства описания NSData не следует рассматривать как приемлемый механизм для HEX-кодирования строки. Это свойство предназначено только для описания и может быть изменено в любое время. Обратите внимание, что до iOS свойство description NSData даже не возвращало данные в шестнадцатеричной форме.

Извините за то, что настаивал на решении, но важно потратить силы на его сериализацию без использования API, который предназначен для чего-то другого, кроме сериализации данных.

@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
источник
однако перед возвратом необходимо освободить (hexChars).
karim
3
@karim, это неверно. Используя initWithCharactersNoCopy: length: freeWhenDone: и имея freeWhenDone в YES, NSString возьмет на себя управление этим байтовым буфером. Вызов бесплатного (hexChars) приведет к сбою. Преимущество здесь существенное, поскольку NSString не будет выполнять дорогостоящий вызов memcpy.
NSProgrammer
@NSProgrammer спасибо. Инициализатор NSSting не заметил.
karim
В документации указано, что он descriptionвозвращает строку в шестнадцатеричной кодировке, поэтому мне это кажется разумным.
Uncommon
не следует ли нам проверять, является ли возвращаемое значение malloc потенциально нулевым?
Cœur
10

Вот более быстрый способ выполнить преобразование:

BenchMark (среднее время для преобразования данных размером 1024 байта, повторенного 100 раз):

Дэйв Галлахер: ~ 8,070 мс
Программист NSP: ~ 0,077 мс
Питер: ~ 0,031 мс
Этот: ~ 0,017 мс

@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
Вы можете увидеть, как я реализовал проверку malloc здесь ( _hexStringметод): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
Cœur
Спасибо за ссылку - Кстати, мне нравится «слишком длинный» - это правда, но теперь я напечатал его, любой может скопировать / вставить - Я шучу - я его сгенерировал - Вы уже знали :) Вы правы длинный, я просто пытался ударить везде, где я могу выиграть микросекунды! Он делит итерацию цикла на 2. Но я признаю, что ему не хватает элегантности. Пока
Moose
8

Функциональная версия Swift

Один лайнер:

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

Вот форма многоразового самодокументируемого расширения:

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

В качестве альтернативы, используйте reduce("", combine: +)вместо того, joinWithSeparator("")чтобы вас видели в качестве функционального мастера.


Изменить: я изменил String ($ 0, radix: 16) на String (format: "% 02x", $ 0), потому что для однозначных чисел необходимо иметь нулевое заполнение

NiñoScript
источник
7

Ответ Питера перенесен на 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 "";
}
john07
источник
4

Мне нужно было решить эту проблему, и я нашел здесь очень полезные ответы, но я беспокоюсь о производительности. Большинство этих ответов связаны с массовым копированием данных из NSData, поэтому я написал следующее, чтобы выполнить преобразование с низкими накладными расходами:

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

Это предварительно выделяет пространство в строке для всего результата и позволяет избежать копирования содержимого NSData с помощью enumerateByteRangesUsingBlock. При замене X на x в строке формата будут использоваться шестнадцатеричные цифры нижнего регистра. Если вам не нужен разделитель между байтами, вы можете уменьшить оператор

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

до всего

[string appendFormat:@"%02X", byte];
Джон Стивен
источник
2
Я считаю, что индекс для извлечения байтового значения нуждается в корректировке, потому что NSRangeуказывает диапазон в пределах большего NSDataпредставления, а не в пределах меньшего буфера байтов (этот первый параметр блока, предоставленного enumerateByteRangesUsingBlock), который представляет единственную непрерывную часть большего NSData. Таким образом, byteRange.lengthотражает размер буфера байтов, но byteRange.locationэто место в пределах большего NSData. Таким образом, вы хотите использовать просто offset, а не byteRange.location + offsetизвлекать байт.
Роб
1
@Rob Спасибо, я понимаю, что вы имеете в виду, и скорректировал код
Джон Стивен
1
Если вы измените утверждение, чтобы использовать только сингл, appendFormatвам, вероятно, также следует изменить его self.length * 3наself.length * 2
T. Colligan
1

Мне нужен был ответ, который работал бы со строками переменной длины, поэтому вот что я сделал:

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

Прекрасно работает как расширение для класса NSString.

BadPirate
источник
1
что, если Apple изменит способ представления описания?
Бренден
1
в iOS13 метод описания возвращает другой формат.
nacho4d
1

Вы всегда можете использовать [yourString uppercaseString] для использования заглавных букв в описании данных.

Ростислав Бачик
источник
1

Лучший способ сериализации / десериализации NSData в NSString - использовать кодировщик / декодер Base64 Google Toolbox для Mac . Просто перетащите в свой проект приложения файлы GTMBase64.m, GTMBase64.he GTMDefines.h из пакета Foundation и выполните что-то вроде

/**
 * 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
источник
Глядя на исходный код, кажется, что класс, обеспечивающий это, теперь называется GTMStringEncoding. Я не пробовал, но похоже, что это отличное новое решение этого вопроса.
sarfata
1
Начиная с Mac OS X 10.6 / iOS 4.0, NSData выполняет кодировку Base-64. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc
@jrc это правда, но рассмотрите возможность кодирования реальных рабочих строк в Base-64. Вам придется иметь дело не только с «безопасным веб» кодированием, которого у вас нет в iOS / MacOS, как в GTMBase64 # webSafeEncodeData. Также вам может потребоваться добавить / удалить "заполнение" Base64, поэтому у вас также есть эта опция: GTMBase64 # stringByWebSafeEncodingData: (NSData *) data padded: (BOOL) padded;
loretoparisi
1

Вот решение с использованием 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
источник
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

Измените %08xна, %08Xчтобы получить заглавные символы.

Дэн Риз
источник
6
это было бы лучше в качестве комментария, поскольку вы не включали никакого контекста. Just Sayin '
Brenden
0

Swift + Property.

Я предпочитаю иметь шестнадцатеричное представление как свойство (так же, как bytesи descriptionсвойства):

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

Идея заимствована из этого ответа

Avt
источник
-4
[deviceToken description]

Вам нужно будет удалить пробелы.

Лично я base64кодирую deviceToken, но это дело вкуса.

Эдди
источник
Это не дает того же результата. description возвращает: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Пока я ищу: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata