最近一直会遇到关于 copy 和 mutableCopy 相关的问题,之前在学习内存管理方面有大致的了解过,但是通过不断的了解发现存在特别多的疑惑点,所以趁着年前的时间将这方面的知识点总结总结.
1. 类对象的 Copy 和 MutableCopy
两者在字面上最直观的差别就是是:
copy 复制出不可变的对象,而 mutableCopy 复制出可变的对象.
乍看上去很好理解,但是就着以下的几个方向思考会发现疑点重重:
① 复制的对象与原对象的关系
按理而言,复制应该就是申请了一块新的内存空间,和原对象只是内容相同.但是实际上并没有这么简单,主要通过非集合类和集合类来分别验证:
非集合类
以 NSString 为例
运行之后,在断点处发发现以上所有创建的字符串对象信息:
NSString * str_1 = @"李周笔记";
NSString * str_2 = [str_1 copy];
NSString * str_3 = [str_1 mutableCopy];
NSMutableString * str_4 = [str_1 copy];
NSMutableString * str_5 = [str_1 mutableCopy];
NSLog(@"%p , %p, %p, %p, %p", str_1, str_2, str_3, str_4, str_5);
字符串对象信息
发现 str_4 即便声明为 MutableString 可变类型,但是 isa 指针仍然指向的还是一个不可变的对象类型.而 str_3 虽然声明为了 NSString 不可变类型,但是 isa 指针却指向了一个可变的对象类型.
此处,isa 指针主要的作用是找到该对象能执行的所有方法
简单的说,str_4 调用 MutableString 相关方法时,可以通过编译却无法成功运行.
[str_4 appendString: @"非常好"];
运行之后直接崩溃报错:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendString:'
最后当打印所有对象的地址时,copy 之后的对象指向的地址和 str_1 指向的内存地址一样.
0x10282ef28 , 0x10282ef28, 0x600000447350, 0x10282ef28, 0x600000448100
以 NSMutableString 为例:
在运行之后,断点查看控制台所有字符串的相关信息,发现所有的对象都是可变类型:
NSMutableString * str_1 = [NSMutableString stringWithString: @"李周笔记"];
NSString * str_2 = [str_1 copy];
NSString * str_3 = [str_1 mutableCopy];
NSMutableString * str_4 = [str_1 copy];
NSMutableString * str_5 = [str_1 mutableCopy];
NSLog(@"%p , %p, %p, %p, %p", str_1, str_2, str_3, str_4, str_5);
字符串对象信息
所以,对于 Copy 和 mutableCopy 的理解应该是:
Copy 复制出和原对象相同类型的对象,mutableCopy 复制出一个可变类型的对象.
以上所有字符串的指向内存地址都不相同,也就是互不影响:
0x60400044dda0 , 0x60400022c380, 0x60400044dd40, 0x60400022e700, 0x60400044dce0
集合类
以 NSArray 为例:
① 元素为非集合类型
打印所有数组对象的内存地址和数组第一个元素指向的内存地址的结果:
NSMutableString * str_1 = [NSMutableString stringWithString: @"李周"];
NSMutableString * str_2 = [NSMutableString stringWithString: @"谢华华"];
NSArray * arr_1 = [NSArray arrayWithObjects: str_1, str_2, nil];
NSArray * arr_2 = [arr_1 copy];
NSArray * arr_3 = [arr_1 mutableCopy];
NSMutableArray * arr_4 = [arr_1 copy];
NSMutableArray * arr_5 = [arr_1 mutableCopy];
使用 copy 生成的对象如 arr_2 和原 NSArray 对象指向的内存地址相同,但是 arr_2 和 arr_4 都是不可变类型,所以在数组层面上无法互相影响.
arr_1 ::0x6040002321a0, arr_1[0] ::0x604000249de0
arr_2 ::0x6040002321a0, arr_2[0] ::0x604000249de0
arr_3 ::0x604000241fb0, arr_3[0] ::0x604000249de0
arr_4 ::0x6040002321a0, arr_4[0] ::0x604000249de0
arr_5 ::0x60400024cc90, arr_5[0] ::0x604000249de0
但是在元素层面上而言,数组的所有的第一个元素指向相同的内存地址,也就是说元素间互相影响.
无论是以上面哪种方式操作,都是改变同一片内存区域中的内容,所以造成是改变了所有数组中的第一个元素.
[str_1 appendString:@"周"];
[arr_5[0] appendString:@"xxxx"];
[arr_1[0] appendString:@"yyyyy"];
② 元素为集合类型或自定义类对象
所有数组对象的打印结果为:
User * user_1 = [[User alloc] initWithName: @"李周"];
User * user_2 = [[User alloc] initWithName: @"谢华华"];
NSArray * arr_1 = [NSArray arrayWithObjects: user_1, user_2, nil];
NSArray * arr_2 = [arr_1 copy];
NSArray * arr_3 = [arr_1 mutableCopy];
NSMutableArray * arr_4 = [arr_1 copy];
NSMutableArray * arr_5 = [arr_1 mutableCopy];
NSLog(@" %p, %p, %p, %p, %p", arr_1, arr_2, arr_3, arr_4, arr_5);
0x600000029ee0, 0x600000029ee0, 0x600000252ff0, 0x600000029ee0, 0x600000252db0
使用 copy 创建的对象如 arr_2 和原对象 arr_1 的指向的内存地址相同.并且通过断点可知:
所有数组对象信息
无论使用 Copy 还是 mutableCopy,所有的数组对象中的元素指向的内存地址都是一样的,也就是说如果改变其中一个数组中的元素,其他对象也会同时改变.
如改变 arr_2 中第一个元素的 name 属性
因为所有数组的第一个元素都指向同一个 user_1 的内存地址,所以所有数组的第一个元素都会改变.
User * user = (User * ) arr_2[0];
user.name = @"李周周";
以上的两个例子涉及到了有关于深拷贝和浅拷贝:
浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间.
深拷贝,不但是对指针的拷贝,而且对指针指向的内容进行拷贝.
简单而言,深拷贝的对象已经和原对象完全没有任何关系了.
上面对 NSArray 的拷贝就是浅拷贝,拷贝的对象中的元素与原对象中的元素指向同一个内存地址.如果想要变为深拷贝,可以将上面的代码改为:
如果直接运行的话,会发现程序直接崩溃:
User * user_1 = [[User alloc] initWithName: @"李周"];
User * user_2 = [[User alloc] initWithName: @"谢华华"];
NSMutableArray * arr_1 = [NSMutableArray arrayWithObjects: user_1, user_2, nil];
NSArray * arr_2 = [[NSArray alloc] initWithArray: arr_1 copyItems: YES];
NSMutableArray * arr_5 = [[NSMutableArray alloc] initWithArray: arr_1 copyItems: YES];
NSLog(@" %p, %p, %p", arr_1, arr_2, arr_5);
user_1.name = @"李周周";
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[User copyWithZone:]: unrecognized selector sent to instance 0x60400020ace0'
深拷贝对内容进行拷贝的关键就是调用相应类的 copyWithZone: 方法:
总结而言的话,只要理解了复制的到底是 "对本身层面上的复制" 还是 "对本身层面之下的复制",就能很好的在实际的开发中避免因为找不到相应的方法而崩溃的情况了.
@interface User : NSObject<NSCopying>
-(id)copyWithZone:(NSZone *)zone
{
User *user = [[User alloc] initWithName:_name];
return user;
}
来源: http://www.jianshu.com/p/c79cd7b1fdc6