Cara terbaik untuk membuat serial NSData menjadi string heksadesimal

101

Saya mencari cara yang bagus untuk membuat serial objek NSData menjadi string heksadesimal. Idenya adalah untuk membuat serial deviceToken yang digunakan untuk pemberitahuan sebelum mengirimnya ke server saya.

Saya memiliki implementasi berikut, tetapi saya berpikir pasti ada cara yang lebih pendek dan lebih bagus untuk melakukannya.

+ (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;
}
sarfata
sumber

Jawaban:

206

Ini adalah kategori yang diterapkan pada NSData yang saya tulis. Ini mengembalikan NSString heksadesimal yang mewakili NSData, di mana datanya bisa berapa pun panjangnya. Mengembalikan string kosong jika NSData kosong.

NSData + Konversi. H

#import <Foundation/Foundation.h>

@interface NSData (NSData_Conversion)

#pragma mark - String Conversion
- (NSString *)hexadecimalString;

@end

NSData + Konversi. 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

Pemakaian:

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

Ini "mungkin" lebih baik daripada menelepon [someData description]dan kemudian menghilangkan spasi, <, dan>. Menelanjangi karakter terasa terlalu "hacky". Ditambah Anda tidak pernah tahu apakah Apple akan mengubah format NSData -descriptiondi masa depan.

CATATAN: Saya telah meminta orang-orang menghubungi saya tentang pemberian lisensi untuk kode dalam jawaban ini. Dengan ini saya mempersembahkan hak cipta saya dalam kode yang saya posting dalam jawaban ini ke domain publik.

Dave
sumber
4
Bagus, tapi dua saran: (1) Saya pikir appendFormat lebih efisien untuk data besar karena menghindari pembuatan NSString menengah dan (2)% x mewakili int unsigned daripada unsigned long, meskipun perbedaannya tidak berbahaya.
svachalek
Jangan menjadi penentang, karena ini adalah solusi bagus yang mudah digunakan tetapi solusi saya pada 25 Jan jauh lebih efisien. Jika Anda mencari jawaban yang dioptimalkan untuk kinerja, lihat jawaban itu . Memberikan suara positif pada jawaban ini sebagai solusi yang bagus dan mudah dipahami.
NSProgrammer
5
Saya harus menghapus cast (unsigned long) dan menggunakan @ "% 02hhx" sebagai format string untuk membuat ini bekerja.
Anton
1
Benar, sesuai developer.apple.com/library/ios/documentation/cocoa/conceptual/… formatnya harus "%02lx"dengan cast itu, atau cast ke (unsigned int), atau lepaskan cast dan gunakan @"%02hhx":)
qix
1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];jauh lebih baik (jejak memori lebih kecil)
Marek R
31

Berikut adalah metode kategori NSData yang sangat dioptimalkan untuk menghasilkan string hex. Meskipun jawaban @Dave Gallagher cukup untuk ukuran yang relatif kecil, kinerja memori dan cpu memburuk untuk data dalam jumlah besar. Saya membuat profil ini dengan file 2MB di iPhone 5. Perbandingan waktu adalah 0,05 vs 12 detik. Jejak memori dapat diabaikan dengan metode ini sementara metode lain meningkatkan tumpukan menjadi 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
sumber
Bagus sekali @Peter - Namun ada solusi yang lebih cepat (tidak lebih dari solusi Anda) - tepat di bawah;)
Moose
1
@Moose, harap rujuk lebih tepat jawaban mana yang Anda bicarakan: suara dan jawaban baru dapat memengaruhi pemosisian jawaban. [edit: oh, biar kutebak, maksudmu jawabanmu sendiri ...]
Cœur
1
Menambahkan cek malloc null. Terima kasih @ Cœur.
Peter
17

Menggunakan properti deskripsi NSData tidak boleh dianggap sebagai mekanisme yang dapat diterima untuk HEX yang mengkodekan string. Properti itu hanya untuk deskripsi dan dapat berubah kapan saja. Sebagai catatan, sebelum iOS, properti deskripsi NSData bahkan tidak mengembalikan datanya dalam bentuk hex.

Maaf telah mengacaukan solusinya tetapi penting untuk mengambil energi untuk membuat serialnya tanpa membonceng API yang dimaksudkan untuk sesuatu yang lain selain serialisasi data.

@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
sumber
Namun, Anda harus membebaskan (hexChars) sebelum kembali.
karim
3
@karim, itu salah. Dengan menggunakan initWithCharactersNoCopy: length: freeWhenDone: dan memiliki freeWhenDone menjadi YES, NSString akan mengendalikan buffer byte tersebut. Memanggil gratis (hexChars) akan mengakibatkan crash. Manfaatnya di sini sangat besar karena NSString tidak perlu melakukan panggilan memcpy yang mahal.
NSProgrammer
@NSProgram terima kasih. Saya tidak melihat penginisialisasi NSSting.
karim
Dokumentasi menyatakan bahwa descriptionmengembalikan string yang dikodekan hex, jadi itu tampaknya masuk akal bagi saya.
Tidak umum
bukankah seharusnya kita memeriksa nilai kembali malloc yang berpotensi null?
Cœur
9

Berikut adalah cara yang lebih cepat untuk melakukan konversi:

BenchMark (waktu rata-rata untuk konversi data 1024 byte yang diulangi 100 kali):

