iOS copy和mutableCopy

2,910 阅读10分钟
  1. 深拷贝 和 浅拷贝
  2. copy 和 mutableCopy 介绍和用法。
  3. 为什么修饰block用copy?
  4. 声明 NSArray 和 NSMutableArray 变量时,哪个更适合用 copy 修饰?
  5. 总结

一、深浅拷贝

  1. 什么是浅拷贝、什么是深拷贝
  • 深拷贝: 拷贝出来的对象的地址不一样, 完全的另一个对象, 修改对原对象没有任何影响
  • 浅拷贝: 拷贝的对象的指针地址, 对象还是那个对象, 修改了的话两个都修改了的呢
  1. 对于 NSArray、NSDictionary、NSSet 等容器类对象, 深拷贝可分为"不完全深拷贝" 和 "完全深拷贝" (简单理解一下,以 NSArray 为例, 可能就是 NSArray 中包含了其他对象... 你懂得...)
  • 不完全深拷贝:拷贝出来的容器是新的对象,但是容器里面的对象还是原来对象。
  • 完全深拷贝:拷贝出来的容器是新的对象,容器里面的对象也是新对象。

二、copy 和 mutableCopy介绍和用法

  1. copy
Returns the object returned by copyWithZone:.
//copy 实际上就是调用 copyWithZone: 这个方法
copyWithZone:
Returns a new instance that’s a copy of the receiver.
//返回一个新的实例,这个实例是接收器的副本。

Discussion
The returned object is implicitly retained by the sender, who is responsible for releasing it. The copy returned is immutable if the consideration “immutable vs. mutable” applies to the receiving object; otherwise the exact nature of the copy is determined by the class.

// 返回的对象由发送方隐式保留,发送者负责释放它。如果对价“不可变”与“可变”适用于接收对象,则返回的副本是不可变的;否则,副本的确切性质由类决定。
  1. mutableCopy
Returns the object returned by mutableCopyWithZone: where the zone is nil.
// 返回 mutableCopyWithZone: 返回的对象,其中区域为nil。
mutableCopyWithZone:
Returns a new instance that’s a mutable copy of the receiver.
Required.

mutableCopyWithZone:
Returns a new instance that’s a mutable copy of the receiver.
返回一个新实例,该实例是接收器的可变副本。

举例使用

1 NSString 、NSMutableString 的 copy 和 mutableCopy
//不可变字符串
NSString * str = @"字符串";
id str_copy = [str copy];
id str_mutableCopy = [str mutableCopy];

//可变字符串
NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"可变字符串"];
id mStr_copy = [mStr copy];
id mStr_mutableCopy = [mStr mutableCopy];

NSLog(@"str = %@ str-p = %p", str, str);
NSLog(@"str_copy = %@ str_copy-p = %p", str_copy, str_copy);
NSLog(@"str_mutableCopy = %@ str_mutableCopy-p = %p", str_mutableCopy, str_mutableCopy);
NSLog(@"--------");
NSLog(@"str = %@ str-p = %p", str, str);
NSLog(@"Str_copy = %@ mStr_copy-p = %p", mStr_copy, mStr_copy);
NSLog(@"Str_mutableCopy = %@ mStr_mutableCopy-p = %p", mStr_mutableCopy, mStr_mutableCopy);

到这里可以想一下, 结果是什么的呢?

//不可变字符串
NSString * str = @"字符串";
id str_copy = [str copy];
id str_mutableCopy = [str mutableCopy];

//可变字符串
NSMutableString *mStr = [[NSMutableString alloc] initWithString:@"可变字符串"];
id mStr_copy = [mStr copy];
id mStr_mutableCopy = [mStr mutableCopy];

NSLog(@"str = %@ str-p = %p", str, str);
NSLog(@"str_copy = %@ str_copy-p = %p", str_copy, str_copy);
NSLog(@"str_mutableCopy = %@ str_mutableCopy-p = %p", str_mutableCopy, str_mutableCopy);
NSLog(@"--------");
NSLog(@"str = %@ str-p = %p", str, str);
NSLog(@"Str_copy = %@ mStr_copy-p = %p", mStr_copy, mStr_copy);
NSLog(@"Str_mutableCopy = %@ mStr_mutableCopy-p = %p", mStr_mutableCopy, mStr_mutableCopy);

    /**
     打印结果
     str = 字符串 str-p = 0x104cd0040
     str_copy = 字符串 str_copy-p = 0x104cd0040
     str_mutableCopy = 字符串 str_mutableCopy-p = 0x600001149ec0
     
     str = 变身 str-p = 0x104cd0040
     Str_copy = 可变字符串 mStr_copy-p = 0x600001149dd0
     Str_mutableCopy = 可变字符串 mStr_mutableCopy-p = 0x600001149f20

     */

从打印信息,可以看出对于不可变和可变字符串的 copymutableCopy 的规律: NSString 类型的字符串: string

NSStringNSMutableString
copy浅拷贝深拷贝
mutableCopy深拷贝深拷贝
问题来了: 如果 str 的 copy 修改 str 的值, str_copy 的值是不是一起变化的呢?

