How to make a copy of an instance with objective-c runtime
I have a class in my project that has many variables, now I want it to conform to the protocol NSCopying
, so I need to "copy" each variable into - (id)copyWithZone:(NSZone *)zone
. If the variable is an object send a copy to it, if it is a scalar value just use the destination character.
The class can change frequently, which means I can add some variables afterwards and then I or someone else can forget to change the code in the method - (id)copyWithZone:(NSZone *)zone
. To avoid this, I'm going to use the objective-c function to do the job.
Below is a part of the class, the variables in the class are simple object or scalar value. You will notice the unsigned int array, this is an exception, you can omit it because I can manually copy it when the encoding type of the variable [10I]
.
@interface DataObject : NSObject <NSCopying>
{
@public
unsigned int arrPrice[10];
}
@property (nonatomic, copy) NSString *code ;
@property (nonatomic, assign) unsigned char type ;
@property (nonatomic, assign) int digit ;
@property (nonatomic, assign) unsigned int count ;
@property (nonatomic, assign) unsigned short time ;
@property (nonatomic, assign) char season ;
@property (nonatomic, assign) int64_t amount ;
@property (nonatomic, strong) NSMutableArray *mArray ;
@property (nonatomic, assign) BOOL isOk ;
@end
AND copyWithZone:
- (id)copyWithZone:(NSZone *)zone
{
DataObject *obj = [[DataObject allocWithZone:zone] init] ;
unsigned int uVarCount = 0 ;
Ivar *pVarList = class_copyIvarList(self.class, &uVarCount) ;
for (unsigned int i = 0; i < uVarCount; ++i) {
Ivar *pVar = pVarList+i ;
const char *name = ivar_getName(*pVar) ;
const char *typeEncoding = ivar_getTypeEncoding(*pVar) ;
NSString *strTypeEncoding = [NSString stringWithUTF8String:typeEncoding] ;
if ([strTypeEncoding isEqualToString:@"[I10]"]) {
// its arrPrice, use memcpy to copy
memcpy(obj->arrPrice, self->arrPrice, sizeof(arrPrice)) ;
continue ;
} else if ([strTypeEncoding hasPrefix:@"@"]) {
// its a object
id o = object_getIvar(self, *pVar) ;
o = [o copy] ;
object_setIvar(obj, *pVar, o) ;
NSLog(@"var name:%s, type:%s, value:%@", name, typeEncoding, o) ;
} else {
// runtime error
id o = object_getIvar(self, *pVar) ;
object_setIvar(obj, *pVar, o) ;
}
}
free(pVarList) ;
return obj ;
}
I am getting a runtime error where the variable is not an object and I find why , but I donβt know how to solve it.
source to share
True, it's difficult to make a perfect solution, but my goal is to support most scalar values ββand objective-c object.
The basic idea is to use ivar_getOffset
to get the ivar position and change it directly. For that, I also need to know the size of ivar, so I created a method - (unsigned int)sizeOfTypeEncoding:(NSString *)typeEncoding supportOrNot:(BOOL *)support
that calculates the size of the ivar and returns it when ivar is not supported, *support
will NO
and exception will be selected.
So when I add an unsupported ivar to the class, the exception might tell me to do it manually like arrPrice
.
@interface DataObject : NSObject <NSCopying>
{
@public
unsigned int arrPrice[10];
}
@property (nonatomic, copy) NSString *code ;
@property (nonatomic, assign) unsigned char type ;
@property (nonatomic, assign) int digit ;
@property (nonatomic, assign) unsigned int count ;
@property (nonatomic, assign) unsigned short time ;
@property (nonatomic, assign) char season ;
@property (nonatomic, assign) int64_t amount ;
@property (nonatomic, strong) NSMutableArray *mArray ;
@property (nonatomic, assign) BOOL isOk ;
@end
@implementation DataObject
- (unsigned int)sizeOfTypeEncoding:(NSString *)typeEncoding supportOrNot:(BOOL *)support
{
*support = YES ;
unsigned int size = 0 ;
if ([typeEncoding isEqualToString:@"c"] ||
[typeEncoding isEqualToString:@"C"]) {
size = sizeof(char) ;
} else if ([typeEncoding isEqualToString:@"i"] ||
[typeEncoding isEqualToString:@"I"]) {
size = sizeof(int) ;
} else if ([typeEncoding isEqualToString:@"s"] ||
[typeEncoding isEqualToString:@"S"]) {
size = sizeof(short) ;
} else if ([typeEncoding isEqualToString:@"l"] ||
[typeEncoding isEqualToString:@"L"]) {
size = sizeof(long) ;
} else if ([typeEncoding isEqualToString:@"q"] ||
[typeEncoding isEqualToString:@"Q"]) {
size = sizeof(long long) ;
} else if ([typeEncoding isEqualToString:@"f"]) {
size = sizeof(float) ;
} else if ([typeEncoding isEqualToString:@"d"]) {
size = sizeof(double) ;
} else if ([typeEncoding isEqualToString:@"B"]) {
size = sizeof(bool) ;
} else {
*support = NO ;
// v is void
// * is char *
// @ is object
// # is class object
// : is method selector
// [ is array
// { is struct
// ( is union
// b is bit
// ^ pointer to type
// ? other
size = 0 ;
}
return size ;
}
- (id)copyWithZone:(NSZone *)zone
{
DataObject *obj = [[DataObject allocWithZone:zone] init] ;
unsigned int uVarCount = 0 ;
Ivar *pVarList = class_copyIvarList(self.class, &uVarCount) ;
for (unsigned int i = 0; i < uVarCount; ++i) {
Ivar *pVar = pVarList+i ;
const char *name = ivar_getName(*pVar) ;
const char *typeEncoding = ivar_getTypeEncoding(*pVar) ;
NSString *strTypeEncoding = [NSString stringWithUTF8String:typeEncoding] ;
NSLog(@"var name:%s, type:%s", name, typeEncoding) ;
if ([strTypeEncoding isEqualToString:@"[10I]"]) {
// it is arrPrice
memcpy(obj->arrPrice, self->arrPrice, sizeof(arrPrice)) ;
continue ;
}
if ([strTypeEncoding hasPrefix:@"@"]) {
// it is a object
id o = object_getIvar(self, *pVar) ;
o = [o copy] ;
object_setIvar(obj, *pVar, o) ;
} else {
unsigned int size = 0 ;
BOOL support = NO ;
size = [self sizeOfTypeEncoding:strTypeEncoding supportOrNot:&support] ;
if (!support) {
NSString *reason = [NSString stringWithFormat:@"don't support type encoding %@", strTypeEncoding] ;
NSException *exception = [NSException exceptionWithName:@"UnSupportTypeException" reason:reason userInfo:nil] ;
[exception raise] ;
}
ptrdiff_t offset = ivar_getOffset(*pVar) ;
uint8_t *src = (uint8_t *)(__bridge void *)self + offset ;
uint8_t *dst = (uint8_t *)(__bridge void *)obj + offset ;
memcpy(dst, src, size) ;
}
}
free(pVarList) ;
return obj ;
}
@end
source to share
First of all: In OOP, copying is a tricky problem and general 90% solutions are usually not a good idea. However,...
If you have a property without an object reference ( NSInteger
, double
, struct
etc.), you should use -object_getInstanceVariable ()
. But things can get more complicated.
Let's try first:
} else {
long *valuePointer;
object_getInstanceVariable(self, ivar_getName(pVar), & valuePointer) ;
object_setInstanceVariable(obj, ivar_getName(pVar), valuePointer) ;
}
As you can see, I am using a pointer to long
. It is legal if the type is property long
. (And may be legal for any other type with the same size as long
.) As for other types, especially types struct
, you cannot do this because they may be of different sizes. Therefore, to have a complete solution, you need to parse the encoding string.
At least at this point, you should think again if a shared copy is a good idea or not. I prefer not branch.
source to share