美文网首页
iOS - 浅拷贝与深拷贝

iOS - 浅拷贝与深拷贝

作者: 阳光下的叶子呵 | 来源:发表于2022-03-24 19:53 被阅读0次

    相关链接

    Why Learn?

    浅拷贝和深拷贝是必须要掌握的知识点,工作中也会频繁用到,所以在这里记录总结一下自己的理解,方便日后查看。

    Ask?

    如果你觉得浅拷贝就是copy,深拷贝就是mutableCopy的话,那就大错特错了,请继续往下看

    定义

    首先浅拷贝是什么?深拷贝又是什么呢?

    这两种都是拷贝(复制)对象的意思,不同的是

    • 浅拷贝在拷贝对象的过程中
      • 拷贝了指向对象内存地址的指针
      • 拷贝后新的指针仍然指向原来的内存地址
    • 深拷贝在拷贝对象的过程中
      • 不仅拷贝了指向对象内存地址的指针
      • 还会拷贝原指针所指向的内存地址所存储的内容
      • 并且在堆中重新生成一块内存地址来存放所拷贝的内容
      • 然后新的指针会指向新的内存地址

    简单对比

    浅拷贝:拷贝指针,不开辟内存地址 深拷贝:拷贝指针和内容,开辟内存地址(存放拷贝的内容)

    如何判断?

    要判断一个拷贝是浅拷贝还是深拷贝,不仅看copy还是mutableCopy,还要考虑拷贝的对象是可变的还是不可变的

    举例说明,假如存在不可变对象A可变对象B,对AB分别进行copymutableCopy,结果如下

    • 例子一:对不可变对象A 进行 copy
    NSString *A = @"aaa";
    id C = [A copy];
    NSLog(@"\n不可变-copy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&A,&C,A,A,C,C);
    复制代码
    

    控制台打印

    2020-06-03 17:52:29.341114+0800 Demo[12665:880626] 
    不可变-copy
    0x7ffee06c70e0 = 原对象指针地址
    0x7ffee06c70d8 = copy的对象指针地址
    0x10f53d358 = 原对象的内存地址 - aaa
    0x10f53d358 = copy的对象的内存地址 - aaa
    复制代码
    

    从打印结果我们可以看出,对不可变对象进行copy

    • 指向内存地址的指针地址不一样
    • 内存地址一样(存放的内容都是aaa

    所以这是一个浅拷贝

    • 例子二:对不可变对象A进行mutableCopy
    NSString *A = @"aaa";
    id C = [A mutableCopy];
    NSLog(@"\n不可变-mutableCopy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&A,&C,A,A,C,C);
    复制代码
    

    控制台打印

    2020-06-03 17:53:01.600945+0800 Demo[12679:881248] 
    不可变-mutableCopy
    0x7ffee97830e0 = 原对象指针地址
    0x7ffee97830d8 = copy的对象指针地址
    0x106481358 = 原对象的内存地址 - aaa
    0x60000209ab20 = copy的对象的内存地址 - aaa
    复制代码
    

    从打印结果我们可以看出,对不可变对象进行mutableCopy

    • 指向内存地址的指针地址不一样
    • 内存地址不一样,开辟了新的内存地址(存放的内容都是aaa

    所以这是一个深拷贝

    • 例子三:对可变对象B进行copy
    NSMutableString *B = [NSMutableString stringWithString:@"bbb"];
    id C = [B copy];
    NSLog(@"\n可变-copy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&B,&C,B,C,B,C);
    复制代码
    

    控制台打印

    2020-06-03 17:51:22.168009+0800 Demo[12647:879529] 
    可变-copy
    0x7ffedfc920e0 = 原对象指针地址
    0x7ffedfc920d8 = copy的对象指针地址
    0x600003af3cc0 = 原对象的内存地址 - bbb
    0xd6bb373121e18d02 = copy的对象的内存地址 - bbb
    复制代码
    

    从打印结果我们可以看出,对可变对象进行copy

    • 指向内存地址的指针地址不一样
    • 内存地址不一样,开辟了新的内存地址(存放的内容都是bbb

    所以这是一个深拷贝

    • 例子四:对可变对象B进行mutableCopy
    NSMutableString *B = [NSMutableString stringWithString:@"bbb"];
    id C = [B mutableCopy];
    NSLog(@"\n可变-mutableCopy\n%p = 原对象指针地址\n%p = copy的对象指针地址\n%p = 原对象的内存地址 - %@\n%p = copy的对象的内存地址 - %@",&B,&C,B,B,C,C);
    复制代码
    

    控制台打印

    2020-06-03 17:54:34.595064+0800 Demo[12717:882756] 
    可变-mutableCopy
    0x7ffee78cf0e0 = 原对象指针地址
    0x7ffee78cf0d8 = copy的对象指针地址
    0x600001143690 = 原对象的内存地址 - bbb
    0x600001143660 = copy的对象的内存地址 - bbb
    复制代码
    

    从打印结果我们可以看出,对不可变对象进行mutableCopy

    • 指向内存地址的指针地址不一样
    • 内存地址不一样,开辟了新的内存地址(存放的内容都是bbb

    所以这是一个深拷贝

    总结

    [immutableObject copy];        //浅拷贝,拷贝后的对象不可变
    [immutableObject mutableCopy]; //深拷贝,拷贝后的对象可变
    [mutableObject copy];          //深拷贝,拷贝后的对象不可变(不安全,不建议用)
    [mutableObject mutableCopy];   //深拷贝,拷贝后的对象可变
    复制代码
    

    通过四种不同情况的代码测试,最终得到的结论是

    • 如果对象是不可变的,copy会进行浅拷贝,mutableCopy会进行深拷贝
    • 如果对象是可变的,那么无论是copy还是mutableCopy都会进行深拷贝
    • 如果是copy,拷贝后的对象不可变
    • 如果是mutableCopy,拷贝后的对象可变

    拓展 - 面试题:修饰属性时用strong还是copy?

    最安全的做法(推荐):修饰不可变对象(NSStringNSArrayNSDictionary等)用copy,修饰可变对象(NSMutableStringNSMutableArrayNSMutableDictionary等)用strong

    • Why?

    我们用字符串(NSString&&NSMutableString)来做测试

    • 定义2个NSString类型的属性,分别用strong和copy修饰
    @property (nonatomic,strong) NSString *strStrong;
    @property (nonatomic,copy) NSString *strCopy;
    复制代码
    

    测试一:strong修饰,赋值不可变

    • 创建一个不可变的局部变量 immutableStr
    • strStrong指向immutableStr所指向的内存地址
    • 再将immutableStr指向新的内存地址
    NSString *immutableStr = @"aaa";
    self.strStrong = immutableStr;
    immutableStr = @"bbb";
    复制代码
    

    经过LLDB调试可以看出,运行过了前两行代码,immutableStrstrStrong指向同一片内存地址,但是他们的指针变量是不一样的

    //运行完前两句代码
    //NSString *immutableStr = @"aaa";
    //self.strStrong = immutableStr;
    //指向的内存地址相同,都是0x000000010bb3d338
    (lldb) p immutableStr
    (__NSCFConstantString *) $0 = 0x000000010bb3d338 @"aaa"
    (lldb) p _strStrong
    (__NSCFConstantString *) $1 = 0x000000010bb3d338 @"aaa"
    //指针地址不同,0x00007ffee40c70e8和0x00007fd7194098b8
    (lldb) p &immutableStr
    (NSString **) $2 = 0x00007ffee40c70e8
    (lldb) p &_strStrong
    (NSString **) $3 = 0x00007fd7194098b8
    
    //运行完第三句代码
    //immutableStr = @"bbb";
    (lldb) p immutableStr
    (__NSCFConstantString *) $4 = 0x000000010bb3d358 @"bbb"
    (lldb) p &immutableStr
    (NSString **) $5 = 0x00007ffee40c70e8
    (lldb) p _strStrong
    (__NSCFConstantString *) $6 = 0x000000010bb3d338 @"aaa"
    (lldb) p &_strStrong
    (NSString **) $7 = 0x00007fd7194098b8
    复制代码
    

    第三句代码的意思是让immutableStr指针变量指向存放@"bbb"的新的内存地址,而且这个操作并不影响strStrong,因为只是改变了immutableStr的指针指向,所以再次打印strStrong的指针地址和内存地址还是跟原来一样,没有变化。 tips:immutableStr是不可变对象,不能改变所指向内存地址的值,只能重新生成一块新的内存地址并指向它。

    测试二:strong修饰,赋值可变

    • 创建一个可变的局部变量 mutableStr
    • strStrong指向mutableStr所指向的内存地址
    • 再给mutableStr追加字符串
    NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
    self.strStrong = mutableStr;
    [mutableStr appendString:@"bbb"];
    复制代码
    

    经过LLDB调试可以看出,运行过了前两行代码,mutableStrstrStrong指向同一片内存地址,但是他们的指针变量是不一样的

    //运行完前两句代码
    //NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
    //self.strStrong = mutableStr;
    (lldb) p mutableStr
    (__NSCFString *) $0 = 0x00006000027ddc20 @"aaa"
    (lldb) p _strStrong
    (__NSCFString *) $1 = 0x00006000027ddc20 @"aaa"
    (lldb) p &mutableStr
    (NSMutableString **) $2 = 0x00007ffeee7460e8
    (lldb) p &_strStrong
    (NSString **) $3 = 0x00007f871e40b658
    
    //运行完第三句代码
    //[mutableStr appendString:@"bbb"];
    (lldb) p mutableStr
    (__NSCFString *) $4 = 0x00006000027ddc20 @"aaabbb"
    (lldb) p _strStrong
    (__NSCFString *) $5 = 0x00006000027ddc20 @"aaabbb"
    (lldb) p &mutableStr
    (NSMutableString **) $6 = 0x00007ffeee7460e8
    (lldb) p &_strStrong
    (NSString **) $7 = 0x00007f871e40b658
    复制代码
    
    • 可以发现,运行完第三句代码后,immutableStrstrStrong的值都变成了@"aaabbb"
    • 看到这里,你可能会有疑问,我只是给immutableStr追加了字符串@“bbb”,为什么strStrong也会跟着变成@"aaabbb"了呢?

    解释:由于mutableStr是可变对象,所以它可以在mutableStr指向的那片内存的字符串后面追加字符串@"bbb",那么这个问题就来了,这个操作实际上已经改变了原来的那片内存地址的值,那么会出现什么问题呢?没错,相当于strStrong所指向的内存地址的值也被修改了,所以strStrong对应内存中的值也变成了@"aaabbb"

    如果你对指针和内存管理不熟悉的话,很容易会遇到这种难以发现的bug,因为站在你的角度来看,你只是改变了A,不会影响B,但实际上B已经被改变了。

    那么如何防止这种问题的产生?

    答:使用copy修饰

    我们再来测试一下用copy修饰后的字符串,不可变的赋值我们就不测试了,因为原理跟上面的一样,我们来测试一下可变的赋值

    测试三:copy修饰,赋值可变

    • 创建一个可变的局部变量 mutableStr
    • strCopy指向mutableStr所指向的内存地址
    • 再给mutableStr追加字符串
    NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
    self.strCopy = mutableStr;
    [mutableStr appendString:@"bbb"];
    复制代码
    

    经过LLDB调试可以看出,运行过了前两行代码,mutableStrstrCopy指向了不同的内存地址,指针变量也是不一样的

    //运行完前两句代码
    //NSMutableString *mutableStr = [NSMutableString stringWithString:@"aaa"];
    //self.strCopy = mutableStr;
    (lldb) p mutableStr
    (__NSCFString *) $0 = 0x000060000271c0f0 @"aaa"
    (lldb) p _strCopy
    (NSTaggedPointerString *) $1 = 0x9c1cf86b18404957 @"aaa"
    (lldb) p &mutableStr
    (NSMutableString **) $2 = 0x00007ffee338a0e8
    (lldb) p &_strCopy
    (NSString **) $3 = 0x00007ff821d0a320
    
    //运行完第三句代码
    //[mutableStr appendString:@"bbb"];
    (lldb) p mutableStr
    (__NSCFString *) $4 = 0x000060000271c0f0 @"aaabbb"
    (lldb) p _strCopy
    (NSTaggedPointerString *) $5 = 0x9c1cf86b18404957 @"aaa"
    (lldb) p &mutableStr
    (NSMutableString **) $6 = 0x00007ffee338a0e8
    (lldb) p &_strCopy
    (NSString **) $7 = 0x00007ff821d0a320
    复制代码
    
    • 当给mutableStr追加完字符串之后,可以看出只有mutableStr改变了内存中的值,而strCopy仍然是@"aaa"
    • 这是因为用copy修饰strCopy属性,底层默认会在strCopysetter方法中对所赋值的可变字符串mutableStr先做一次copy操作然后再赋值,那么我们很容易能推出来这是一次深拷贝,会开辟新的内存地址,那么使得strCopy会指向新的内存地址,存放copy后的值
    • 那么就能得出mutableStr追加字符串并不会影响strCopy了,因为现在他们是两个不同的指针分别指向不同的内存地址

    如果你已经看完这么详细(啰嗦:D)的一个分析,相信上面那个面试题就可以很好的回答出来了。

    参考文章 面试题分解—「浅复制/深复制、定义属性使用copy还是strong ?

    相关文章

      网友评论

          本文标题:iOS - 浅拷贝与深拷贝

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