第一感觉, 应该是一起变化的, 对吧, 我也这觉得的, 但是还的拿事实说话, 对吧, 继续...

//不可变字符串
NSString * str = @"字符串";
id str_copy = [str copy];
id str_mutableCopy = [str mutableCopy];
str = @"变身";
NSLog(@"str = %@ str-p = %p", str, str);
NSLog(@"str_copy = %@ str_copy-p = %p", str_copy, str_copy);
NSLog(@"str_mutableCopy = %@ str_mutableCopy-p = %p", str_mutableCopy, str_mutableCopy);
/**
打印结果
 str = 变身 str-p = 0x104cd0040
 str_copy = 字符串 str_copy-p = 0x104cd0020
 str_mutableCopy = 字符串 str_mutableCopy-p = 0x600001149ec0
 
 */

那为什么 id str_copy = [str copy]; 是不同的指针指向同一块内存空间,str_copy 从新赋值后两个内存空间就不一样了呢?

因为 copy 出来的对象和源对象是互不影响的, 可理解为

  1. 修改了源对象(属性和行为),不会影响副本对象。
  2. 修改了副本对象属性和行为,不会影响源对象。

所以在 str_copy = str 的时候,两个字符串都是不可变的,指向的同一块内存空间中的 @"字符串", 是不可能变成 @"变身" 的。所以这个时候,为了使优化性能,系统没必要另外提供内存,只生成另外一个指针,指向同一块内存空间就行。 但是当你从新给 str 或者 str_copy 赋值的时候,因为之前的内容不可变,还有互不影响的原则下,这个时候,系统会从新开辟一块内存空间。

so ? 理解了吧....

  1. 容器类型:NSArray 、NSMutableArray, NSDictionary,NSMutableDictionary NSSet, NSMutableSetcopymutableCopy 以数组为例: 直接上代码
NSString * str1 = @"str1";
NSString * str2 = @"str2";
//不可变数组
NSArray *arr = [NSArray arrayWithObjects:str1, str2, nil];
id arr_copy = [arr copy];
id arr_mutableCopy = [arr mutableCopy];
//可变数组
NSMutableArray *mArr = [NSMutableArray arrayWithObjects:str1, str2, nil];
id mArr_copy = [mArr copy];
id mArr_mutableCopy = [mArr mutableCopy];

NSLog(@"\narr = %@ arr-p = %p", arr, arr);
NSLog(@"\narr_copy = %@ arr_copy-p = %p", arr_copy, arr_copy);
NSLog(@"\narr_mutableCopy = %@ arr_mutableCopy-p = %p", arr_mutableCopy, arr_mutableCopy);
NSLog(@"--------");
NSLog(@"\nmArr = %@ mArr-p = %p", mArr, mArr);
NSLog(@"\nmArr_copy = %@ mArr_copy-p = %p", mArr_copy, mArr_copy);
NSLog(@"\nmArr_mutableCopy = %@ mArr_mutableCopy-p = %p", mArr_mutableCopy, mArr_mutableCopy);

/*
打印结果
arr = (
    str1,
    str2
) arr-p = 0x600000ac1320

arr_copy = (
    str1,
    str2
) arr_copy-p = 0x600000ac1320

arr_mutableCopy = (
    str1,
    str2
) arr_mutableCopy-p = 0x600000414780

mArr = (
    str1,
    str2
) mArr-p = 0x600000414750

mArr_copy = (
    str1,
    str2
) mArr_copy-p = 0x600000ac1300

mArr_mutableCopy = (
    str1,
    str2
) mArr_mutableCopy-p = 0x600000414690
    
*/

从输出信息,可以看出对于不可变和可变数组的copy和mutableCopy的规律:

NSArrayNSMutableArray
copy浅拷贝深拷贝
mutableCopy深拷贝深拷贝

总之,一句话,copy一般情况下是浅拷贝,但是在一些情况下,copy又是深拷贝。

2.1 copy 一个可变的数组,会出现什么结果?

NSMutableArray *mArr = [[NSArray arrayWithObjects:@"1", @"2", nil] mutableCopy];
NSMutableArray *mArr_copy = [mArr copy];

NSLog(@"\n mArr = %@ mArr-p = %p mArr class = %@", mArr, mArr, [mArr class]);
NSLog(@"\n mArr_copy = %@ mArr_copy-p = %p mArr_copy class = %@", mArr_copy, mArr_copy, [mArr_copy class]);
/*
打印结果
 mArr = (
    1,
    2
) 
mArr-p = 0x600002ad4db0 
mArr class = __NSArrayM

 mArr_copy = (
    1,
    2
) 
mArr_copy-p = 0x600002432d40 
mArr_copy class = __NSArrayI
*/

copy 为什么不是复制指针的吗? 按理说地址应该是一样的, 结果呢? 内存地址不一样,而且 mArr_copy 是不可变的。

  1. mArr_copy 是通过 copy 得来的,关键点在于 copy 的结果 和 mArr 无关,所以他是不可变的。
  2. mArr 指向的内存空间是可变的,如果对 mArr 进行修改,同一内存空间的内容就会发生变化。 遵循相会不影响的原则 mArr_copy 是不可变的, mArr 是可变的, mArr 的内存空间已经不合适,所以此时的 copy 需要重新开辟内存空间。

