Meilleur moyen de sérialiser un NSData dans une chaîne hexadeximale

101

Je recherche un moyen sympa de sérialiser un objet NSData en une chaîne hexadécimale. L'idée est de sérialiser le deviceToken utilisé pour la notification avant de l'envoyer à mon serveur.

J'ai l'implémentation suivante, mais je pense qu'il doit y avoir un moyen plus court et plus agréable de le faire.

+ (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
la source

Réponses:

206

C'est une catégorie appliquée à NSData que j'ai écrite. Elle renvoie une NSString hexadécimale représentant le NSData, où les données peuvent être de n'importe quelle longueur. Renvoie une chaîne vide si NSData est vide.

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

Usage:

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

C'est "probablement" mieux que d'appeler [someData description]puis de supprimer les espaces, <et>. Décapage des personnages semble trop "hacky". De plus, vous ne savez jamais si Apple modifiera le formatage des NSData -descriptionà l'avenir.

REMARQUE: des personnes m'ont contacté au sujet de la licence pour le code dans cette réponse. Par la présente, je dédie mes droits d'auteur sur le code que j'ai publié dans cette réponse au domaine public.

Dave
la source
4
Bien, mais deux suggestions: (1) Je pense que appendFormat est plus efficace pour les données volumineuses car il évite de créer une NSString intermédiaire et (2)% x représente un entier non signé plutôt que non signé long, bien que la différence soit inoffensive.
svachalek
Ne pas être un opposant, car c'est une bonne solution facile à utiliser mais ma solution du 25 janvier est bien plus efficace. Si vous recherchez une réponse optimisée en termes de performances, consultez cette réponse . Voter cette réponse comme une solution agréable et facile à comprendre.
NSProgrammer
5
J'ai dû supprimer le cast (non signé) et utiliser @ "% 02hhx" comme chaîne de format pour que cela fonctionne.
Anton
1
À droite, par developer.apple.com/library/ios/documentation/cocoa/conceptual/ ... le format doit être "%02lx"avec ce casting, ou cast vers (unsigned int), ou abandonner le casting et utiliser @"%02hhx":)
qix
1
[hexString appendFormat:@"%02x", (unsigned int)dataBuffer[i]];est beaucoup mieux (empreinte mémoire plus petite)
Marek R
31

Voici une méthode de catégorie NSData hautement optimisée pour générer une chaîne hexadécimale. Alors que la réponse de @Dave Gallagher est suffisante pour une taille relativement petite, les performances de la mémoire et du processeur se détériorent pour de grandes quantités de données. J'ai profilé cela avec un fichier de 2 Mo sur mon iPhone 5. La comparaison de temps était de 0,05 vs 12 secondes. L'empreinte mémoire est négligeable avec cette méthode tandis que l'autre méthode a augmenté le tas à 70 Mo!

- (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
la source
Nice one @Peter - Il existe cependant une solution encore plus rapide (pas beaucoup plus que la vôtre) - juste en dessous;)
Moose
1
@Moose, veuillez indiquer plus précisément la réponse dont vous parlez: les votes et les nouvelles réponses peuvent affecter le positionnement des réponses. [modifier: oh, laissez-moi deviner, vous voulez dire votre propre réponse ...]
Cœur
1
Ajout de la vérification nulle de malloc. Merci @ Cœur.
Peter
17

L'utilisation de la propriété description de NSData ne doit pas être considérée comme un mécanisme acceptable pour le codage HEX de la chaîne. Cette propriété n'est fournie qu'à titre indicatif et peut changer à tout moment. À noter, avant iOS, la propriété de description NSData ne retournait même pas ses données sous forme hexadécimale.

Désolé de parler de la solution, mais il est important de prendre l'énergie nécessaire pour la sérialiser sans utiliser une API destinée à autre chose que la sérialisation des données.

@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
la source
cependant, vous devez libérer (hexChars) avant le retour.
karim
3
@karim, c'est incorrect. En utilisant initWithCharactersNoCopy: length: freeWhenDone: et en ayant freeWhenDone être YES, la NSString prendra le contrôle de ce tampon d'octets. L'appel gratuit (hexChars) entraînera un crash. L'avantage ici est substantiel car NSString n'aura pas à passer un appel memcpy coûteux.
NSProgrammer
@NSProgrammer merci. Je n'ai pas remarqué l'initialiseur NSSting.
karim
La documentation indique que descriptionrenvoie une chaîne codée en hexadécimal, donc cela me semble raisonnable.
Peu fréquent du
ne devrions-nous pas vérifier si la valeur renvoyée par malloc est potentiellement nulle?
Cœur
9

Voici un moyen plus rapide d'effectuer la conversion:

BenchMark (temps moyen pour une conversion de données de 1024 octets répétée 100 fois):