Dave Gallagher: ~ 8.070 ms
NP Programmer: ~ 0.077 ms
Peter: ~ 0.031 ms Yang
Ini: ~ 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
Moose
sumber
1
Anda dapat melihat bagaimana saya menerapkan pemeriksaan malloc di sini ( _hexStringmetode): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive/…
Cœur
Terima kasih untuk referensinya - BTW Saya suka yang 'terlalu panjang' - Itu benar, tapi sekarang saya sudah mengetiknya, siapa saja bisa copy / paste - Saya bercanda - Saya sudah membuatnya - Anda sudah tahu :) Anda benar Selama ini, saya hanya mencoba untuk memukul ke mana pun saya dapat memenangkan mikrodetik! Ini membagi iterasi loop dengan 2. Tapi saya akui itu kurang elegan. Bye
Moose
8

Versi Swift fungsional

Satu liner:

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

Berikut adalah formulir ekstensi yang dapat digunakan kembali dan mendokumentasikan sendiri:

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

Atau, gunakan reduce("", combine: +)alih-alih joinWithSeparator("")dilihat sebagai master fungsional oleh rekan-rekan Anda.


Sunting: Saya mengubah String ($ 0, radix: 16) menjadi String (format: "% 02x", $ 0), karena satu digit angka diperlukan untuk padding nol

NiñoScript
sumber
7

Jawaban Peter ditransfer ke 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 "";
}

Cepat 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
sumber
4

Saya perlu memecahkan masalah ini dan menemukan jawabannya di sini sangat berguna, tetapi saya khawatir tentang kinerja. Sebagian besar jawaban ini melibatkan penyalinan data secara massal dari NSData, jadi saya menulis yang berikut ini untuk melakukan konversi dengan overhead rendah:

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

Ini mengalokasikan ruang dalam string untuk seluruh hasil dan menghindari menyalin konten NSData dengan menggunakan enumerateByteRangesUsingBlock. Mengubah X menjadi x dalam format string akan menggunakan digit hex huruf kecil. Jika Anda tidak menginginkan pemisah antara byte, Anda dapat mengurangi pernyataan tersebut

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

turun menjadi adil

[string appendFormat:@"%02X", byte];
John Stephen
sumber
2
Saya percaya indeks untuk mengambil nilai byte perlu penyesuaian, karena NSRangemenunjukkan kisaran dalam NSDatarepresentasi yang lebih besar , bukan dalam buffer byte yang lebih kecil (parameter pertama dari blok yang dipasok ke enumerateByteRangesUsingBlock) yang mewakili satu bagian bersebelahan yang lebih besar NSData. Jadi, byteRange.lengthmencerminkan ukuran buffer byte, tetapi byteRange.locationlokasi dalam yang lebih besar NSData. Jadi, Anda ingin menggunakan offset, bukan byteRange.location + offset, untuk mengambil byte.
Rob
1
@Rob Terima kasih, saya mengerti maksud Anda dan telah menyesuaikan kodenya
John Stephen
1
Jika Anda mengubah pernyataan menjadi hanya menggunakan single appendFormatAnda mungkin juga harus mengubah self.length * 3keself.length * 2
T. Colligan
1

Saya membutuhkan jawaban yang akan berfungsi untuk string panjang variabel, jadi inilah yang saya lakukan:

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

Berfungsi bagus sebagai ekstensi untuk kelas NSString.

BadPirate
sumber
1
bagaimana jika Apple mengubah cara mereka merepresentasikan deskripsi?
Brenden
1
dalam metode deskripsi iOS13 mengembalikan format yang berbeda.
nacho4d
1

Anda selalu dapat menggunakan [yourString uppercaseString] untuk membuat huruf kapital dalam deskripsi data

Rostyslav Bachyk
sumber
1

Cara yang lebih baik untuk membuat serial / deserialisasi NSData ke NSString adalah dengan menggunakan encoder / decoder Google Toolbox untuk Mac Base64. Cukup seret ke Proyek Aplikasi Anda file GTMBase64.m, GTMBase64.he GTMDefines.h dari paket Foundation dan lakukan sesuatu seperti

/**
 * 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
sumber
Melihat kode sumbernya , tampaknya kelas yang menyediakannya sekarang adalah GTMStringEncoding. Saya belum mencobanya tetapi sepertinya solusi baru yang bagus untuk pertanyaan ini.
sarfata
1
Pada Mac OS X 10.6 / iOS 4.0, NSData melakukan pengkodean Base-64. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc
@jrc itu benar, tetapi pertimbangkan untuk menyandikan string kerja nyata di Base-64. Anda harus berurusan dengan pengkodean "web safe", yang tidak Anda miliki di iOS / MacOS, seperti di GTMBase64 # webSafeEncodeData. Anda juga mungkin perlu menambah / menghapus "padding" Base64, jadi Anda memiliki opsi ini juga: GTMBase64 # stringByWebSafeEncodingData: (NSData *) data padded: (BOOL) padded;
loretoparisi
1

Berikut adalah solusi menggunakan 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
sumber
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)
Ramesh
sumber
0

Ubah %08xuntuk %08Xmendapatkan karakter kapital.

Dan Reese
sumber
6
ini akan lebih baik sebagai komentar karena Anda tidak memasukkan konteks apa pun. Katakan saja
Brenden
0

Swift + Properti.

Saya lebih suka memiliki representasi hex sebagai properti (sama dengan bytesdan descriptionproperti):

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

Ide dipinjam dari jawaban ini

Avt
sumber
-4
[deviceToken description]

Anda harus menghapus spasi.

Secara pribadi saya base64menyandikannya deviceToken, tapi ini masalah selera.

Eddie
sumber
Ini tidak mendapatkan hasil yang sama. deskripsi kembali: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Sementara saya mencari: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata