一. copy(不可变拷贝)、mutableCopy(可变拷贝)
copy就是拷贝, 拷贝的目的:产生一个副本对象,跟源对象互不影响。
修改了源对象,不会影响副本对象,修改了副本对象,不会影响源对象。
iOS提供了两个拷贝方法:
- copy,不可变拷贝。不管原来是可变还是不可变,copy之后产生的都是不可变副本。
- mutableCopy,可变拷贝。不管原来是可变还是不可变,mutableCopy之后产生的都是可变副本。
我们都知道:当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。
现在你应该明白了,拷贝会产生一个新的副本,就是一个新的对象,所以在不需要这个对象时,就要调用release或者autorelease来释放它。
二. 深拷贝和浅拷贝
- 深拷贝:内容拷贝,产生新的对象
- 浅拷贝:指针拷贝,没有产生新的对象
- 调用copy、mutableCopy后到底是深拷贝还是浅拷贝,系统说了算,只要达到产生一个副本对象,并且副本对象和源对象互不影响的目的就可以。
在MRC环境下,str1是不可变字符串,运行如下代码:
void test2()
{
NSString *str1 = [[NSString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 浅拷贝,指针拷贝,没有产生新对象
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝,内容拷贝,有产生新对象
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p", str1, str2, str3);
[str3 release];
[str2 release];
[str1 release];
}
打印:
test test test
0xaaf541fbfb3f44e9 0xaaf541fbfb3f44e9 0x100667a20
可以发现,str1和str2内存地址一模一样,str3内存地址不一样,我们可以画出内存图:
现在想一下,拷贝的目的是产生一个副本对象,并且副本对象和源对象互不影响。
- 当不可变的str1调用copy,本来str1就是不可变的,要变成不可变的str2,既然都是不可变的,那就谈不上影响这回事,如果重新创建一个一模一样不可变的对象岂不是浪费,所以就干脆变成指针拷贝了(也就是浅拷贝),这样也达到了拷贝的目的。
- 当不可变的str1调用mutableCopy,需要从不可变的str1变成可变的str3,一个不可变,一个可变,为了达到副本对象和源对象互不影响的目的,这里只能使用深拷贝了。
- 上面的str1和str2指向同一个对象,所以[str1 release]和[str2 release]效果都是一样的,他们都是让这个对象的引用计数器减一,两次release之后,引用计数器为0,对象被释放,这样也合情合理。
同理,如果str1是可变字符串呢?
void test3()
{
NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
NSString *str2 = [str1 copy]; // 深拷贝
NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
NSLog(@"%@ %@ %@", str1, str2, str3);
NSLog(@"%p %p %p", str1, str2, str3);
[str1 release];
[str2 release];
[str3 release];
}
打印:
test test test
0x1005182b0 0x7656f0fdae5b70cb 0x100518390
可以看出,上面都是深拷贝。这个也很容易理解,刚开始str1已经是可变字符串了,为了达到拷贝后副本对象和源对象互不影响的目的,就不能指针拷贝了,所以这里都是深拷贝,内存图如下:
同理,我们可以用代码验证NSArray、NSDictionary拷贝之后的情况,总结如下图,验证代码可见文末Demo。
总结:
拷贝的目的就是产生一个新的副本,并且副本对象和源对象互不影响。
为了达到这个目的并且尽量不占用没必要的内存,当调用copy、mutableCopy方法时,系统会自动决定是深拷贝还是浅拷贝(当从不可变到不可变,既然大家都是不可变,那么就直接指针拷贝得了,还省内存,其他情况的拷贝只要有可变的,为了拷贝之后互不影响只能深拷贝了)。
三. copy修饰属性
如果使用copy修饰属性:
@property (copy, nonatomic) NSArray *data;
那这个属性的setter方法就是这样的:
#import "MJPerson.h"
@implementation MJPerson
- (void)setData:(NSArray *)data
{
if (_data != data) {
[_data release];
_data = [data copy];
}
}
- (void)dealloc
{
self.data = nil;
[super dealloc];
}
@end
运行如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *p = [[MJPerson alloc] init];
p.data = @[@"jack",@"rose"];
[p release];
}
return 0;
}
当调用p.data = @[@"jack",@"rose"],就是调用setData:方法,这时候就把传进来的数组进行copy操作,从不可变数组到不可变数组,所以这里是浅拷贝,外面传进来的对象和里面指向的对象都是同一个对象。
如果将上面的不可变数组换成可变数组:
@property (copy, nonatomic) NSMutableArray *data;
执行如下代码:
#import <Foundation/Foundation.h>
#import "MJPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *p = [[MJPerson alloc] init];
p.data = [NSMutableArray array];
[p.data addObject:@"jack"]; //报错
[p.data addObject:@"rose"];
[p release];
}
return 0;
}
发现会报错:
-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x100505750
原因很简单,因为我们使用的是copy修饰,把一个可变数组传进去,copy之后就变成了不可变数组,给不可变数组添加元素当然会报如上错误啦!
总结:
所以,为了防止上面的错误,一般使用copy修饰,右边都放的是不可变对象,以防出现不可预料的错误,如下:
@property (copy, nonatomic) NSString *str;
@property (copy, nonatomic) NSArray *data;
注意:属性的修饰只有copy,不存在什么mutableCopy。
四. 为什么NSString使用copy
观察系统的属性,你会发现,系统的字符串属性都是使用copy:
比如UITextField的text属性
@property(nullable, nonatomic,copy) NSString *text; // default is nil
那这个属性的setter方法就是这样的:
- (void)setText:(NSString *)text
{
if (_text != text) {
[_text release];
_text = [text copy];
}
}
- (void)dealloc
{
self.text = nil;
[super dealloc];
}
思考一下为什么这么设计?
使用copy之后,不管你外面传进来的是可变还是不可变的,我都能保证我里面的属性是不可变的。如果你想修改text,那么你直接给text属性赋一个新的字符串就好了,如下:
UITextField *textField;
textField.text = @"标题";
我不希望你下面这样改,不给你提供这样的方式。
[textField.text appendString:@"标题"];
比如如下代码:
NSMutableString *mutableStr = [NSMutableString stringWithFormat:@"123"];
UITextField *textField;
textField.text = mutableStr;
//修改mutableStr不会影响到textField.text
[mutableStr appendString:@"456"];
textField.text是不可变的,给他传进去一个可变的mutableStr,修改mutableStr不会影响到textField.text,因为它们是拷贝之后两个独立的对象。
如果要是调用[mutableStr appendString:@"456"]之后,显示到UI界面上的文字也改变了,那么这就很诡异了,所以一般对于字符串这种和UI界面相关的,我们都使用copy,对于NSArray、NSDictionary一般还是使用strong。
五. 自定义copy
以前我们讲copy都是拿Foundation框架自带的类进行操作,那么我们自定义类可以使用copy吗?iOS中确实提供了这样的机制来做这种事情。
对于Foundation框架自带的这些类,有copy和mutableCopy操作,如下:
NSArray, NSMutableArray;
NSDictionary, NSMutableDictionary;
NSString, NSMutableString;
NSData, NSMutableData;
NSSet, NSMutableSet;
但是我们自定义的类没有mutableCopy,因为mutableCopy只是Foundation框架自带的这些类才有。而且我们给自定义对象添加可变或不可变也没有意义啊(因为自定义对象里面的属性都可以改嘛,比如:person.name = @"test"),所以对于自定义对象我们不区分什么可变或不可变,我们只要管理好它的copy就好了。
下面就讲一下,如何自定义copy
自定义类如果想实现copy方法,必须遵守NSCopying协议,并且实现copyWithZone方法,copy方法底层就是调用copyWithZone方法。
代码如下:
MJPerson.h
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject <NSCopying>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double weight;
@end
MJPerson.m
#import "MJPerson.h"
@implementation MJPerson
- (id)copyWithZone:(NSZone *)zone
{
MJPerson *person = [[MJPerson allocWithZone:zone] init];
person.age = self.age;
person.weight = self.weight;
return person;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"age = %d, weight = %f", self.age, self.weight];
}
@end
运行代码:
#import <Foundation/Foundation.h>
#import "MJPerson.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *p1 = [[MJPerson alloc] init];
p1.age = 20;
p1.weight = 50;
MJPerson *p2 = [p1 copy]; //将p1拷贝一份
p2.age = 30;
NSLog(@"p1对象:%@", p1);
NSLog(@"p2对象:%@", p2);
NSLog(@"p1对象指针:%p", p1);
NSLog(@"p2对象指针:%p", p2);
[p2 release];
[p1 release];
}
return 0;
}
打印:
p1对象:age = 20, weight = 50
p2对象:age = 30, weight = 50
p1对象指针:0x1004b6fc0
p2对象指针:0x1004b7020
通过打印可知,成功copy了两个对象。
注意:
- 上面的属性都是基本数据类型,所以可以直接赋值。
- 属性如果是copy修饰的字符串,也可以直接赋值,因为set方法内部也是调用copy。
- 属性如果是strong修饰的对象,要使用它的copy方法,前提这个对象也要遵守NSCopying协议,并实现copyWithZone方法。
Demo地址:copy