iOS进阶专项分析(六)、OC内存管理之深拷贝与浅拷贝

先来看两个经典的面试题:

1、属性NSString为什么要用copy修饰?而不是用strong?
2、NSArrayNSMutableArraycopy修饰还是strong?

带着这两个问题我们开始本篇的内容:

  • 1、案例1:非容器类对象(例如NSString)的深拷贝与浅拷贝
  • 2、案例2:容器类对象(例如NSArrayNSMutableArray)的深拷贝与浅拷贝
  • 3、OC内存管理:深拷贝与浅拷贝知识点
  • 4、项目中实际用法的经验总结

一、案例1:非容器类对象(例如NSString)的深拷贝与浅拷贝


测试方法:

用可变字符串NSMutableString给这NSString字符串属性赋值,赋值完成后,故意修改了这个可变字符串,看看strongcopy这两种关键字修饰的属性是否变化

新建工程实现如下代码

#import "ViewController.h"
@interface ViewController ()

@property (nonatomic, strong)NSString * testStrongString;//浅拷贝
@property (nonatomic, copy)NSString * testCopyString;//深拷贝

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    //1、字符串验证
    [self conformTestString];
}

- (void)conformTestString {
    //用可变字符串分别给这两个属性赋值,然后修改可变字符串的值
    
    NSMutableString * tempMutableString = [[NSMutableString alloc] initWithString:@"初始值"];
    self.testStrongString = tempMutableString;
    self.testCopyString = tempMutableString;
    NSLog(@"初始\n浅拷贝的地址:%p\n浅拷贝的值:%@", self.testStrongString, self.testStrongString);
    NSLog(@"初始\n深拷贝的地址:%p\n深拷贝的值:%@", self.testCopyString, self.testCopyString);
    
    //修改可变字符串
    [tempMutableString setString:@"修改后的值"];
    NSLog(@"修改后\n浅拷贝字符串的地址:%p\n浅拷贝字符串的值:%@", self.testStrongString, self.testStrongString);
    NSLog(@"修改后\n深拷贝字符串的地址:%p\n深拷贝字符串的值:%@", self.testCopyString, self.testCopyString);
}

@end

运行上面的代码,打印台:

2020-06-22 10:33:47.098767+0800 TestProject strong©[1287:51282] 初始
浅拷贝的地址:0x6000036a4630
浅拷贝的值:1
2020-06-22 10:33:47.099390+0800 TestProject strong©[1287:51282] 初始
深拷贝的地址:0x60000389fc00
深拷贝的值:1


2020-06-22 10:33:47.099660+0800 TestProject strong©[1287:51282] 修改后
浅拷贝字符串的地址:0x6000036a4630
浅拷贝字符串的值:9999999
2020-06-22 10:33:47.099885+0800 TestProject strong©[1287:51282] 修改后
深拷贝字符串的地址:0x60000389fc00
深拷贝字符串的值:1

对比这两种不同关键字修饰的测试字符串,我们发现了一个可怕的结果:

在我们没有直接对属性进行重新赋值的情况下,发现用strong修饰的字符串的值做了变化!!!而copy修饰的属性值则没有变化

这也就意味着我们用strong修饰的这个属性的值是随动的!而copy修饰的属性只要我们不重新赋值,值就是固定不变的

针对这个案例以及文章前第一个问题,做出回答如下:
问题:

1、属性NSString为什么要用copy修饰?而不是用strong?

Copy是为了安全 ,防止 NSMutableString 赋值给 NSString 时,前者修改引起后者值变化而使用的。

下面继续来探讨第二个问题:

案例2:容器类对象(例如NSArray,NSMutableArray)的深拷贝与浅拷贝


1、容器类对象(例如NSArray,NSMutableArray)作为属性时,应该是用strong还是copy?可变容器对象与不可变容器对象在使用上是否有区别?

1.1 针对不可变数组NSArray作为属性时,应该用copy还是strong?
测试方法:

用可变数组NSMutableArrayNSArray容器属性赋值,赋值完成后,故意移除数组中的元素,看看strongcopy这两种关键字修饰的NSArray属性是否发生变化

代码如下:

声明两个属性

