iOS进阶专项分析(六)、OC内存管理之深拷贝与浅拷贝
先来看两个经典的面试题:
1、属性
NSString
为什么要用copy
修饰?而不是用strong
?
2、NSArray
与NSMutableArray
用copy
修饰还是strong
?
带着这两个问题我们开始本篇的内容:
- 1、案例1:非容器类对象(例如
NSString
)的深拷贝与浅拷贝 - 2、案例2:容器类对象(例如
NSArray
,NSMutableArray
)的深拷贝与浅拷贝 - 3、OC内存管理:深拷贝与浅拷贝知识点
- 4、项目中实际用法的经验总结
一、案例1:非容器类对象(例如NSString)的深拷贝与浅拷贝
测试方法:
用可变字符串
NSMutableString
给这NSString
字符串属性赋值,赋值完成后,故意修改了这个可变字符串,看看strong
、copy
这两种关键字修饰的属性是否变化
新建工程实现如下代码
#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
?
测试方法:
用可变数组
NSMutableArray
给NSArray
容器属性赋值,赋值完成后,故意移除数组中的元素,看看strong
、copy
这两种关键字修饰的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
方法修改内存的引用计数的,我们必须理解MRC
下retain
、copy
以及mutableCopy
的特点:
retain
:
始终是浅拷贝,让新对象指针指向原对象,只是原来的内存地址多了一个指针指向,引用计数增加了1(但是系统会在底层进行各种优化,不一定会加,像常量的引用计数就一直保保持-1,不会变动,所以对常量string
进行retain
也还是不会变)。返回对象是否可变与被复制的对象保持一致。(MRC
下的retain
等同于ARC
下的strong
)
copy
:
对于可变对象为深拷贝(开辟新内存,与原对象指向的不是同一个对象了);
对于不可变对象是浅拷贝(不开辟新内存,只是原内存地址加了一个新的指针指向,引用计数+1)。返回的对象始终是一个不可变的对象。
mutableCopy
:
始终是深拷贝(开辟新内存,与原来对象指向的内存空间不是同一处)。返回的对象始终是一个可变对象。
Objective-C
中对象的拷贝不光分为深拷贝和浅拷贝,另外还有容器类对象及非容器类对象的差别:
1、对于非容器类对象(如
NSString
、NSMUtableString
类对象)使用浅拷贝:拷贝的就是对象的地址,没有分配新的内存空间,只是原来的那块内存区域多了一个指针指向。也就是说 新对象与原对象都是指向同一个内存地址,那么内容当然一样。2、对于非容器类对象(
NSString
、NSMutableString
类对象)使用深拷贝:拷贝的是整个对象的内容,通过给新对象分配了一块新的内存空间,然后把原对象对应内存中的值一模一样的在新的内存空间再写一份,所以内容是一样的,但是此时新对象和原对象的内存地址是不同的。3、对容器类对象(
NSArray
、NSMutableArray
类对象)使用浅拷贝:新的容器类对象也是指向新的内存地址,但是容器内保存的对象没有进行拷贝,指向的内存地址还是和原容器对象内保存的对象指向的内存一样,也就是说你改了其中一个容器对象中的元素对象,那么另一个容器对象中的元素对象也会做相应的修改(因为容器对象内其中包含的元素是同一个内存地址)对容器类对象(
NSArray
、NSMutableArray
类对象)使用深拷贝:需要对容器类对象中的每一个元素都进行拷贝
四、项目中实际用法的经验总结
- 在给类添加
NSString
类型的属性时,要使用copy
关键字修饰; 不然可能导致你保存的这个属性值可能会随动,作为一个合格的iOS开发者一定要避免出现这种低级bug,保证代码健壮度! - 添加
NSArray
类型的属性时,要使用copy
关键字修饰; 不然也可能导致你保存的这个属性值可能会随动;而添加NSMutableArray
类型的属性时,要使用strong
关键字修饰, 因为用copy
修饰NSMutableArray
,就是相当于对NSMutableArray
进行copy
处理,得到的是不可变数组。 - 容器类属性(例如
NSArray
、NSMutableArray
类属性)在使用时,需要注意NSArray
要用copy
关键字修饰,NSMutableArray
只能用strong
修饰(其他容器类似NSArray
、NSDictionary
、NSSet
用copy
,而NSMutableArray
、NSMutableDictionary
、NSMutableSet
用strong
)