Introduction
NSData has not always returned the HEX string of its data. There was a time when it behaved far more modestly (and usefully in my opinion) when it just indicated its pointer and length. Let's take advantage of the method swizzling tools we learned about in a previous post so that we can get NSData's description to be something more useful than a data dump - and we'll make it configurable too so that at runtime you can decide how NSData's description should behave. I'll also throw in an optimal NSData-to-hex-string method for good measure, hopefully to help counteract the awful practice some developers have adopted of using the NSData description for that serialization.
Putting NSData's Description on a Diet!
So let's utilize our awesome method swizzling to be able to configure the description of NSData. The information we can show is pretty simple:
- The object info: the class name and the object pointer. Ex// NSData:0xdeadbeef
- The data length: Ex// length=128
- The data in HEX: Ex// DEADBEEF 01234567
So let's define those pieces of information as an enum
mask:
typedef enum
{
NSDataDescriptionOption_None = 0, NSDataDescriptionOption_ObjectInfo = 1 << 0,
NSDataDescriptionOption_Length = 1 << 1,
NSDataDescriptionOption_Data = 1 << 2,
NSDataDescriptionOption_ObjectInfoAndLength =
NSDataDescriptionOption_ObjectInfo | NSDataDescriptionOption_Length,
NSDataDescriptionOption_ObjectInfoAndData =
NSDataDescriptionOption_ObjectInfo | NSDataDescriptionOption_Data,
NSDataDescriptionOption_LengthAndData =
NSDataDescriptionOption_Length | NSDataDescriptionOption_Data,
NSDataDescriptionOption_AllOptions =
NSDataDescriptionOption_ObjectInfo | NSDataDescriptionOption_Length |
NSDataDescriptionOption_Data,
} NSDataDescriptionOptions;
Now we'll declare a category specifically for configuring the description of NSData objects:
@interface NSData (Description)
+ (NSDataDescriptionOptions) descriptionOptions;
+ (NSDataDescriptionOptions) setDescriptionOptions:
(NSDataDescriptionOptions)options;
@end
Now let's get into the swizzling:
static NSDataDescriptionOptions s_options =
NSDataDescriptionOption_None;
@implementation NSData (Description)
+ (NSDataDescriptionOptions) descriptionOptions
{
@synchronized (self) {
return s_options;
}
}
+ (NSDataDescriptionOptions) setDescriptionOptions:(NSDataDescriptionOptions)options
{
@synchronized(self) {
if (s_options != options)
{
if (NSDataDescriptionOption_None == s_options ||
NSDataDescriptionOption_None == options)
{
SwizzleInstanceMethods([NSData class],
@selector(description), @selector(_configuredDescription));
}
NSDataDescriptionOptions tmp = s_options;
s_options = options;
options = tmp;
}
return options;
}
}
#pragma mark - Internal
- (NSString*) _configuredDescription
{
NSDataDescriptionOptions options = s_options;
NSMutableString* dsc = [NSMutableString string];
[dsc appendString:@"<"];
if (NSDataDescriptionOption_ObjectInfo & options)
{
[dsc appendFormat:@"%@:%p", NSStringFromClass([self class]), self];
}
if (NSDataDescriptionOption_Length & options)
{
if (dsc.length > 1)
[dsc appendString:@" "];
[dsc appendFormat:@"length=%d", self.length];
}
if (NSDataDescriptionOption_Data & options)
{
if (dsc.length > 1)
[dsc appendString:@" "];
[dsc appendString:[self hexStringValueWithDelimter:@" "
everyNBytes:4]]; }
[dsc appendString:@">"];
return dsc; }
@end
We'll use a static
variable to keep track of what configuration has been set. When the configuration is None
the description method will be the native implementation, otherwise it's our custom _configuredDescription
method.
We'll synchronize on both getting and setting the description options, but as an added tool for synchronization, we'll have the setDescriptionOptions:
class method return the previous NSDataDescriptionOptions
.
For the setDescriptionOptions:
we take advantage of our method swizzling, knowing that if we are transitioning from the native description to a custom description, we need to swizzle and if we are transitioning from our custom description to the native one, we just swizzle back. So simple! We also swap the static
options with the provided options so that we can return the options
variable as the previously used description options.
Implementing the actual new description is also straightforward, it's just a matter of adding all the matching pieces of information to the description in a priority order and having the pieces be separated by a space and surrounded in triangle braces.
I did put in some handwaving with the hexStringValueWithDelimeter:everyNBytes:
method. Now, remembering that the description method for NSData has changed in the past and could change in the future, it is better to not presume that the description is a HEX string and that's why we will implement our own NSData to HEX string method and use that.
@interface NSData (Serialize)
- (NSString*) hexStringValue;
- (NSString*) hexStringValueWithDelimeter:(NSString*)delim everyNBytes:(NSUInteger)nBytes;
@end
#define HEX_ALPHA_BASE_CHAR 'A'
NS_INLINE void byteToHexComponents(unsigned char byte, unichar* pBig, unichar* pLil)
{
assert(pBig && pLil);
unsigned char c = byte / 16;
if (c < 10)
{
c += '0';
}
else
{
c += HEX_ALPHA_BASE_CHAR - 10;
}
*pBig = c;
c = byte % 16;
if (c < 10)
{
c += '0';
}
else
{
c += HEX_ALPHA_BASE_CHAR - 10;
}
*pLil = c;
}
@implementation NSData (Serialize)
- (NSString*) hexStringValue
{
return [self hexStringValueWithDelimeter:nil everyNBytes:0]; }
- (NSString*) hexStringValueWithDelimeter:(NSString*)delim everyNBytes:(NSUInteger)nBytes
{
NSUInteger len = self.length;
NSUInteger newLength = 0;
BOOL doDelim = nBytes > 0 && delim.length;
if (doDelim)
{
newLength = (len / nBytes) * delim.length;
if ((len % nBytes) == 0 && newLength > 0)
newLength -= delim.length;
}
newLength += len*2; unichar* hexChars = (unichar*)malloc(sizeof(unichar) * newLength);
unichar* hexCharsPtr = hexChars;
unsigned char* bytes = (unsigned char*)self.bytes;
SEL getCharsSel = @selector(getCharacters:range:);
IMP getCharsImp = [delim methodForSelector:getCharsSel];
NSRange getCharsRng = NSMakeRange(0, delim.length);
for (NSUInteger i = 0; i < len; i++)
{
if (doDelim && (i > 0) && (i % nBytes == 0))
{
getCharsImp(delim, getCharsSel, hexCharsPtr, getCharsRng);
hexCharsPtr += getCharsRng.length;
}
byteToHexComponents(bytes[i], hexCharsPtr++, hexCharsPtr++);
}
assert(hexCharsPtr - newLength == hexChars);
return [[[NSString alloc] initWithCharactersNoCopy:hexChars
length:newLength
freeWhenDone:YES] autorelease];
}
@end
Find this code on github.