[iphone] NSData를 16 진수 문자열로 직렬화하는 가장 좋은 방법

NSData 개체를 16 진수 문자열로 직렬화하는 멋진 코코아 방법을 찾고 있습니다. 아이디어는 내 서버로 보내기 전에 알림에 사용되는 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;
}



답변

제가 작성한 NSData에 적용되는 카테고리입니다. NSData를 나타내는 16 진수 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]공백, < ‘s 및>를 호출 한 다음 제거하는 것보다 “아마”더 낫습니다 . 벗기는 문자는 너무 “해키”느낌이 듭니다. 또한 Apple이 -description미래 에 NSData의 형식을 변경할지 알 수 없습니다 .

참고 : 이 답변의 코드에 대한 라이선스에 대해 사람들이 저에게 연락했습니다. 이로써 나는이 답변에 게시 한 코드에 내 저작권을 공개 도메인에 바칩니다.


답변

다음 은 16 진 문자열을 생성하기 위해 고도로 최적화 된 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 @"";
}


답변

NSData의 설명 속성을 사용하는 것은 문자열을 HEX 인코딩하는 데 허용되는 메커니즘으로 간주되어서는 안됩니다. 이 속성은 설명 용이며 언제든지 변경할 수 있습니다. 참고로 iOS 이전의 NSData 설명 속성은 16 진수 형식으로 데이터를 반환하지도 않았습니다.

솔루션에 대해 고민해서 미안하지만 데이터 직렬화 이외의 용도로 사용되는 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


답변

변환을 수행하는 더 빠른 방법은 다음과 같습니다.

BenchMark (1024 바이트 데이터 변환의 평균 시간이 100 회 반복됨) :

데이브 갤러거 : ~
8.070ms NS 프로그래머 : ~ 0.077ms
Peter : ~ 0.031ms
This One : ~ 0.017ms

@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


답변

기능적인 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)으로 변경했습니다. 한 자리 숫자는 패딩 0이 필요하기 때문입니다.


답변

Peter의 대답은 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 "";
}

스위프트 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 "";
}


답변

이 문제를 해결해야했고 여기서 답변이 매우 유용하다는 것을 알았지 만 성능이 걱정됩니다. 이 답변의 대부분은 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로 변경하면 소문자 16 진수를 사용합니다. 바이트 사이에 구분 기호를 원하지 않으면 문을 줄일 수 있습니다.

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

아래로

[string appendFormat:@"%02X", byte];