@property (nonatomic, strong)NSArray * tempStrongArray;
@property (nonatomic, copy)NSArray * tempCopyArray;

viewDidLoad中调用

//用可变数组分别给这两个属性赋值,然后移除可变数组中的元素
- (void)testArray {
    NSMutableArray * aArray = [NSMutableArray arrayWithArray:@[@"1",@"2",@"3",@"4",@"5"]];
    
    self.tempStrongArray = aArray;
    self.tempCopyArray = aArray;
    
    NSLog(@"初始\n浅拷贝的地址:%p\n浅拷贝的成员数量:%ld", self.tempStrongArray, self.tempStrongArray.count);
    NSLog(@"初始\n深拷贝的地址:%p\n浅拷贝的成员数量:%ld", self.tempCopyArray, self.tempCopyArray.count);
    
    [aArray removeLastObject];
    NSLog(@"初始\n浅拷贝的地址:%p\n浅拷贝的成员数量:%ld", self.tempStrongArray, self.tempStrongArray.count);
    NSLog(@"初始\n深拷贝的地址:%p\n浅拷贝的成员数量:%ld", self.tempCopyArray, self.tempCopyArray.count);
    
}

打印结果

2020-06-22 21:53:08.093548+0800 Test strong©[3829:284299] 初始
浅拷贝的地址:0x600002b0a4f0
浅拷贝的成员数量:5
2020-06-22 21:53:08.093857+0800 Teststrong©[3829:284299] 初始
深拷贝的地址:0x60000301cbc0
浅拷贝的成员数量:5
2020-06-22 21:53:08.094072+0800 Teststrong©[3829:284299] 初始
浅拷贝的地址:0x600002b0a4f0
浅拷贝的成员数量:4
2020-06-22 21:53:08.094252+0800 Teststrong©[3829:284299] 初始
深拷贝的地址:0x60000301cbc0
浅拷贝的成员数量:5

我们发现了,修饰不可变数组NSArray其实和NSString类似,如果用strong修饰,也会发生变化,所以NSArray类型的属性,也是必须要用copy进行修饰的。

结论:

1.1 针对不可变数组NSArray作为属性时,应该用copy还是strong?

和NSString一样,用Copy是为了安全,防止 NSMutableArray 赋值给 NSArray 时,前者修改引起后者值变化而使用的。

1.2 针对可变数组NSMutableArray作为属性时,应该用copy还是strong?

先来看下面这段代码

先声明个属性

@property (nonatomic, copy)NSMutableArray * mutableArray;

viewDidLoad中调用

- (void)testMutableArray {
    NSMutableArray * bArray = [NSMutableArray arrayWithArray:@[@"1",@"2",@"3",@"4",@"5"]];
    
    self.mutableArray = bArray;
    
    [self.mutableArray removeAllObjects];
}

运行一下,发现崩了

2020-06-22 22:19:41.360899+0800 深拷贝与浅拷贝strong©[4007:303276] -[__NSArrayI removeAllObjects]: unrecognized selector sent to instance 0x600000631880

发现我们的可变数组变成了不可变数组!

其实上面这串代码

@interface ViewController ()

@property (nonatomic, copy)NSMutableArray * mutableArray;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSMutableArray * bArray = [NSMutableArray arrayWithArray:@[@"1",@"2",@"3",@"4",@"5"]];
    
    self.mutableArray = bArray;
}


等同于

@interface ViewController ()

@property (nonatomic, strong)NSMutableArray * mutableArray;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSMutableArray * bArray = [NSMutableArray arrayWithArray:@[@"1",@"2",@"3",@"4",@"5"]];
    
    self.mutableArray = [bArray copy];
}

所以最后进行移除元素操作的时候,等同于不可变数组NSArray调用移除元素removeAllObjects的方法,所以会报错方法找不到。所以用copy修饰可变数组,等同于该数组是不可变数组,所以是行不通的。用strong则没问题,使用可变数组的目的就是要对同一片内存数据进行操作,所以浅拷贝就行了

结论:

1.2 针对可变数组NSMutableArray作为属性时,应该用copy还是strong?

