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

Answers:


206

这是应用于我编写的NSData的类别。它返回代表NSData的十六进制NSString,其中数据可以是任何长度。如果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]然后剥离空格,<和> 更好。剥离字符感觉太“ hacky”。另外,您永远不会知道苹果将来是否会更改NSData的格式-description

注意:我有很多人就此答案中的代码许可与我联系。我特此将我在此答复中发布的代码中的版权专用于公共领域。


4
很好,但有两个建议:(1)我认为appendFormat对于大数据更为有效,因为它避免了创建中间NSString的行为;(2)%x表示无符号的int而不是无符号的long,尽管区别无害。
svachalek

不要成为反对者,因为这是一个易于使用的好解决方案,但是在1月25日提出的解决方案效率更高。如果您正在寻找性能优化的答案,请参见该答案。将此答案作为一个不错的,易于理解的解决方案。
NSProgrammer 2012年

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的答案对于较小的尺寸已经足够了,但是对于大量数据,内存和cpu性能却下降了。我在iPhone 5上使用2MB的文件对此进行了分析。时间比较为0.05对12秒。用这种方法可以忽略内存占用,而用另一种方法可以将堆增加到70MB!

- (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 2015年

1
@Moose,请更精确地引用您正在谈论的答案:投票和新答案可能会影响答案的位置。[编辑:哦,让我猜猜,你的意思是你自己的答案...]
心教堂

1
添加了malloc空检查。谢谢@Cœur。
彼得

17

使用NSData的description属性不应被视为HEX编码字符串的可接受机制。该属性仅用于描述,可以随时更改。需要注意的是,在iOS之前的版本中,NSData description属性甚至没有以十六进制形式返回其数据。

很抱歉使用该解决方案,但重要的是要花精力进行序列化,而又不要背负要用于数据序列化以外的其他功能的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)。
karim 2014年

3
@karim,那是不正确的。通过使用initWithCharactersNoCopy:length:freeWhenDone:并将freeWhenDone为YES,NSString将控制该字节缓冲区。调用free(hexChars)将导致崩溃。因为NSString不必进行昂贵的memcpy调用,所以这里的好处是巨大的。
NSProgrammer 2014年

@NSProgrammer谢谢。我没有注意到NSSting初始化程序。
karim 2014年

文档指出description返回的是十六进制编码的字符串,因此对我来说似乎很合理。
罕见

我们是否应该检查malloc返回的值是否可能为null?
心教堂

9

这是进行转换的更快方法:

BenchMark(1024字节数据转换的平均时间重复100次):

Dave Gallagher:〜8.070 ms
NSProgrammer:〜0.077 ms
Peter:〜0.031 ms
这一个:〜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
你可以看到我是如何在这里实现malloc的检查(_hexString法):github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/...
心教堂

感谢您的参考-顺便说一句,我喜欢'太长'-是的,但是现在我已经输入了,任何人都可以复制/粘贴-我在开玩笑-我已经生成了它-您已经知道了:)漫长的时间,我只是想在任何可以赢得微秒的地方都击中目标!它将循环迭代数除以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),因为一位数字需要填充零


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

这将为整个结果在字符串中预分配空间,并避免使用enumerateByteRangesUsingBlock复制NSData内容。将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谢谢,我明白了您的意思,并且已经调整了代码
John Stephen

1
如果您将语句修改为仅使用单个语句,appendFormat则可能还应该将其更改self.length * 3self.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类的扩展。


1
如果Apple更改了表示方式,该怎么办?
布伦登

1
iOS13中的description方法返回不同的格式。
nacho4d


1

将NSData序列化/反序列化为NSString的更好方法是使用Google Toolbox for Mac Base64编码器/解码器。只需从软件包Foundation中将文件GTMBase64.m,GTMBase64.he GTMDefines.h拖到您的App Project中,然后执行类似的操作

/**
 * 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。我没有尝试过,但是它看起来像是一个很好的解决此问题的新方法。
sarfata 2011年

1
从Mac OS X 10.6 / iOS 4.0开始,NSData执行Base-64编码。string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc 2015年

@jrc是正确的,但请考虑在Base-64中编码实际的工作字符串。您需要处理的是“ Web安全”编码,这在iOS / MacOS中是没有的,例如GTMBase64#webSafeEncodeData。另外,您可能需要添加/删除Base64“填充”,因此也具有此选项:GTMBase64#stringByWebSafeEncodingData:(NSData *)data
loretoparisi 2015年

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

Swift +属性。

我更喜欢使用十六进制表示形式作为属性(与bytesdescription属性相同):

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.