iOS-weak和assign区别,copy和strong的区别和应用

weak和assign区别

经常会有面试题问weak和assign的区别,这里介绍一下。
weak和strong是对应的,一个是强引用,一个是弱引用。weak和assign的区别主要是体现在两者修饰OC对象时的差异。上面也介绍过,assign通常用来修饰基本数据类型,如int、float、BOOL等,weak用来修饰OC对象,如UIButton、UIView等。

基本数据类型用weak来修饰

假设声明一个int类型的属性,但是用weak来修饰,会发生什么呢?
@property (nonatomic, weak) int age;
复制代码
Xcode会直接提示错误,错误信息如下:
Property with 'weak' attribute must be of object type
复制代码
也就是说,weak只能用来修饰对象,不能用来修饰基本数据类型,否则会发生编译错误。

对象使用assign来修饰

假设声明一个UIButton类型的属性,但是用assign来修饰,会发生什么呢?
@property (nonatomic, assign) UIButton *assignBtn;
复制代码
编译,没有问题,运行也没有问题。我们再声明一个UIButton,使用weak来修饰,对比一下:
1 @interface ViewController ()
2  
3 @property (nonatomic, assign) UIButton *assignBtn;
4  
5 @property (nonatomic, weak) UIButton *weakButton;
6  
7 @end

 

正常初始化两个button:
 UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100,100,100,100)];
 [btn setTitle:@"Test" forState:UIControlStateNormal];
 btn.backgroundColor = [UIColor lightGrayColor];
 self.assignBtn = btn;
 self.weakButton = btn;
 NSLog(@"这时候打印self.assignBtn,self.weakButton 此时打印两个button,没有区别");
//如果加上如下 btn = nil
btn = nil; NSLog(@"这时候打印self.assignBtn,self.weakButton 就会crash问题");

NSLog(@"self.weakBtn = %@",self.weakButton);

NSLog(@"self.assignBtn = %@",self.assignBtn);
 
释放之后打印self.weakBtn和self.assignBtn
1 NSLog(@"self.weakBtn = %@",self.weakButton);
2 NSLog(@"self.assignBtn = %@",self.assignBtn);
 
运行,执行到self.assignBtn的时候崩溃了,崩溃信息是
EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
复制代码
weak和assign修饰对象时的差别体现出来了。
weak修饰的对象,当对象释放之后,即引用计数为0时,指针变量同时也会置为nil
2018-12-06 16:17:05.774298+0800 TestClock[15863:192570] self.weakBtn = (null)
而向nil发送消息是没有问题的,不会崩溃。
assign修饰的对象,当对象释放之后,即引用计数为0时,指针变量并不会同时置为nil,全局变量就是变为野指针,不知道指向哪,再向该对象发消息,非常容易崩溃。
因此,当属性类型是对象时,不要使用assign,会带来一些风险。

堆和栈

上面说到,属性用assign修饰,当被释放后,容易变为野指针,容易带来崩溃问题,那么,为何基本数据类型可以用assign来修饰呢?这就涉及到堆和栈的问题。
相对来说,堆的空间大,通常是不连续的结构,使用链表结构。使用堆中的空间,需要开发者自己去释放。OC中的对象,如 UIButton 、UILabel ,[[UIButton alloc] init] 出来的,都是分配在堆空间上。
栈的空间小,约1M左右,是一段连续的结构。栈中的空间,开发者不需要管,系统会帮忙处理。iOS开发 中 int、float等变量分配内存时是在栈上。如果栈空间使用完,会发生栈溢出的错误。
由于堆、栈结构的差异,栈和堆分配空间时的寻址方式也是不一样的。因为栈是连续的控件,所以栈在分配空间时,会直接在未使用的空间中分配一段出来,供程序使用;如果剩下的空间不够大,直接栈溢出;堆是不连续的,堆寻找合适空间时,是顺着链表结点来寻找,找到第一块足够大的空间时,分配空间,返回。根据两者的数据结构,可以推断,堆空间上是存在碎片的。
回到问题,为何assign修饰基本数据类型没有野指针的问题?因为这些基本数据类型(变量和值)是分配在栈上,栈上空间的分配和回收都是系统来处理的,因此开发者无需关注,也就不会产生野指针的问题。

