【问题标题】:Best way to serialize an NSData into a hexadeximal string将 NSData 序列化为十六进制字符串的最佳方法
【发布时间】:2010-11-21 06:30:55
【问题描述】:

我正在寻找一种不错的可可方法来将 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;
}

【问题讨论】:

    标签: iphone notifications nsdata


    【解决方案1】:

    这是我编写的应用于 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”了。另外,您永远不知道 Apple 是否会在未来更改 NSData 的 -description 的格式。

    注意:我已经有人与我联系,以获取此答案中代码的许可。我在此将我在此答案中发布的代码的版权奉献给公共领域。

    【讨论】:

    • 不错,但有两个建议:(1)我认为 appendFormat 对于大数据更有效,因为它避免了创建中间 NSString 和(2)%x 表示无符号整数而不是无符号长,尽管差异是无害的。
    • 不要反对,因为这是一个易于使用的好解决方案,但 1 月 25 日的 my solution 效率更高。如果您正在寻找性能优化的答案,请参阅 answer。将此答案作为一个很好的、易于理解的解决方案来支持。
    • 我必须删除(无符号长)强制转换并使用 @"%02hhx" 作为格式字符串才能完成这项工作。
    • 对,根据developer.apple.com/library/ios/documentation/cocoa/conceptual/…,格式应该是"%02lx",使用该演员表,或者转换为(unsigned int),或者放弃演员表并使用@"%02hhx" :)
    • [hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]]; 更好(内存占用更小)
    【解决方案2】:

    这是一个高度优化的NSData category 生成十六进制字符串的方法。虽然@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,请更准确地参考您正在谈论的答案:投票和新答案可能会影响答案的定位。 [编辑:哦,让我猜猜,你的意思是你自己的答案......]
    • 添加了 malloc 空检查。谢谢@Cœur。
    【解决方案3】:

    不应将使用 NSData 的 description 属性视为对字符串进行 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)。
    • @karim,这是不正确的。通过使用 initWithCharactersNoCopy:length:freeWhenDone: 并让 freeWhenDone 为 YES,NSString 将控制该字节缓冲区。调用 free(hexChars) 会导致崩溃。这里的好处是巨大的,因为 NSString 不必进行昂贵的 memcpy 调用。
    • @NSProgrammer 谢谢。我没有注意到 NSSting 初始化程序。
    • 文档指出description 返回一个十六进制编码的字符串,所以这对我来说似乎是合理的。
    • 我们不应该检查 malloc 返回的值是否可能为 null 吗?
    【解决方案4】:

    这是一种更快的转换方法:

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

    戴夫·加拉格尔:~8.070 毫秒
    NSProgrammer:~0.077 毫秒
    彼得:~0.031 毫秒
    这个:~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
    

    【讨论】:

    • 你可以在这里看到我是如何实现malloc检查的(_hexString方法):github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
    • 感谢您的参考 - 顺便说一句,我喜欢“太冗长” - 这是真的,但现在我已经输入了它,任何人都可以复制/粘贴 - 我在开玩笑 - 我已经生成了 - 你已经知道了:)你说得对,我只是想在任何地方都能赢得微秒!它将循环迭代除以 2。但我承认它缺乏优雅。再见
    【解决方案5】:

    功能性 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),因为一位数字需要填充零

    【讨论】:

      【解决方案6】:

      彼得的答案移植到 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 "";
      }
      

      斯威夫特 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 "";
      }
      

      【讨论】:

        【解决方案7】:

        我需要解决这个问题,发现这里的答案非常有用,但我担心性能。这些答案中的大多数都涉及从 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];
        

        【讨论】:

        • 我认为检索字节值的索引需要调整,因为NSRange 表示在较大的NSData 表示范围内,而不是在较小的字节缓冲区内(提供的块的第一个参数到enumerateByteRangesUsingBlock),它代表较大的NSData 的单个连续部分。因此,byteRange.length 反映了字节缓冲区的大小,但byteRange.location 是较大的NSData 中的位置。因此,您只想使用offset 而不是byteRange.location + offset 来检索字节。
        • @Rob 谢谢,我明白你的意思并调整了代码
        • 如果您将语句修改为仅使用单个appendFormat,您可能还应该将self.length * 3 更改为self.length * 2
        【解决方案8】:

        我需要一个适用于可变长度字符串的答案,所以这就是我所做的:

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

        非常适合作为 NSString 类的扩展。

        【讨论】:

        • 如果 Apple 改变它们表示描述的方式会怎样?
        • 在iOS13描述方法中返回不同的格式。
        【解决方案9】:

        您可以随时使用 [yourString uppercaseString] 将数据描述中的字母大写

        【讨论】:

          【解决方案10】:

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

          /**
           * 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];
          
          }
          

          【讨论】:

          • 查看source code 似乎提供的类现在是GTMStringEncoding。我没有尝试过,但它看起来是这个问题的一个很好的新解决方案。
          • 从 Mac OS X 10.6 / iOS 4.0 开始,NSData 进行 Base-64 编码。 string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
          • @jrc 确实如此,但请考虑在 Base-64 中编码实际工作字符串。您必须处理 iOS/MacOS 中没有的“网络安全”编码,例如 GTMBase64#webSafeEncodeData。此外,您可能需要添加/删除 Base64“填充”,因此您也可以使用此选项:GTMBase64#stringByWebSafeEncodingData:(NSData *)data padded:(BOOL)padded;
          【解决方案11】:

          这是使用 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
              }
          
          }
          

          【讨论】:

            【解决方案12】:
            @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)
            

            【讨论】:

              【解决方案13】:

              %08x 更改为%08X 以获取大写字符。

              【讨论】:

              • 这作为评论会更好,因为您没有包含任何上下文。只是说'
              【解决方案14】:

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

              想法是从这个answer借来的

              【讨论】:

                【解决方案15】:
                [deviceToken description]
                

                您需要删除空格。

                我个人将base64 编码为deviceToken,但这只是个人喜好问题。

                【讨论】:

                • 这不会得到相同的结果。描述返回: 而我正在寻找:2CF56D5D2FAB0A47....7738CE777E791759
                猜你喜欢
                • 2010-09-27
                • 2013-12-01
                • 2011-04-06
                • 2013-05-07
                • 2015-11-20
                • 2023-03-24
                • 2014-12-17
                • 2011-11-23
                相关资源
                最近更新 更多