@property修饰符
定义一个属性时,nonatomic、copy、strong、assign等被称作是关键字,或者是修饰符。
修饰符种类
- 原子性。原子性有nonatomic、atomic两个值,如果不写nonatomic,那么默认是atomic的。如果属性是atomic的,那么在访问其getter和setter方法之前,会有一些判断,大概是判断是否可以访问等,这里系统使用的是自旋锁。由于使用atomic并不能绝对保证线程安全,且会耗费一些性能,因此通常情况下都使用nonatomic。
- 读写权限。读写权限有两个取值,readwrite和readonly。声明属性时,如果不指定读写权限,那么默认是readwrite的。如果某个属性不想让其他人来写,那么可以设置成readonly。
- 内存管理。内存管理的取值有assign、strong、weak、copy、unsafe_unretained。
- set、get方法名。如果不想使用自动合成所生成的setter、getter方法,声明属性时甚至可以指定方法名。
默认修饰符
声明属性时,如果不显示指定修饰符或者不指定修饰符时,那么默认修饰符分两种
- 基本数据类型:atomic, readwrite, assign
- Objective-C对象类型:atomic, readwrite, strong
atomic是否是线程安全的
声明属性时,通常使用nonatomic修饰符,原因就是因为atomic并不能保证绝对的线程安全。举例来说,假设有一个线程A在不断的读取属性name的值,同时有一个线程B修改了属性name的值,那么即使属性name是atomic,线程A读到的仍旧是修改后的值,可见不是线程安全的。如果想要实现线程安全,需要手动的实现锁。示例:
stu.name = @"aaa";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for(int i = 0 ; i < 1000; ++i){
NSLog(@"stu.name = %@",stu.name);
}
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
stu.name = @"bbb";
});
输出:
2019-01-16 14:42:26.837215+0800 TestClock[15405:175815] stu.name = aaa
2019-01-16 14:42:26.837837+0800 TestClock[15405:175815] stu.name = bbb
如此证实了即使使用了atomic,也不能保证线程安全。
weak和assign区别
weak和strong是对应的,一个是强引用,一个是弱引用。weak和assign的区别主要是体现在两者修饰OC对象时的差异。上面也介绍过,assign通常用来修饰基本数据类型,如int、float、BOOL等,weak用来修饰OC对象,如UIButton、UIView等。
基本数据类型用weak来修饰:假设声明一个int类型的属性,但是用weak来修饰,那么Xcode会直接提示错误,错误信息如下
Property with 'weak' attribute must be of object type
也就是说,weak只能用来修饰对象,不能用来修饰基本数据类型,否则会发生编译错误。
对象使用assign来修饰:假设声明一个UIButton类型的属性,但是用assign来修饰,你会发现编译没有问题,运行也没有问题。我们声明两个UIButton类型属性,一个用assign修饰,一个用weak修饰,创建button,打印比较发现一致没问题。但是释放button,将两个属性置为nil,再打印这两个属性:
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;
btn = nil;
NSLog(@"self.weakBtn = %@",self.weakButton);
NSLog(@"self.assignBtn = %@",self.assignBtn);
运行,执行到self.assignBtn的时候崩溃了,崩溃信息是:
EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
那么weak和assign修饰对象时的差别体现出来了:weak修饰的对象,当对象释放之后,即引用计数为0时,对象会置为nil;assign修饰的对象,当对象释放之后,即引用计数为0时,对象会变为野指针,不知道指向哪,再向该对象发消息,非常容易崩溃。因此,当属性类型是对象时,不要使用assign,会带来一些风险。
堆和栈
上面说到,属性用assign修饰,当被释放后,容易变为野指针,容易带来崩溃问题,那么,为何基本数据类型可以用assign来修饰呢?这就涉及到堆和栈的问题。
相对来说,堆的空间大,通常是不连续的结构,使用链表结构。使用堆中的空间,需要开发者自己去释放。OC中的对象,如 UIButton 、UILabel ,[[UIButton alloc] init] 出来的,都是分配在堆空间上。
栈的空间小,约1M左右,是一段连续的结构。栈中的空间,开发者不需要管,系统会帮忙处理。iOS开发 中 int、float等变量分配内存时是在栈上。如果栈空间使用完,会发生栈溢出的错误。
由于堆、栈结构的差异,栈和堆分配空间时的寻址方式也是不一样的。因为栈是连续的控件,所以栈在分配空间时,会直接在未使用的空间中分配一段出来,供程序使用;如果剩下的空间不够大,直接栈溢出;堆是不连续的,堆寻找合适空间时,是顺着链表结点来寻找,找到第一块足够大的空间时,分配空间,返回。根据两者的数据结构,可以推断,堆空间上是存在碎片的。
那么assign修饰基本数据类型没有野指针的问题?因为这些基本数据类型是分配在栈上,栈上空间的分配和回收都是系统来处理的,因此开发者无需关注,也就不会产生野指针的问题。
栈线程安全
进程和线程的关系:
线程是进程的一个实体,是CPU调度和分派的基本单位。一个进程可以拥有多个线程。线程本身是不配拥有系统资源的,只拥有很少的,运行中必不可少的资源(如程序计数器、寄存器、栈)。但是线程可以与同属于一个进程的其他线程,共享进程所拥有的资源。一个进程中所有的线程共享该进程的地址空间,但是每个线程有自己独立的栈,iOS系统中,每个线程栈的大小是1M。而堆则不同。堆是进程所独有的,通常一个进程有一个堆,这个堆为本进程中的所有线程所共享。
通过上面的介绍,我们可以清楚的了解到:栈是线程安全的。
堆是多个线程所共有的空间,操作系统在对进程进行初始化的时候,会对堆进行分配;
栈是每个线程所独有的,保存线程的运行状态和局部变量。栈在线程开始的时化,每个线程的栈是互相独立的,因此栈是线程安全的。