美文网首页
iOS-内存管理4-Copy

iOS-内存管理4-Copy

作者: Imkata | 来源:发表于2019-12-24 18:28 被阅读0次

    一. 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 <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了两个对象。

    Demo地址:copy

    相关文章

      网友评论

          本文标题:iOS-内存管理4-Copy

          本文链接:https://www.haomeiwen.com/subject/duefoctx.html