复制对象 - OC

175 阅读5分钟

在开始讲解复制对象之前先讲解下 赋值

举个例子:

// person 和 child 表示带有两个字符串实例变量 name 和 sex 的 XLPerson对象。
person = child;

这样赋值的结果仅仅是将对象 child 的地址复制到 person 中。在赋值操作结束时,两个变量都指向内存中的同一个地址。使用一条消息对实例变量进行修改,如:

[child setName:@"小明" sex:@"男"];

因为 personchild 引用内存中的同一个对象,所以对应的 namesex都会修改。

Fundation 对象中,将一个变量赋值给另一个对象,仅仅创建另一个对这个对象的引用。例如:dataArray 和 dataArray2 都是 NSMutableArray 对象。执行如下操作:

dataArray2 = dataArray;
[dataArray removeObjectAtIndex: 0];

将从 dataArraydataArray2 两个变量引用的同一个数组中删除第一个元素。

copy 和 mutableCopy

Fundation 类实现名为 copymutableCopy 的方法,可以使用这些方法创建对象的副本。通过实现一个符合 <NSCopying> 协议的方法完成此任务。如果类必须区分产生对象的是可变副本还是不可变副本,还需要根据 <NSMutableCopying> 协议实现一个方法。

分析可变和不可变的字符串、字典、数组 内存地址。

字符串 - 不可变

NSString * str = @"字符串";
id strCopy = [str copy];
id strMutableCopy = [str mutableCopy];

# lldb 命令 打印地址
(lldb) p str
(__NSCFConstantString *) $0 = 0x000000010235c3c0 @"字符串"

(lldb) p strCopy
(__NSCFConstantString *) $1 = 0x000000010235c3c0 @"字符串"

(lldb) p strMutableCopy
(__NSCFString *) $2 = 0x000060000096fbd0 @"字符串"

不可变字符串 copy 后地址不变,mutableCopy后地址修改。

str = @"修改字符串";

(lldb) p str
(__NSCFConstantString *) $3 = 0x000000010235c3e0 @"修改字符串"
(lldb) p strCopy
(__NSCFConstantString *) $4 = 0x000000010235c3c0 @"字符串"
(lldb) p strMutableCopy
(__NSCFString *) $5 = 0x000060000096fbd0 @"字符串"

不可变字符串内容更新后, copymutableCopy 的内存地址不变。

注:str = @"修改字符串"; 相当于重新初始化,分配内存。

字符串 - 可变

NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"可变字符串"];
id mStrCopy = [mStr copy];
id mStrMutableCopy = [mStr mutableCopy];

(lldb) p mStr
(__NSCFString *) $0 = 0x00006000003b1b30 @"可变字符串"

(lldb) p mStrCopy
(__NSCFString *) $1 = 0x00006000003b1b60 @"可变字符串"

(lldb) p mStrMutableCopy
(__NSCFString *) $2 = 0x00006000003b1b90 @"可变字符串"

可变字符串 copymutableCopy后内存地址更新。

[mStr setString:@"修改可变数组"];

(lldb) p mStr
(__NSCFString *) $3 = 0x00006000003b1b30 @"修改可变数组"

(lldb) p mStrCopy
(__NSCFString *) $4 = 0x00006000003b1b60 @"可变字符串"

(lldb) p mStrMutableCopy
(__NSCFString *) $5 = 0x00006000003b1b90 @"可变字符串"

可变字符串内容更新后, copymutableCopy 的地址不变。

字符串拷贝结论如下:

NSStringNSMutableString
copy浅拷贝深拷贝
mutableCopy深拷贝深拷贝

字典、数组

// 不可变字典
NSDictionary *dic = @{
    @"name": @"x",
    @"age": @(22)
};
        
id dicCopy = [dic copy];
id dicMCopy = [dic mutableCopy];

// 可变字典
NSMutableDictionary *mDic = [NSMutableDictionary dictionaryWithDictionary:dic];
        
id mDicCopy = [mDic copy];
id mDicMCopy = [mDic mutableCopy];