2.2 直接 copy 对象

  NSArray *arr = @[@"123", @"456", @"asd"];
  self.mArr = arr;
//  self.mArr = [arr copy];
  NSLog(@"\n arrP = %p \n self.mArrP = %p, self.mArr class = %@", arr, self.mArr, [self.mArr class]);
  
/*
结果
 arrP = 0x6000005ee550 
 self.mArrP = 0x6000005ee550, self.mArr class = __NSArrayI
  */

直接赋值或者 copy 的时候, 由于是不可变数组, 可以直接复制指针,内存地址没有变化。

copy 修饰 NSMutableArray @property (nonatomic, copy) NSMutableArray *mArr;,对 mArr 赋值会有什么结果?

NSArray *arr = @[@"1", @"2", @"3"];
    self.mArr = [arr mutableCopy];
    NSLog(@"\n arrP = %p \n self.mArrP = %p, self.mArr class = %@", arr, self.mArr, [self.mArr class]);

/*
打印结果
 arrP = 0x600002588bd0 
 self.mArrP = 0x600002588ba0
 self.mArr class = __NSArrayI
*/

由此可以看出内存地址不一样,但是 _mArr 是不可变的数组。 因为 _mArr 声明的时候是用 copy 修饰,那么就限制了他为不可变的数组。 赋值的时候是用 mutableCopy, 可变数组的复制方法,所以会从新分配内存。

再接着分析

NSMutableArray * a = [NSMutableArray array].copy;
NSMutableArray * b = [NSMutableArray array].mutableCopy;
 
NSLog(@"\n a = %p \n a class = %@", a, [a class]);
NSLog(@"\n b = %p \n b class = %@", b, [b class]);
/*
 a = 0x7fff8004b160 
 a class = __NSArray0
  
 b = 0x6000030dae50 
 b class = __NSArrayM
*/

  1. copy: 只是把 NSMutableArray 执行了 copy 操作,但是 a 却变成了不可变数组了,
  2. mutableCopy: b为可变数组

由此可见 copy 之后变成了不可变的数组, 也就是上面一步中为什么成了不可变的, 如果修改内容, 就会 Crash, 因为 OC 是动态语言, 只有在执行的时候才知道是什么类型的对象, 为什么懒加载就可以避免了这种 crash

- (NSMutableArray *)mArr {
    if (!_mArr) {
        _mArr = [NSMutableArray array];
    }
    return _mArr;
}

我们看出来使用的 mArr 的实例变量, 实例变量是系统帮你生成的, 不会存在属性那样的 copy 的属性,所以不会直接执行 copy 操作, 把可变数组变成不可变数组, 实例变量的自然也是可变的。

其实证明用 retainstrong 修饰的话, self.mArr = [NSMutableArray array]; 这个方法生成的对象也是可变数组。retain 引用计数 +1; strong也就是强引用一次

三、为什么使用 copy 修饰 block

简单来说,block 就像一个函数指针,指向我们要使用的函数。

就和函数调用一样的,不管你在哪里写了这个 block,只要你把它放在了内存中(通过调用存在这个 block 的方法或者是函数),不管放在栈中还是在堆中,还是在静态区。只要他没有被销毁,你都可以通过你声明的 block 调用他。

说到在类中声明一个 block 为什么要用 copy 修饰的话,那就要先说block的三种类型。

1.NSConcreteGlobalBlock 全局的静态block,不会访问外部的变量。就是说如果你的block没有调用其他的外部变量,那你的block类型就是这种。例如:你仅仅在你的block里面写一个NSLog("hello world");

2.NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。这个 block 就是你声明的时候不用 copy 修饰,并且你的 block 访问了外部变量。

3.NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。好了,这个就是今天的主角 ,用 copy 修饰的 block 。

我们知道,函数的声明周期是随着函数调用的结束就终止了。我们的block是写在函数中的。 如果是全局静态 block 的话,他直到程序结束的时候,才会被被释放。但是我们实际操作中基本上不会使用到不访问外部变量的 block。

如果是保存在栈中的 block,他会随着函数调用结束被销毁。从而导致我们在执行一个包含 block 的函数之后,就无法再访问这个block。因为(函数结束,函数栈就销毁了,存在函数里面的 block 也就没有了),我们再使用block时,就会产生空指针异常。

如果是堆中的 block,也就是 copy 修饰的 block。他的生命周期就是随着对象的销毁而结束的。只要对象不销毁,我们就可以调用的到在堆中的 block。

这就是为什么我们要用 copy 来修饰 block。因为不用copy修饰的访问外部变量的block,只在他所在的函数被调用的那一瞬间可以使用。之后就消失了。

结论

  • copy 修饰的 或者赋值的变量肯定是不可变的。
  • copy 赋值,要看源对象是否是可变的, 来决定只拷贝指针、还是也拷贝对象到另一块内存空间
  • 对象之间 mutableCopy 赋值,肯定会拷贝整个对象内存到另一块内存中
  • 对象之间赋值之后, 再改变, 遵循互不影响的原则

End