请尊重作者码字的辛苦:原创转载请注明出处
自定义类
故事来源于最近项目中有一个对接ERP功能,在编辑一些信息后突然放弃返回到上一个界面,再进入发现上一次编辑信息在没保存的情况依然生效了。分析:原有的信息肯定是从上一个页面带过来的,第一反应是修改的是同一个对象。
解决:如果要避免这种情况,可以在编辑界面属性用copy修饰如:
@property (nonatomic, copy) ContactPersonModel *ContactPersonModel;
这样无论你在编辑界面怎么修改都不会影响上层数据。
我就知道你不信我改为strong修饰对比:
@property (nonatomic, strong) ContactPersonModel *ContactPersonModel;
通过以上的图对比很直观的就知道发生了什么问题,同时请注意这是由自己定义的类
通过copy和strong修饰可以达到不同的效果,以下观点是本人的理解。
copy:会生成一个新对象,新的内存地址,但是新对象里面的内容还是指向原来的内容,新对象retainCount为1,旧对象不会发生改变。
strong: 源对象和副本都指向同一个地址,也就是这个对象的retainCount加1了,修改会影响源对象。
这里不得不提下在开发中NSString
的使用。在不可变的情况下无论你是用copy还是strong修饰都是指向同一个地址,并且引用计数都会加1;如果是可变的NSMutableString
情况下用copy修饰的属性,源对象和副本指向不同的地址,不会影响源对象,副本对象引用计数为1,类型为不可变的NSString
;用strong修饰源对象和副本指向同一个地址,类型为NSMutableString
,所以修改副本对象会影响到源对象。
@property (nonatomic, copy) NSString *copiedString;
@property (atomic, copy) NSString *atomicCopyString;
@property (nonatomic, strong) NSString *strongString;
@property (atomic, strong) NSString *atomicStrongString;
//不可变string
NSString *string = @"leefenghy";
self.copiedString = string;
self.atomicCopyString = string;
self.strongString = string;
self.atomicStrongString = string;
//可变string
NSMutableString *string = [[NSMutableString alloc] initWithString:@"leefenghy"];
self.copiedString = string;
self.atomicCopyString = string;
self.strongString = string;
self.atomicStrongString = string;
对于assign和weak这里就不讨论了,这两个东西使用场景很明显也很好理解。
解决这个问题还有另外一种思路,自定义类实现NSCopying和NSMutableCopying
也可以达到目的,我这里选择的是NSCopying
,给自己的类实现NSMutableCopying
总感觉怪怪的。
- (id)copyWithZone:(NSZone *)zone
{
ContactPersonModel *model = [[[self class] allocWithZone:zone] init];
// 这里self其实就要被copy的那个对象,很显然要自己赋值给新对象,所以这里可以控制copy的属性
model.personID = self.personID;
model.appellation = self.appellation;
model.appellationStr = self.appellationStr;
model.name = self.name;
model.enName = self.enName;
model.mail = self.mail;
model.tel = self.tel;
model.remark = self.remark;
model.remove = self.remove;
model.seleted = self.seleted;
return model;
}
使用: addVC.ContactPersonModel = [personModel copy];
这样自定义类就有和系统Foundation
类一样有了copy功能了。
由上面代码看出实现- (id)copyWithZone:(NSZone *)zone
时,类alloc init重新分配了内存空间。
最终效果:
系统Foundation类浅拷贝和深拷贝
- 官方的解释图胜过千言万语
There are two kinds of object copying: shallow copies and deep copies.
The normal copy is a shallow copy that produces a new collection that shares ownership of the objects with the original.
Deep copies create new objects from the originals and add those to the new collection.
This difference is illustrated by Figure:
翻译:对象拷贝有两种方式:浅拷贝和深拷贝。浅拷贝并不会拷贝对象本身,仅仅是拷贝指向对象的指针;深拷贝是直接拷贝整个对象内存到另一块内存中(内容拷贝)。
- Shallow Copies
官方注释:
There are a number of ways to make a shallow copy of a collection. When you create a shallow copy,
the objects in the original collection are sent a retain message and the pointers are copied to the new
collection.
翻译:集合类型有多种浅拷贝方式,当你创建一个浅拷贝的集合时,集合里面的对象会发送一个retain消息同时对象的指针会拷贝到这个新的集合。
浅拷贝:发送retain消息,引用计数+1,同时指针被拷贝到新的集合。
- Deep Copies
There are two ways to make deep copies of a collection. You can use the collection’s equivalent of
initWithArray:copyItems: with YES as the second parameter. If you create a deep copy of a collection in
this way, each object in the collection is sent a copyWithZone: message. If the objects in the collection
have adopted the NSCopying protocol, the objects are deeply copied to the new collection, which is
then the sole owner of the copied objects. If the objects do not adopt the NSCopying protocol,
attempting to copy them in such a way results in a runtime error. However, copyWithZone: produces a
shallow copy. This kind of copy is only capable of producing a one-level-deep copy. If you only need a
one-level-deep copy. you can explicitly call for one as in 方式一.
翻译:集合的深拷贝有两种方式:initWithArray:copyItems:
将第二参数设置为YES即可为深拷贝。如果你用这种方式建立一个深拷贝集合,集合里面的每个对象都会发送copyWithZone:
消息。如果集合里面的对象都遵守了NSCopying
协议,这些对象都会深拷贝到这个新的集合,如果集合里面的对象没有遵守NSCopying
协议用这种方式深拷贝,会发生运行时报错。不管怎么样copyWithZone:
这种拷贝方式只能提供one-level-deep copy
单层深拷贝。
方式一:one-level-deep copy
NSArray *deepCopyArray=[[NSArray alloc] initWithArray:someArray copyItems:YES];
方式二: A true deep copy
将集合进行归档(archive),然后解档(unarchive),如:
NSArray* trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
集合的单层深复制 (one-level-deep copy)
看到这里,有同学会问:如果在多层数组中,对第一层进行内容拷贝,其它层进行指针拷贝,
这种情况是属于深复制,还是浅复制?对此,苹果官网文档有这样一句话描述:
This kind of copy is only capable of producing a one-level-deep copy.
If you only need a one-level-deep copy。你可以采用方式一。
If you need a true deep copy, such as when you have an array of arrays, you can archive and then
unarchive the collection, provided the contents all conform to the NSCoding protocol。你可以采用方式二。
从文中可以看出,苹果认为这种复制不是真正的深复制,而是将其称为单层深复制(one-level-deep copy)。
因此,网上有人对浅复制、深复制、单层深复制做了概念区分。
浅复制(shallow copy):在浅复制操作时,对于被复制对象的每一层都是指针复制。
深复制(one-level-deep copy):在深复制操作时,对于被复制对象,至少有一层是深复制。
完全复制(real-deep copy):在完全复制操作时,对于被复制对象的每一层都是对象复制。
当然,这些都是概念性的东西,没有必要纠结于此。只要知道进行拷贝操作时,被拷贝的是指针还是内容即可。
总结
自定义对象copy和mutableCopy
遵守NSCopying
或者NSMutableCopying
协议,符合以上浅拷贝和深拷贝基本原则。
系统对象的copy与mutableCopy方法
不管是集合类对象,还是非集合类对象,接收到copy
和mutableCopy
消息时都遵守以下原则:
- copy返回immutable对象(不可改变对象),所以对copy返回的对象调用mutable对象接口就会crash。
- mutableCopy返回的是mutable对象(可变对象)。
非集合类对象的copy与mutableCopy
非集合类对象经常使用的有NSString、NSNumber、NSValue、NSDate、NSData、NSCache...
copy:
NSString *string = @"leefenghy";
NSString *stringCopy = [string copy];
NSMutableString *mutableString = [string mutableCopy];
通过查看内存:string和stringCopy的地址是一样的,是指针拷贝;mutableString地址不一样,是内容拷贝。
mutableCopy:
NSMutableString *mutString = [[NSMutableString alloc] initWithString:@"leefenghy"];
NSString *stringCopy = [mutString copy];
NSMutableString *mutStringCopy = [mutString copy];
NSMutableString *stringMutCopy = [mutString mutableCopy];
[mutStringCopy appendString:@"github"];
由上图可知:mutString是一个可变string,stringCopy和mutStringCopy的地址是一样的(都是不可变的),其他的都不一样。我猜想系统对可变对象和不可变对象储存位置不一样,应该是分开来管理。实际
NSMutableString *mutStringCopy = [mutString copy];
这样操作后是一个不可变对象,应该存储在静态区。所以执行
[mutStringCopy appendString:@"github"];
程序会crash
-[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xa020004402da5659
,因为不可变对象调用了可变对象接口。
非集合对象:
对immutable对象copy是指针的拷贝,mutableCopy都是内容拷贝;对mutable对象copy后是一个immutable对象和mutable存储在不同的位置,mutableCopy是内容拷贝。
集合类对象的copy与mutableCopy
集合类对象经常使用的有:NSArray、NSDictionary、NSIndexPath、NSSet...
immutable copy:
NSArray *array = @[@[@"ll",@"lee"],@[@"jane",@"kang"]];
NSArray *arrayCopy = [array copy];
NSMutableArray *arrayMutCopy = [array mutableCopy];
由图可知:array、arrayCopy的地址相同,arrayMutCopy和array地址是不同的,说明copy执行了
指针拷贝
,mutableCopy执行了
内容拷贝
。但是这里要指出的arrayMutableCopy
内容拷贝
仅仅是对array对象拷贝(one-level-deep copy)。array、arrayCopy、arrayMutCopy里面的元素还是执行的
指针拷贝
。
mutable copy:
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:@[@[[NSMutableString stringWithString:@"ll"],@"lee"],@[@"jane",@"kang"]]];
NSArray *arrayCopy = [array copy];
NSMutableArray *mutableArrayCopy = [array copy];
NSMutableArray *mutableArrayMutableCopy = [array mutableCopy];
[mutableArrayCopy addObject:@"sark"];
由图可知:array、arrayCopy、mutableArrayCopy、mutableArrayMutableCopy的内存地址都不一样,都是内容拷贝,但是对象里面的元素都是指针拷贝。
[mutableArrayCopy addObject:@"sark"];
执行这段代码同样会crash,因为此时mutableArrayCopy 是一个
NSArrayI
类型是不可变的,没有
addObject:
这个方法。
集合对象:
- 在集合类对象中,对immutable对象进行copy,是
指针拷贝
,mutableCopy是内容拷贝
;对mutable对象进行copy和mutableCopy都是内容拷贝
。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针拷贝
。对于集合对象我想系统对于内存的分配和管理应该是在同一个表中。 - 执行copy后的对象都是不可变的。