// 不可变数组
NSArray *array = [NSArray arrayWithObject:@"qaz"];
        
id arrayCopy = [array copy];
id arrayMCopy = [array mutableCopy];

// 可变数组        
NSMutableArray *mArray = [NSMutableArray arrayWithArray:array];
        
id mArrayCopy = [mArray copy];
id mArrayMCopy = [mArray mutableCopy];

(lldb) p dic
(NSConstantDictionary *) $0 = 0x00000001001017a8 2 key/value pairs
(lldb) p dicCopy
(NSConstantDictionary *) $1 = 0x00000001001017a8 2 key/value pairs
(lldb) p dicMCopy
(__NSDictionaryM *) $2 = 0x00006000015f9240 2 key/value pairs

(lldb) p dic[@"name"]
(__NSCFConstantString *) $3 = 0x00000001000fc398 @"x"
(lldb) p dicCopy[@"name"]
(__NSCFConstantString *) $4 = 0x00000001000fc398 @"x"
(lldb) p dicMCopy[@"name"]
(__NSCFConstantString *) $5 = 0x00000001000fc398 @"x"

(lldb) p mDic
(__NSDictionaryM *) $6 = 0x00006000015f92a0 2 key/value pairs
(lldb) p mDicCopy
(__NSFrozenDictionaryM *) $7 = 0x00006000015f9340
(lldb) p mDicMCopy
(__NSDictionaryM *) $8 = 0x00006000015f9360 2 key/value pairs

(lldb) p mDic[@"name"]
(__NSCFConstantString *) $9 = 0x00000001000fc398 @"x"
(lldb) p mDicCopy[@"name"]
(__NSCFConstantString *) $10 = 0x00000001000fc398 @"x"
(lldb) p mDicMCopy[@"name"]
(__NSCFConstantString *) $11 = 0x00000001000fc398 @"x"
(__NSSingleObjectArrayI *) $0 = 0x0000600000c2c060 @"1 element"
(lldb) p arrayCopy
(__NSSingleObjectArrayI *) $1 = 0x0000600000c2c060 @"1 element"
(lldb) p arrayMCopy
(__NSArrayM *) $2 = 0x0000600000078b70 @"1 element"
(lldb) p mArray
(__NSArrayM *) $3 = 0x0000600000078ba0 @"1 element"
(lldb) p mArrayCopy
(__NSSingleObjectArrayI *) $4 = 0x0000600000c2c090 @"1 element"
(lldb) p mArrayMCopy
(__NSArrayM *) $5 = 0x0000600000078bd0 @"1 element"

(lldb) p array[0]
(__NSCFConstantString *) $6 = 0x0000000104cd4378 @"qaz"
(lldb) p arrayMCopy[0]
(__NSCFConstantString *) $7 = 0x0000000104cd4378 @"qaz"
(lldb) p mArray[0]
(__NSCFConstantString *) $8 = 0x0000000104cd4378 @"qaz"

字符串、字典、数组拷贝结论如下:

1、对应深浅拷贝

不可变(NSString,NSDictionary,NSArray)可变(NSMutableString,NSMutableDictionary,NSMutableArray)
copy浅拷贝深拷贝
mutableCopy深拷贝深拷贝

2、 字典、数组拷贝

  • 浅拷贝,变量引用内存下的同一对象,当对象属性变更时,变量都会更新。
    
  • 深拷贝,变量深拷贝后在内存中创建了不同的对象,但对象内部属性,引用地址相同。当属性赋值修改时,仅对当前对象有效;当属性为可变时,如果通过可变属性修改内容,所有变量都会更新;替换属性变量,则只会对当前变量有效。
    

注:可以试验下嵌套数组 的深浅copy,内部数组为可变数组和不可变数组时对应的内存地址,和属性更新时的变化。

自定义对象

@property声明中 copy 修饰

  1. strong 只是指针引用,修饰的属性指向赋值对象地址,引用计数加1。故:当 NSMutableString 的字符串赋值给 strong 修饰的字符串时,NSMutableString 的字符串变了,它也会改变。
  2. copy 修饰的属性赋值时会重新申请一份内存并赋予相同的内容,两个指针指向不同的内存地址,且该属性为不可变对象。
