iOS-内存管理4-Copy

一. copy(不可变拷贝)、mutableCopy(可变拷贝)

copy就是拷贝, 拷贝的目的:产生一个副本对象,跟源对象互不影响
修改了源对象,不会影响副本对象,修改了副本对象,不会影响源对象。

iOS提供了两个拷贝方法:

  1. copy,不可变拷贝。不管原来是可变还是不可变,copy之后产生的都是不可变副本。
  2. mutableCopy,可变拷贝。不管原来是可变还是不可变,mutableCopy之后产生的都是可变副本。

我们都知道:当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。
现在你应该明白了,拷贝会产生一个新的副本,就是一个新的对象,所以在不需要这个对象时,就要调用release或者autorelease来释放它。

二. 深拷贝和浅拷贝

  1. 深拷贝:内容拷贝,产生新的对象
  2. 浅拷贝:指针拷贝,没有产生新的对象
  3. 调用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内存地址不一样,我们可以画出内存图:

内存图.png

现在想一下,拷贝的目的是产生一个副本对象,并且副本对象和源对象互不影响。

  1. 当不可变的str1调用copy,本来str1就是不可变的,要变成不可变的str2,既然都是不可变的,那就谈不上影响这回事,如果重新创建一个一模一样不可变的对象岂不是浪费,所以就干脆变成指针拷贝了(也就是浅拷贝),这样也达到了拷贝的目的。
  2. 当不可变的str1调用mutableCopy,需要从不可变的str1变成可变的str3,一个不可变,一个可变,为了达到副本对象和源对象互不影响的目的,这里只能使用深拷贝了。
  3. 上面的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已经是可变字符串了,为了达到拷贝后副本对象和源对象互不影响的目的,就不能指针拷贝了,所以这里都是深拷贝,内存图如下:

内存图.png

同理,我们可以用代码验证NSArray、NSDictionary拷贝之后的情况,总结如下图,验证代码可见文末Demo。

深拷贝、浅拷贝.png

总结:

拷贝的目的就是产生一个新的副本,并且副本对象和源对象互不影响。
为了达到这个目的并且尽量不占用没必要的内存,当调用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 
#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 

@interface MJPerson : NSObject 
@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 
#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了两个对象。

注意:

  1. 上面的属性都是基本数据类型,所以可以直接赋值。
  2. 属性如果是copy修饰的字符串,也可以直接赋值,因为set方法内部也是调用copy。
  3. 属性如果是strong修饰的对象,要使用它的copy方法,前提这个对象也要遵守NSCopying协议,并实现copyWithZone方法。

Demo地址:copy

你可能感兴趣的:(iOS-内存管理4-Copy)