美文网首页iOS点点滴滴iOS
iOS开发 图文并茂理解深拷贝与浅拷贝

iOS开发 图文并茂理解深拷贝与浅拷贝

作者: 铁头娃_e245 | 来源:发表于2019-01-15 23:32 被阅读82次

    深拷贝和浅拷贝(Shallow copy 和 Deep copy)

    一.概念定义

    对象复制有两种:浅拷贝和深拷贝。 普通副本是浅拷贝,它生成一个新集合,该集合与原始对象共享对象的所有权。 深层副本从原始文件创建新对象,并将其添加到新集合中。

    1.浅拷贝

    浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间,当内存销毁的时候,指向这片内存的几个指针需要重新定义才可以使用,要不然会成为野指针。指针拷贝,即修改A,B也会跟这改变

    浅拷贝.png
    2.深拷贝

    深拷贝是指拷贝对象的具体内容,而内存地址是自主分配的,拷贝结束之后,两个对象虽然存的值是相同的,但是内存地址不一样,两个对象也互不影响,互不干涉。分配了新内存,即A和B没有任何关系,改变A的值B也不会跟着变

    深拷贝.png
    3.总结:

    深拷贝就是内容拷贝,浅拷贝就是指针拷贝。
    本质区别在于:

    • 是否开启新的内存地址(内存地址是否一致)
    • 是否影响内存地址的引用计数

    二.示例分析 -- 实践是检验真理的唯一标准

    在iOS中深拷贝与浅拷贝要更加的复杂,涉及到容器与非容器、可变与不可变对象的copy与mutableCopy。下面用示例逐一分析:

    2.1 非容器类对象的深拷贝、浅拷贝

    这里指的是NSString,NSNumber等等一类的对象

    2.1.1 不可变对象的copy与mutableCopy
    // 非容器类 不可变对象
    - (void)immutableObject {
        // 1.创建一个string字符串。
        NSString *string = @"Jason Mraz";
        NSString *stringB = string;
        NSString *stringCopy = [string copy];
        NSString *stringMutableCopy = [string mutableCopy];
        
        // 2.输出指针指向的内存地址。
        NSLog(@"string = %p",string);
        NSLog(@"stringB = %p",stringB);
        NSLog(@"stringCopy = %p",stringCopy);
        NSLog(@"stringMutableCopy = %p",stringMutableCopy);
    }
    
    //打印结果
    string = 0x1085da078
    stringB = 0x1085da078
    stringCopy = 0x1085da078
    stringMutableCopy = 0x60400005b4b0
    

    可以看到,string、stringB和stringCopy内存地址一致,即指向的是同一块内存区域,进行了浅复制操作。而stringMutableCopy与另外三个变量内存地址不同,系统为其分配了新内存,即进行了深复制操作。
    即在<非容器类 不可变对象> copy实现了浅拷贝,mutableCopy实现了深拷贝

    2.1.2 可变对象的copy与mutableCopy
    // 2.非容器类 可变对象
    - (void)mutableObject {
        // 1.创建一个可变字符串。
        NSMutableString *mString = [NSMutableString stringWithString:@"Coca Cola"];
        NSString *mStringCopy = [mString copy];
        NSMutableString *mutablemString = [mString copy];
        NSMutableString *mStringMutableCopy = [mString mutableCopy];
        
        // 2.在可变字符串后添加字符串。
        [mString appendString:@"AA"];
        [mutablemString appendString:@"BB"];  // 运行时,这一行会报错。
        [mStringMutableCopy appendString:@"CC"];
        
        // 3.输出指针指向的内存地址。
        NSLog(@"Memory location of \n mString = %p,\n mstringCopy = %p,\n mutablemString = %p,\n mStringMutableCopy = %p",mString, mStringCopy, mutablemString, mStringMutableCopy);
    }
    

    在上面代码中,注释2部分为可变字符串拼接字符串,运行到为mutablemString拼接字符串这一行代码时,程序会崩溃,因为通过copy方法获得的字符串是不可变字符串。所以在运行前要注释掉这一行。

    //打印结果 
    mString = 0x608000050470,
    mstringCopy = 0xa1b0ce20f6c30889,
    mutablemString = 0xa1b0ce20f6c30889,
    mStringMutableCopy = 0x608000050770
    

    可以看到使用copy的对象内存地址是相同的,但所有数据都有原数据不同。所以,这里的copy和mutableCopy执行的均为深复制。

    综合上面两个例子,我们可以得出这样结论:

    • 对不可变对象执行copy操作,是指针复制,执行mutableCopy操作是内容复制。
    • 对可变对象执行copy操作和mutableCopy操作都是内容复制。
    • copy返回不可变对象,mutableCopy返回可变对象

    用代码表示如下:

    [immutableObject copy];                 // 浅复制
    [immutableObject mutableCopy];          // 深复制
    [mutableObject copy];                   // 深复制
    [mutableObject mutableCopy];            // 深复制
    

    2.2 容器类对象的深拷贝、浅拷贝

    容器类对象指NSArray、NSDictionary等。容器类对象的深复制、浅复制如下图所示:


    CopyingCollections.png

    对于容器类,除了容器本身内存地址是否发生了变化外,也需要探讨的是复制后容器内元素的变化。

    // 3.浅复制容器类对象
    - (void)shallowCopyCollections {
        // 1.创建一个不可变数组,数组内元素为可变字符串。
        NSMutableString *red = [NSMutableString stringWithString:@"Red"];
        NSMutableString *green = [NSMutableString stringWithString:@"Green"];
        NSMutableString *blue = [NSMutableString stringWithString:@"Blue"];
        NSArray *myArray1 = [NSArray arrayWithObjects:red, green, blue, nil];
        
        // 2.进行浅复制。
        NSArray *myArray2 = [myArray1 copy];
        NSMutableArray *myMutableArray3 = [myArray1 mutableCopy];
        NSArray *myArray4 = [[NSArray alloc] initWithArray:myArray1];
        
        // 3.修改myArray2的第一个元素。
        NSMutableString *tempString = myArray2.firstObject;
        [tempString appendString:@"Color"];  //第一个元素添加Color
        
        myMutableArray3[0] = @"no good";
        [myMutableArray3 addObject:@"11111"];
        [myMutableArray3 removeObjectAtIndex:1];
        
        // 4.输出四个数组内存地址及四个数组内容。
        NSLog(@"Memory location of \n myArray1 = %p, \n myArray2 %p, \n myMutableArray3 %p, \n myArray4 %p",myArray1, myArray2, myMutableArray3, myArray4);
        NSLog(@"Contents of \n myArray1 %@, \n myArray2 %@, \n myMutableArray3 %@, \n myArray4 %@",myArray1, myArray2, myMutableArray3, myArray4);
    }
    

    运行demo,可以看到控制台输出如下:

    myArray1 = 0x6000002402a0, 
    myArray2 0x6000002402a0, 
    myMutableArray3 0x600000240300, 
    myArray4 0x600000240210
    Contents of 
     myArray1 (
        RedColor,
        Green,
        Blue
    ), 
     myArray2 (
        RedColor,
        Green,
        Blue
    ), 
     myMutableArray3 (
        "no good",
        Blue,
        11111
    ), 
     myArray4 (
        RedColor,
        Green,
        Blue
    )
    

    可以看到myArray1和myArray2数组内存地址相同,myMutableArray3和myArray4与其它数组内存地址各不相同。这是因为mutableCopy的对象会被分配新的内存,alloc会为对象分配新的内存空间。

    观察数组内元素,发现修改myArray2数组内第一个元素,四个数组第一个元素都发生了改变,所以这里对于数组内元素只进行了浅复制。但通过对myMutableArray3数组内元素的增删改查发现改变时并未影响其余数组,内存地址也不与其他数组一致,即对于容器类对象进行复制操作时,深拷贝也只是单层拷贝,可以把深拷贝理解为单层深拷贝,容器内元素还是浅拷贝(指针拷贝)

    2.3 自定义对象的深拷贝、浅拷贝

    自定义的类需要我们自己实现NSCopying、NSMutableCopying协议,这样才可以调用copy和mutableCopy方法。如果没有遵循,拷贝时会直接Crash。

    @interface Person : NSObject <NSCopying>
    @property (nonatomic,copy) NSString *name;
    
    -(id)copyWithZone:(NSZone *)zone;
    @end
    

    NSCopying协议只有一个必须实现的copyWithZone: 方法。进入Person.m,实现copyWithZone: 方法。

    -(id)copyWithZone:(NSZone *)zone{
        Person *copy = [[[self class] allocWithZone:zone] init];
        copy->_name = [_name copy];
        //copy.name = self.name;
        return copy;
    }
    

    测试代码如下:

    // 4.自定义对象
    - (void)personCopy {
        //model可变对象, 测试copy的意义:会生成新的地址,如果用=就是指向相同的地址
        Person *person = [Person new];
        person.name = @"qqq";
        Person *newPerson = [Person new];
        
        newPerson = [person copy];
        
        NSLog(@"person.name=%p",person.name);
        NSLog(@"newPerson.name=%p",newPerson.name);
        
        newPerson.name = @"PPP";
        
        NSLog(@"person.name=%p",person.name);
        NSLog(@"newPerson.name=%p",newPerson.name);
    }
    

    打印结果如下:

    person.name=0x10921c288
    newPerson.name=0x10921c288
    
    person.name=0x10921c288
    newPerson.name=0x10921c2e8
    

    断点结果如下:


    image.png

    结合打印数据和断点图片可以看到当自定义类person使用copy方法后,person.name和newPerson.name依然是相同地址,即指针拷贝,但person和newPerson地址不同,结论与可变数组一致,都为单层深拷贝
    至于为什么修改了修改newPerson.name的值person.name地址一样确没有跟着改变,后面会单独一个模块讲述

    三.完全深拷贝的实现

    我们之前测试看到即使是深拷贝也是单层深拷贝,下面我们介绍实现完全深拷贝的方法
    数组存放model(最常见的模型)

    ①第一种方案
    // 5.数组存放自定义对象
    - (void)arrayPersonCopy {
        //copy两块内存地址不一样  深拷贝
        Person *person = [Person new];
        person.name = @"qqq";
        
        Person *newPerson = [Person new];
        newPerson.name = @"www";
        
        //单层深拷贝   内部自定义变量还是指向同一地址  会根据内容改变
        NSArray *listArr = @[person,newPerson];
        NSLog(@"listArr == %@",listArr);
        
        Person *eee = listArr[0];
        eee.name = @"111";
        
        //循环取出内部元素逐个复制
        NSArray *arr = [NSArray array];
        NSMutableArray *tempArr = [NSMutableArray array];
        for (Person *tempP in listArr) {
            Person *newPerson = [tempP copy];
            [tempArr addObject:newPerson];
        }
        arr = tempArr;
        
        Person *ddd = listArr[0];
        ddd.name = @"asjdkasjkdakjas";
        
        NSLog(@"%@",arr);
    }
    

    打印结果如下:

    listArr == (
        "<Person: 0x608000002f50>",
        "<Person: 0x608000002f00>"
    )
    
    arr == (
        "<Person: 0x608000002f60>",
        "<Person: 0x608000002f90>"
    )
    

    可以看到数组本身和内部元素内存地址都不相同,实现了深拷贝,但还是需要注意层级问题,如果model里还有容器类对象依然存在无法完全复制的情况

    ②第二种方案
    NSArray *arr = [[NSArray alloc] initWithArray:listArr copyItems:YES];
    

    可以实现多层深拷贝,但必须保证容器的内部元素都可以实现了NSCopying协议,也就是实现了copyWithZone方法,不然会发生崩溃

    ③第三种方案
    NSArray *arr = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject: listArr]];
    

    需要在model.m中实现归档编/解码方法

    // 归档需要实现的方法
    // 1.编码方法
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        [aCoder encodeObject:self.name forKey:@"PersonName"];
    }
    
    // 2.解码方法
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        if (self = [super init]) {
            self.name = [aDecoder decodeObjectForKey:@"PersonName"];
        }
        return self;
    }
    

    可以使用归档功能实现深复制,可以将对象归档到一个缓冲区,然后把它从缓冲区解归档,这样就实现了深复制。

    四.修改指针指向

    // 6.更改指针指向地址
    - (void)pointToAnotherMemoryAddress {
        // 1.指针a、b同时指向字符串pro
        NSString *a = @"gaoyu";
        NSString *b = a;
        NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
        // 断点1位置
        
        // 2.指针a指向字符串pro648
        a = @"https://www.jianshu.com/u/9b0fa2e3ac62";
        NSLog(@"Memory location of \n a = %p, \n b = %p", a, b);
        // 断点2位置
    }
    
    断点1截图

    指针a指向字符串gaoyu内存地址,b = a表示b是a的浅复制,指针b 也指向字符串gaoyu内存地址。也可以看到a和b的值是一样的


    断点2截图

    可以看到,a、b指针指向不同内存地址,a指向字符串https,b指向字符串gaoyu。

    这是因为字符串是存在于常量区的内存数据,"="号的赋值已经更改了a元素的内存地址,但b还是指向之前的内存地址,所以a和b的内存地址已经不一样了,对应的值也不一样,自然不会因为a的改变而改变b(常量区每个字符串的内存地址都是固定的)

    五.属性中copy与strong特性的区别

    首先要搞清楚的就是对NSString类型的成员变量用copy修饰和用strong修饰的区别。如果使用了copy修饰符,那么在给成员变量赋值的时候就会对被赋值的对象进行copy操作,然后再赋值给成员变量。如果使用的是strong修饰符,则不会执行copy操作,直接将被赋值的变量赋值给成员变量。
    假设有一个NSString类型的成员变量string,对其进行赋值:

    NSString *testString = @"test";
    self.string = testString;
    

    如果该成员变量是用copy修饰的,则等价于:

    self.string = [testString copy];
    

    如果是用strong修饰的,则没有copy操作:

    self.string = testString;   //指针拷贝
    

    知道了使用copy和strong的区别后,我们再来分析为什么要使用copy修饰符。先看一段代码:

    NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"test"];
        self.string = mutableString;
        NSLog(@"%@", self.string);
        [mutableString appendString:@"addstring"];
        NSLog(@"%@", self.string);
    

    如果这里成员变量string是用strong修饰的话,打印结果就是:

    test
    testaddstring
    

    很显然,当mutableString的值发生了改变后,string的值也随之发生改变,因为self.string = mutableString;这行代码实际上是执行了一次指针拷贝。string的值随mutableString的值的发生改变这显然不是我们想要的结果。
    如果成员变量string是用copy修饰,打印结果就是:

    test
    test
    

    这是因为使用copy修饰符后,self.string = mutableString;就等价于self.string = [mutableString copy];,也就是进行了一次深拷贝,所以mutableString的值再发生变化就不会影响到string的值。

    结论:

    NSString类型的成员变量使用copy修饰而不是strong修饰是因为有时候赋给该成员变量的值是NSMutableString类型的,这时候如果修饰符是strong,那成员变量的值就会随着被赋值对象的值的变化而变化。若是用copy修饰,则对NSMutableString类型的值进行了一次深拷贝,成员变量的值就不会随着被赋值对象的值的改变而改变。

    当然,最后附送简单易懂对照表


    葵花宝典
    本文知识点汇总:

    ①深/浅拷贝的定义及理解
    ②copy和mutableCopy的区别与实现的拷贝效果
    ③完全深拷贝的实现方法
    ④NSString在赋值时修改了指针指向
    ⑤属性中copy与strong特性的区别

    面试相关:

    深/浅拷贝这块的知识也是一道经典面试题,如果被问到拷贝相关的知识,多半是在考验你对于内存分配,内存地址相关的内容,Good Luck

    总结

    No1:可变对象的copy和mutableCopy方法都是深拷贝(区别完全深拷贝与单层深拷贝) 。
    No2:不可变对象的copy方法是浅拷贝,mutableCopy方法是深拷贝。
    No3:copy方法返回的对象都是不可变对象。
    No4:"="等号是浅拷贝,即指针拷贝
    
    Demo下载地址:

    https://github.com/gaoyuGood/copy-mutableCopy

    参考文献:

    深复制、浅复制、copy、mutableCopy
    从源码看iOS中的深拷贝和浅拷贝
    OC之数组的copy方法
    iOS 图文并茂的带你了解深拷贝与浅拷贝

    相关文章

      网友评论

        本文标题:iOS开发 图文并茂理解深拷贝与浅拷贝

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