@interface XLPerson : NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) NSString *fullName;

@property (nonatomic, copy) NSArray *arrayC;
@property (nonatomic, strong) NSArray *arrayS;

@end
XLPerson *per = [XLPerson new];
    
NSMutableString *name = [NSMutableString stringWithString:@"明"];
per.name = name;
[name appendString:@"灵"];
    
NSMutableString *fullName = [NSMutableString stringWithString:@"李"];
per.fullName = fullName;
[fullName appendString:@"灵"];

NSMutableArray *array = [NSMutableArray arrayWithArray:@[ @"1", @"2", @"3"]];
per.arrayC = array;
per.arrayS = array;

[array addObject:@"4"];
(lldb) p fullNameM
(__NSCFString *) $0 = 0x00006000028f8000 @"李灵"
(lldb) p per.fullName
(__NSCFString *) $1 = 0x00006000026c91c0 @"李"
(lldb) p nameM
(__NSCFString *) $2 = 0x00006000028f8030 @"明灵"
(lldb) p per.name
(__NSCFString *) $3 = 0x00006000028f8030 @"明灵"

(lldb) p array
(__NSArrayM *) $0 = 0x00006000019de7f0 @"4 elements"
(lldb) p per.arrayS
(__NSArrayM *) $1 = 0x00006000019de7f0 @"4 elements"
(lldb) p per.arrayC
(__NSArrayI *) $2 = 0x00006000019de7c0 @"3 elements"

上述分析了 NSStringNSArray,可以尝试打印下 NSDictionary

扩展 - @property内存管理策略选择

非ARC

1copy:只用于 NSString\NSDictionary\NSArray\block
2retain:除 NSString\NSDictionary\NSArray\block以外的OC对象
3assign:基本数据类型、枚举、结构体(非OC对象),当2个对象相互引用,一端用retain,一端用assign

ARC

1copy:只用于NSString\NSDictionary\NSArray\block
2strong:OC对象
3weak:当2个对象相互引用,一端用strong,一端用weak
4、assgin:基本数据类型、枚举、结构体(非OC对象)

<NSCopying>

未实现 <NSCopying> 协议时,使用 copy 方法,如下:

XLPerson *xPerson = [per copy];

会抛出异常,异常信息如下:

2022-04-26 14:45:15.796824+0800 RetainCount[10234:6153776] -[XLPerson copyWithZone:]: unrecognized selector sent to instance 0x600002f8e400

2022-04-26 14:45:15.799449+0800 RetainCount[10234:6153776] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[XLPerson copyWithZone:]: unrecognized selector sent to instance 0x600002f8e400'

  1. NSCopying 是对象拷贝的协议;
  2. 实现自定义类复制,必须遵循 <NSCopying> 协议;
  3. 必须实现 <NSCopying> 协议中的 - (id)copyWithZone:(nullable NSZone *)zone

如果要区分开可变副本和不可变副本,需要根据 <NSMutableCopying> 协议实现 - (id)mutableCopyWithZone:(nullable NSZone *)zone 方法。

实现:

1、@interface 指令:

@interface XLPerson : NSObject <NSCopying>

2、在 XLPerson.m 中实现 copyWithZone: 方法,如下:

- (id) copyWithZone:(NSZone *)zone {
    XLPerson *newPer = [[XLPerson allocWithZone:zone] init];
    
    newPer.name = _name;
    newPer.fullName = _fullName;
    
    return newPer;
}

用设值方法和取值方法复制对象

采用设值方法和取值方法复制对象时需要考虑是否需要保护这些值。若在设值方法函数内,只是简单的将参数赋值给相应的实例变量:

- (void) setName:(NSString *) theName {
    _name = theName;
}

当 NSMutableString 的字符串赋值时,NSMutableString 的字符串变了,它也会改变。及可改写为:

- (void) setName:(NSString *) theName {
    _name = [theName copy];
}