栈是线程安全的嘛

扩展一下,栈是线程安全的嘛?回答问题之前,先看一下进程和线程的关系。
进程和线程的关系
线程是进程的一个实体,是CPU调度和分派的基本单位。一个进程可以拥有多个线程。线程本身是不配拥有系统资源的,只拥有很少的,运行中必不可少的资源(如程序计数器、寄存器、栈)。但是线程可以与同属于一个进程的其他线程,共享进程所拥有的资源。一个进程中所有的线程共享该进程的地址空间,但是每个线程有自己独立的栈,iOS系统中,每个线程栈的大小是1M。而堆则不同。堆是进程所独有的,通常一个进程有一个堆,这个堆为本进程中的所有线程所共享
栈的线程安全
其实通过上面的介绍,该问题答案已经很明显了:栈是线程安全的
堆是多个线程所共有的空间,操作系统在对进程进行初始化的时候,会对堆进行分配; 栈是每个线程所独有的,保存线程的运行状态和局部变量。栈在线程开始的时化,每个线程的栈是互相独立的,因此栈是线程安全的。

copy、strong、mutableCopy

属性修饰符中,还有一个经常被问到的面试题是copy和strong。什么时候用copy,为什么?什么时候用strong,为什么?以及mutableCopy又是什么?这一节介绍一下这些内容。

copy和strong

首先看一下copy和strong,copy和strong的区别也是面试中出现频率最高的。之前举得例子中其实已经出现了copy和strong:
1 @property (nonatomic, copy) NSString *sex;
2  
3 @property (nonatomic, strong) NSMutableArray *books;

 

通常情况下,不可变对象属性修饰符使用copy,可变对象属性修饰符使用strong。
可变对象和不可变对象
Objective-C中存在可变对象和不可变对象的概念。像NSArray、NSDictionary、NSString这些都是不可变对象,像NSMutableArray、NSMutableDictionary、NSMutableString这些是可变对象。可变对象和不可变对象的区别是,不可变对象的值一旦确定就不能再修改。下面看个例子来说明。
1 - (void)testNotChange
2 {
3 NSString *str = @"123";
4 NSLog(@"str = %p",str);
5 str = @"234";
6 NSLog(@"after str = %p",str);
7 }

 

复制代码
NSString是不可变对象。虽然在程序中修改了str的值,但是此处的修改实际上是系统重新分配了空间,定义了字符串,然后str重新指向了一个新的地址。这也是为何修改之后地址不一致的原因:
2018-12-06 22:02:41.350812+0800 TestClock[884:17969] str = 0x106ec1290
2018-12-06 22:02:41.350919+0800 TestClock[884:17969] after str = 0x106ec12d0

 

再来看可变对象的例子:
1 - (void)testChangeAble
2 {
3 NSMutableString *mutStr = [NSMutableString stringWithString:@"abc"];
4 NSLog(@"mutStr = %p",mutStr);
5 [mutStr appendString:@"def"];
6 NSLog(@"after mutStr = %p",mutStr);
7 }

 

