Найкращий спосіб серіалізації NSData в шістнадцяткову рядок


101

Я шукаю приємний спосіб какао для серіалізації об’єкта NSData в шістнадцятковий рядок. Ідея полягає в тому, щоб серіалізувати пристрійToken, який використовується для сповіщення, перед тим, як відправити його на мій сервер.

У мене є така реалізація, але я думаю, що для цього повинен бути коротший і приємніший спосіб.

+ (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 + конверсія.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]а потім знімати пробіли, <'і>. Зачистки персонажів просто почуваються надто "хакітними". Крім того, ви ніколи не знаєте, чи -descriptionв майбутньому Apple змінить форматування NSData .

ПРИМІТКА. У цій відповіді люди до мене зверталися з приводу ліцензування коду. Я присвячую свої авторські права в коді, який я опублікував у цій відповіді, у загальнодоступне надбання.


4
Приємно, але дві пропозиції: (1) Я думаю, що appendFormat є більш ефективним для великих даних, оскільки дозволяє уникнути створення проміжного NSString, і (2)% x являє собою неподписаний int, а не безпідписаний довгий, хоча різниця нешкідлива.
свачалек

Не бути найсайєром, оскільки це хороше рішення, яке просте у використанні, але моє рішення 25 січня є набагато ефективнішим. Якщо ви шукаєте відповідь, оптимізовану для виконання, дивіться цю відповідь . Оголошення цієї відповіді як приємне, легко зрозуміле рішення.
NSProgrammer

5
Мені довелося видалити (непідписаний довгий) амплуа і використовувати @ "% 02hhx" як рядок формату, щоб зробити цю роботу.
Антон

1
Право, на developer.apple.com/library/ios/documentation/cocoa/conceptual/… формат повинен бути "%02lx"з цим (unsigned int)@"%02hhx"
амплітудою

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 рядка HEX. Ця властивість є лише для опису і може змінюватися в будь-який час. Як зауважимо, перед iOS, властивість опису 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

проте ви повинні безкоштовно (hexChars) перед поверненням.
Карим

3
@karim, це неправильно. Використовуючи initWithCharactersNoCopy: length: freeWhenDone: та маючи freeWhenDone бути ТАК, NSString візьме під контроль цей байт-буфер. Виклик безкоштовно (hexChars) призведе до аварії. Перевага тут істотна, оскільки NSString не доведеться робити дорогий memcpy-дзвінок.
NSProgrammer

@NSProgrammer дякую. Я не помітив ініціалізатора NSSting.
Карим

У документації зазначено, що descriptionповертає шістнадцяткову кодовану рядок, так що мені здається розумним.
нечасто

чи не слід перевіряти, чи повертається значення malloc потенційно нульовим?
Cœur

9

Ось більш швидкий спосіб здійснити конверсію:

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

Дякую за довідку - BTW Мені подобається "занадто тривалий" - Це правда, але тепер я його набрав, кожен може скопіювати / вставити - я жартую - я створив це - Ви вже знали :) Ви маєте рацію тривалий, я просто намагався вдарити скрізь, де можу виграти мікросекунди! Він розділяє ітерації петлі на 2. Але я визнаю, що їй не вистачає елегантності. Bye
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, радікс: 16) на String (формат: "% 02x", $ 0), тому що для нумерації прокладки потрібен один цифр


7

Відповідь Пітера перенесла Свіфта

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

стрімкий3

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

Швидкий 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

Мені потрібно було вирішити цю проблему і знайшов відповіді тут дуже корисними, але я переживаю за ефективність. Більшість цих відповідей стосуються копіювання даних масово з 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
Т. Colligan

1

Мені потрібна відповідь, яка буде працювати для рядків змінної довжини, тож ось що я зробив:

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

Чудово працює як розширення для класу NSString.


1
що робити, якщо Apple змінить спосіб опису?
Бренден

1
в методі опису iOS13 повертається інший формат.
nacho4d

1

Ви завжди можете використовувати [yourString верхній регістрString] для використання великих літер в описі даних


1

Кращий спосіб серіалізувати / десеріалізувати NSData в NSString - це використання Google Toolbox для кодера / декодера Mac Base64. Просто перетягніть у свій додаток Project файли 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];

}

Дивлячись на вихідний код, здається, що класом, який зараз є GTMStringEncoding. Я не пробував цього, але це виглядає як чудове нове рішення цього питання.
сарфата

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. Також вам може знадобитися додати / видалити "padding" Base64, тому у вас є також такий варіант: GTMBase64 # stringByWebSafeEncodingData: (NSData *) дані додані: (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
    }

}

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
це було б краще як коментар, оскільки ви не включили жодного контексту. Justinin '
Brenden

0

Swift + Власність.

Я вважаю за краще мати шістнадцяткове представлення як властивість (те саме, що 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("")
    }
}

Ідея запозичена з цієї відповіді


-4
[deviceToken description]

Вам потрібно буде видалити пробіли.

Особисто я base64кодую deviceToken, але це питання смаку.


Це не дає однакового результату. опис повертається: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Поки я шукаю: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.