最近一直会遇到关于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];
打印所有数组对象的内存地址和数组第一个元素指向的内存地址的结果:
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
使用copy生成的对象如arr_2和原NSArray对象指向的内存地址相同,但是arr_2和arr_4都是不可变类型,所以在数组层面上无法互相影响。
但是在元素层面上而言,数组的所有的第一个元素指向相同的内存地址,也就是说元素间互相影响.
[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 * user = (User *)arr_2[0];
user.name = @"李周周";
因为所有数组的第一个元素都指向同一个user_1的内存地址,所以所有数组的第一个元素都会改变。
以上的两个例子涉及到了有关于深拷贝和浅拷贝:
浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。
深拷贝,不但是对指针的拷贝,而且对指针指向的内容进行拷贝。
简单而言,深拷贝的对象已经和原对象完全没有任何关系了。
上面对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;
}
总结而言的话,只要理解了复制的到底是“对本身层面上的复制”还是“对本身层面之下的复制”,就能很好的在实际的开发中避免因为找不到相应的方法而崩溃的情况了。
网友评论