NSMutableString是可变对象。程序中改变了mutStr的值,且修改前后mutStr的地址一致:
2018-12-06 22:10:08.457179+0800 TestClock[1000:21900] mutStr = 0x600002100540
2018-12-06 22:10:08.457261+0800 TestClock[1000:21900] after mutStr = 0x600002100540
不可变对象用strong
上面说了,可变对象使用strong,不可变对象使用copy。那么,如果不可变对象使用strong来修饰,会有什么问题呢?写代码测试一下:
@property (nonatomic, strong) NSString *strongStr;
复制代码
首先明确一点,既然类型是NSString,那么则代表我们不希望testStr被改变,否则直接使用可变对象NSMutableString就可以了。另外需要提醒的一点是,NSMutableString是NSString的子类,对继承了解的应该都知道,子类是可以用来初始化父类的。
介绍完之后,来看一段代码。
1 - (void)testStrongStr
2 {
3 NSString *tempStr = @"123";
4 NSMutableString *mutString = [NSMutableString stringWithString:tempStr];
5 self.strongStr = mutString; // 子类初始化父类
6 NSLog(@"self str = %p mutStr = %p",self.strongStr,mutString); // 两者指向的地址是一样的
7 [mutString insertString:@"456" atIndex:0];
8 NSLog(@"self str = %@ mutStr = %@",self.strongStr,mutString); // 两者的值都会改变,不可变对象的值被改变
9 }

 

注意:**我们定义的不可变对象strongStr,在开发者无感知的情况下被篡改了。**所谓无感知,是因为开发者没有显示的修改strongStr的值,而是再修改其他变量的值时,strongStr被意外的改变。这显然不是我们想得到的,而且也是危险的。项目中出现类似的bug时,通常都很难定位。这就是不可变对象使用strong修饰所带来的风险。
可变对象用copy
上面说了不可变对象使用strong的问题,那么可变对象使用copy有什么问题呢?还是写代码来验证一下:
@property (nonatomic, copy) NSMutableString *mutString;

 

这里还是强调一下,既然属性类型是可变类型,说明我们期望再程序中能够改变mutString的值,否则直接使用NSString了。
看一下测试代码:
1 - (void)testStrCopy
2 {
3 NSString *str = @"123";
4 self.mutString = [NSMutableString stringWithString:str];
5 NSLog(@"str = %p self.mutString = %p",str,self.mutString); // 两者的地址不一样
6 [self.mutString appendString:@"456"]; // 会崩溃,因为此时self.mutArray是NSString类型,是不可变对象
7 }

 