Dave Gallagher: ~ 8,070 ms
NSProgrammer: ~ 0,077 ms
Peter: ~ 0,031 ms
Celui-ci: ~ 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
élan
la source
1
Vous pouvez voir comment j'ai implémenté la vérification de malloc ici ( _hexStringméthode): github.com/ZipArchive/ZipArchive/blob/master/SSZipArchive
Cœur
Merci pour la référence - BTW j'aime le `` trop long '' - C'est vrai, mais maintenant je l'ai tapé, tout le monde peut copier / coller - Je plaisante - Je l'ai généré - Vous le saviez déjà :) Vous avez raison le long, j'essayais juste de frapper partout où je peux gagner des microsecondes! Il divise les itérations de la boucle par 2. Mais j'avoue qu'il manque d'élégance. Au revoir
Moose
8

Version Swift fonctionnelle

Bon mot:

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

Voici sous une forme d'extension réutilisable et auto-documentée:

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

Vous pouvez également utiliser reduce("", combine: +)au lieu de joinWithSeparator("")pour être considéré comme un maître fonctionnel par vos pairs.


Edit: J'ai changé String ($ 0, radix: 16) en String (format: "% 02x", $ 0), car les nombres à un chiffre devaient avoir un zéro de remplissage

NiñoScript
la source
7

La réponse de Peter portée sur 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 "";
}

Swift 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
la source
4

J'avais besoin de résoudre ce problème et j'ai trouvé les réponses ici très utiles, mais je m'inquiète des performances. La plupart de ces réponses impliquent de copier les données en vrac hors de NSData, j'ai donc écrit ce qui suit pour effectuer la conversion avec une faible surcharge:

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

Cela pré-alloue de l'espace dans la chaîne pour l'ensemble du résultat et évite de copier le contenu NSData à l'aide de enumerateByteRangesUsingBlock. Changer le X en x dans la chaîne de format utilisera des chiffres hexadécimaux minuscules. Si vous ne voulez pas de séparateur entre les octets, vous pouvez réduire l'instruction

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

jusqu'à juste

[string appendFormat:@"%02X", byte];
John Stephen
la source
2
Je crois que l'index pour récupérer la valeur d'octet doit être ajusté, car le NSRangeindique la plage dans la plus grande NSDatareprésentation, pas dans le tampon d'octets plus petit (ce premier paramètre du bloc fourni à enumerateByteRangesUsingBlock) qui représente une seule partie contiguë du plus grand NSData. Ainsi, byteRange.lengthreflète la taille du tampon d'octets, mais byteRange.locationest l'emplacement dans le plus grand NSData. Ainsi, vous souhaitez utiliser simplement offset, non byteRange.location + offset, pour récupérer l'octet.
Rob
1
@Rob Merci, je vois ce que vous voulez dire et j'ai ajusté le code
John Stephen
1
Si vous modifiez la déclaration pour n'utiliser que le single, appendFormatvous devriez probablement également changer le self.length * 3enself.length * 2
T. Colligan
1

J'avais besoin d'une réponse qui fonctionnerait pour des chaînes de longueur variable, alors voici ce que j'ai fait:

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

Fonctionne très bien comme extension de la classe NSString.

BadPirate
la source
1
Et si Apple modifiait la façon dont ils représentaient la description?
Brenden
1
dans la méthode de description iOS13 renvoie un format différent.
nacho4d
1

Vous pouvez toujours utiliser [yourString uppercaseString] pour mettre des lettres en majuscule dans la description des données

Rostyslav Bachyk
la source
1

Un meilleur moyen de sérialiser / désérialiser NSData dans NSString consiste à utiliser l' encodeur / décodeur Google Toolbox pour Mac Base64. Faites simplement glisser dans votre projet d'application les fichiers GTMBase64.m, GTMBase64.he GTMDefines.h du package Foundation et faites quelque chose comme

/**
 * 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
la source
En regardant le code source, il semble que la classe fournissant qui est maintenant GTMStringEncoding. Je ne l'ai pas essayé mais cela ressemble à une excellente nouvelle solution à cette question.
sarfata
1
À partir de Mac OS X 10.6 / iOS 4.0, NSData effectue un encodage Base-64. string = [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]
jrc
@jrc c'est vrai, mais envisagez d'encoder de vraies chaînes de travail en Base-64. Vous devez faire face à l'encodage "Web safe", que vous n'avez pas sous iOS / MacOS, comme dans GTMBase64 # webSafeEncodeData. Vous devrez peut-être également ajouter / supprimer un «remplissage» Base64, donc vous avez également cette option: GTMBase64 # stringByWebSafeEncodingData: (NSData *) données complétées: (BOOL) padded;
loretoparisi
1

Voici une solution utilisant 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
la source
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
la source
0

Remplacez %08xpar %08Xpour obtenir des caractères majuscules.

Dan Reese
la source
6
ce serait mieux comme commentaire puisque vous n'avez inclus aucun contexte. Just sayin '
Brenden
0

Swift + Propriété.

Je préfère avoir une représentation hexadécimale comme propriété (la même chose que byteset descriptionproperties):

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

L'idée est empruntée à cette réponse

Avt
la source
-4
[deviceToken description]

Vous devrez supprimer les espaces.

Personnellement base64j'encode le deviceToken, mais c'est une question de goût.

Eddie
la source
Cela n'obtient pas le même résultat. la description renvoie: <2cf56d5d 2fab0a47 ... 7738ce77 7e791759> Pendant que je recherche: 2CF56D5D2FAB0A47 .... 7738CE777E791759
sarfata