应该用strong,用copy修饰NSMutableArray,就是相当于对NSMutableArray进行copy处理,得到的是不可变数组。如果继续来调用可变数组专用的API,会Crash并提示方法找不到

三、深拷贝与浅拷贝知识点总结


我们知道OC内存管理的方式采用的是引用计数机制,其他语言如java使用的是垃圾回收机制,对比其他机制,引用计数机制的好处和缺点如下:

优点:一旦没有引用,内存直接释放,处理内存的时间分摊到了平时,不像其他机制需要等待到特定时机,内存管理非常高效。

缺点:维护引用计数会消耗资源,而且会造循环引用问题!循环引用会导致应用程序退出前,这块内存一直存在,始终无法释放!同时会引起应用程序运行期间内存暴涨,更甚者内存飙升的非常厉害,当内存过高会直接被系统杀掉,也就是造成应用程序闪退crash!

ARC下,我们是不可以对对象调用retain,release方法修改内存的引用计数的,我们必须理解MRCretaincopy以及mutableCopy的特点:

retain
始终是浅拷贝,让新对象指针指向原对象,只是原来的内存地址多了一个指针指向,引用计数增加了1(但是系统会在底层进行各种优化,不一定会加,像常量的引用计数就一直保保持-1,不会变动,所以对常量string进行retain也还是不会变)。返回对象是否可变与被复制的对象保持一致。(MRC下的retain等同于ARC下的strong)

copy
对于可变对象为深拷贝(开辟新内存,与原对象指向的不是同一个对象了);
对于不可变对象是浅拷贝(不开辟新内存,只是原内存地址加了一个新的指针指向,引用计数+1)。返回的对象始终是一个不可变的对象。

mutableCopy:
始终是深拷贝(开辟新内存,与原来对象指向的内存空间不是同一处)。返回的对象始终是一个可变对象。

Objective-C中对象的拷贝不光分为深拷贝和浅拷贝,另外还有容器类对象及非容器类对象的差别:

1、对于非容器类对象(如NSStringNSMUtableString类对象)使用浅拷贝:拷贝的就是对象的地址,没有分配新的内存空间,只是原来的那块内存区域多了一个指针指向。也就是说 新对象与原对象都是指向同一个内存地址,那么内容当然一样。

2、对于非容器类对象(NSStringNSMutableString类对象)使用深拷贝:拷贝的是整个对象的内容,通过给新对象分配了一块新的内存空间,然后把原对象对应内存中的值一模一样的在新的内存空间再写一份,所以内容是一样的,但是此时新对象和原对象的内存地址是不同的。

3、对容器类对象(NSArrayNSMutableArray类对象)使用浅拷贝:新的容器类对象也是指向新的内存地址,但是容器内保存的对象没有进行拷贝,指向的内存地址还是和原容器对象内保存的对象指向的内存一样,也就是说你改了其中一个容器对象中的元素对象,那么另一个容器对象中的元素对象也会做相应的修改(因为容器对象内其中包含的元素是同一个内存地址)

对容器类对象(NSArrayNSMutableArray类对象)使用深拷贝:需要对容器类对象中的每一个元素都进行拷贝

四、项目中实际用法的经验总结


  1. 在给类添加NSString类型的属性时,要使用copy关键字修饰; 不然可能导致你保存的这个属性值可能会随动,作为一个合格的iOS开发者一定要避免出现这种低级bug,保证代码健壮度!
  2. 添加NSArray类型的属性时,要使用copy关键字修饰; 不然也可能导致你保存的这个属性值可能会随动;而添加NSMutableArray类型的属性时,要使用strong关键字修饰, 因为用copy修饰NSMutableArray,就是相当于对NSMutableArray进行copy处理,得到的是不可变数组。
  3. 容器类属性(例如NSArrayNSMutableArray类属性)在使用时,需要注意NSArray要用copy关键字修饰,NSMutableArray只能用strong修饰(其他容器类似NSArrayNSDictionaryNSSetcopy,而NSMutableArrayNSMutableDictionaryNSMutableSetstrong

溪浣双鲤的技术摸爬滚打之路

你可能感兴趣的:(iOS进阶专项分析(六)、OC内存管理之深拷贝与浅拷贝)