执行程序后,会崩溃,崩溃原因是:
[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0xed877425eeef9883

 

即 self.mutString没有appendString方法。self.mutString是NSMutableString类型,为何没有appendString方法呢?这就是使用copy造成的。看一下
 
self.mutString = [NSMutableString stringWithString:str];
 
这行代码到底发生了什么。这行代码实际上完成了两件事:
// 首先声明一个临时变量
NSMutableString *tempString = [NSMutableString stringWithString:str];
// 将该临时变量copy,赋值给self.mutString
self.mutString = [tempString copy];

 

注意,通过[tempString copy]得到的self.mutString是一个不可变对象,不可变对象自然没有appendString方法,这也是为何会崩溃的原因。

copy和mutableCopy

另外常用来做对比的是copy和mutableCopy。copy和mutableCopy之间的差异主要和深拷贝和浅拷贝有关,先看一下深拷贝、浅拷贝的概念。
深拷贝、浅拷贝
所谓浅拷贝,在Objective-C中可以理解为引用计数加1,并没有申请新的内存区域,只是另外一个指针指向了该区域。深拷贝正好相反,深拷贝会申请新的内存区域,原内存区域的引用计数不变。看图来说明深拷贝和浅拷贝的区别。
首先A指向一块内存区域,现在设置B = A
现在B和A指向了同一块内存区域,即为浅拷贝。
再来看深考贝
首先A指向一块内存区域,现在设置B = A
A和B指向的不是同一块内存区域,只是这两块内存区域中的内容是一样的,即为深拷贝。
可变对象的copy、mutableCopy
可变对象的copy和mutableCopy都是深拷贝。以可变对象NSMutableString和NSMutableArray为例,测试代码:
 1 - (void)testMutableCopy
 2 {
 3 NSMutableString *str1 = [NSMutableString stringWithString:@"abc"];
 4 NSString *str2 = [str1 copy];
 5 NSMutableString *str3 = [str1 mutableCopy];
 6 NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
 7 NSMutableArray *array1 = [NSMutableArray arrayWithObjects:@"a",@"b", nil];
 8 NSArray *array2 = [array1 copy];
 9 NSMutableArray *array3 = [array1 mutableCopy];
10 NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
11 }

 

输出结果:
2018-12-07 13:01:27.525064+0800 TestClock[9357:143436] str1 = 0x60000086d8f0 str2 = 0xc8c1a5736a50d5fe str3 = 0x60000086d9b0
2018-12-07 13:01:27.525198+0800 TestClock[9357:143436] array1 = 0x600000868000 array2 = 0x60000067e5a0 array3 = 0x600000868030

 

可以看到,只要是可变对象,无论是集合对象,还是非集合对象,copy和mutableCopy都是深拷贝。
不可变对象的copy、mutableCopy
不可变对象的copy是浅拷贝,mutableCopy是深拷贝。以NSString和NSArray为例,测试代码如下:
 1 - (void)testCopy
 2 {
 3 NSString *str1 = @"123";
 4 NSString *str2 = [str1 copy];
 5 NSMutableString *str3 = [str1 mutableCopy];
 6 NSLog(@"str1 = %p str2 = %p str3 = %p",str1,str2,str3);
 7 NSArray *array1 = @[@"1",@"2"];
 8 NSArray *array2 = [array1 copy];
 9 NSMutableArray *array3 = [array1 mutableCopy];
10 NSLog(@"array1 = %p array2 = %p array3 = %p",array1,array2,array3);
11 }

 

输出结果:
2018-12-07 13:06:29.439108+0800 TestClock[9442:147133] str1 = 0x1045612b0 str2 = 0x1045612b0 str3 = 0x6000017e4450
2018-12-07 13:06:29.439236+0800 TestClock[9442:147133] array1 = 0x6000019f5c80 array2 = 0x6000019f5c80 array3 = 0x6000017e1170

 

可以看到,只要是不可变对象,无论是集合对象,还是非集合对象,copy都是浅拷贝,mutableCopy都是深拷贝。

自定义对象如何支持copy方法

项目开发中经常会有自定义对象的需求,那么自定义对象是否可以copy呢?如何支持copy?
自定义对象可以支持copy方法,我们所需要做的是:自定义对象遵守NSCopying协议,且实现copyWithZone方法。NSCopying协议是系统提供的,直接使用即可。
遵守NSCopying协议:
 1 @interface Student : NSObject <NSCopying>
 2 {
 3 NSString *_sex;
 4 }
 5  
 6 @property (atomic, copy) NSString *name;
 7  
 8 @property (nonatomic, copy) NSString *sex;
 9  
10 @property (nonatomic, assign) int age;
11  
12 @end

 

实现CopyWithZone方法:
 1 - (instancetype)initWithName:(NSString *)name age:(int)age sex:(NSString *)sex
 2 {
 3 if(self = [super init]){
 4 self.name = name;
 5 _sex = sex;
 6 self.age = age;
 7 }
 8 return self;
 9 }
10  
11 - (instancetype)copyWithZone:(NSZone *)zone
12 {
13 // 注意,copy的是自己,因此使用自己的属性
14 Student *stu = [[Student allocWithZone:zone] initWithName:self.name age:self.age sex:_sex];
15 return stu;
16 }

 

测试代码:
1 - (void)testStudent
2 {
3 Student *stu1 = [[Student alloc] initWithName:@"Wang" age:18 sex:@"male"];
4 Student *stu2 = [stu1 copy];
5 NSLog(@"stu1 = %p stu2 = %p",stu1,stu2);
6 }

 

输出结果:
stu1 = 0x600003a41e60 stu2 = 0x600003a41fc0
 
这里是一个深拷贝,根据copyWithZone方法的实现,应该很容易明白为何是深拷贝。
除了NSCopying协议和copyWithZone方法,对应的还有NSMutableCopying协议和mutableCopyWithZone方法,实现都是类似的,不做过多介绍。
 
 
 
 
posted @ 2020-08-19 16:04  俊华的博客  阅读(1093)  评论(0编辑  收藏  举报