ObjC 基础 关键字-属性关键字
属性的功能:
- 编译器会自动生成setter和getter方法的声明与实现。
- 如果没有声明成员变量,自动声明一个_属性名的私有变量(默认的成员变量是受保护的)。
属性关键字大体分为几大类:
- 原子操作类:atomic、nonatomic。默认是atomic,保证线程安全。
- 内存管理类:retain、strong、copy、assign、unsafe_unretained、weak。默认是assign。
- 读写权限类:readwrite、readonly。默认是readwrite,可读可写。
- iOS9新增关键字:nonnull、nullable、null_resettable、null_unspecified
原子操作类
atomic
表示原子性(默认属性),用于保证属性的setter和getter方法内部都是原子性操作,相当于在setter和getter方法内部加了线程同步的锁。
- atomic并不能保证使用属性的过程是线程安全的;
- 属性的调用频率非常高,如果使用atomic,那太耗性能,而iOS设备的内存本来就小,在iOS开发中几乎不会使用,在mac开发中才可能会使用。
- atom:原子,代表不可再分割的单位;
- 底层源码实现 (objc-accessors.mm)
// setter
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
// getter
id objc_getProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
return objc_autoreleaseReturnValue(value);
}
nonatomic
表示非原子性,属性的setter和getter方法是非线程安全的,开发中经常使用。
内存管理类
想要持有某个对象,就让它的引用计数+1,进行retain操作,不想再持有某个对象,就让它的引用计数-1,进行release操作。
retain
诞生于MRC,表示持有该对象,会进行retain操作,会对该对象的引用计数加1。
- 只适用于指针类型,一般用于修饰对象类型(继承自NSObject的类)或id类型。
- ARC环境下 ,strong取代了retain。
- 底层实现伪代码:
- (void)setCar:(FSLCar *)car
{
if (_car != car) {
// 原先的car会先释放
[_car release];// 引用计数 -1
// 新的car会后持有
_car = [car retain];//引用计数 +1
}
}
- (FSLCar *)car
{
return _car;
}
strong
诞生于ARC,是强引用(强指针),表示持有该对象,会进行retain操作,会对该对象的引用计数加1。
- 只适用于指针类型,经常用于修饰对象类型(继承自NSObject的类)或id类型。
- strong类似于MRC环境下的retain。
copy
诞生于MRC,是不可变拷贝,表示产生一个引用计数为1的不可变副本对象。
- 只适用于指针类型,适用于那些遵循NSCopying协议的对象类型(继承自NSObject的子类)或id类型,经常用于修饰字符串类型或Block。
- Foundation框架下具备区分可变/不可变的能力的对象且该对象是可变类型,不要用copy修饰,如:可变字符串、可变数组、可变字典等,不要用copy修饰。
- 底层实现伪代码:
- (void)setName:(NSString *)name
{
if (_name != name) {
// 原先的_name会先释放
[_name release];// 引用计数 -1
/*
新的name会后持有
注意:
浅拷贝时,不会产生副本对象,只是指针拷贝,[name copy]相当于[name retain]引用计数+1
深拷贝时,会产生引用计数为1的副本对象,存在另一块内存中,[name copy]相当于alloc,
*
/
_name = [name copy];
}
}
- (NSString *)name
{
return _name;
}
为什么经常用copy来修饰Block属性,而不用strong?
Block是对象,也有isa指针的,有三种类型:__NSGlobalBlock __、__NSStackBlock __、__NSMallocBlock __ 。
不同类型Block调用copy方法,也就是 [xxBlcok copy] 后的结果如下图:
block | 环境 | 原先内存区域 | 调用copy后的内存区域 |
---|---|---|---|
_NSGlobalBlock_ | 没有访问外部auto变量 | 在数据区 | 什么也不做,还在数据区 |
_NSStackBlock_ | 访问外部auto变量 | 在栈区 | 从栈拷贝到堆区 |
_NSMallocBlock_ | 通过copy修饰或调用copy方法 | 在堆区 | 还在堆区,引用计数增加 |
对上图进行解释:
Block块内没有访问外部局部变量时存放在全局区(ARC和MRC下均是),不常用。 Block块内访问外部局部变量,也是开发者常用的方式:
MRC环境下:Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对Block进行copy后,Block存放在堆区,所以在使用Block属性时使用copy修饰。 ARC环境下:Block只要被赋值给强指针,编译器会自动将栈区Block拷贝到堆区的,不用开发者进行copy操作。实际开发中,用copy或strong修饰Block都是可以的,只是习惯了用copy。
为什么经常用copy来修饰NSString属性,而不用strong?
能保证NSString字符串属性肯定是不可变的。iOS开发时对UIKit的使用是很频繁的,将后台数据显示到UI的那一刻,从安全性与维护性考虑,保证数据内容与UI显示的内容是不影响的,数据显示完的那一刻开始,数据内容发生什么改变都不可以影响到已经UI显示的内容,要改可以再重新赋值,再刷新UI。
举个UILabel例子:
// UIKit-UILbel 的属性
@property(nullable, nonatomic,copy) NSString *text;
// 假设这是其中的一条后台可变数据
NSMutableString *dataText = [[NSMutableString alloc] initWithFormat: @"我的内容是可以变化的哦"];
UILabel *lab = [[UILabel alloc] init];
// UILabel的属性text是copy修饰的,代表内容是不可修改的属性
lab.text = dataText;
/*
dataText是可变的,如果lab.text不用copy而用strong修饰,那就只是指针赋值而已,意味着lab.text 与 dataText 指向的是同一块内存,那dataText指向的内容改变了,lab.text的内容也会跟着改变。
上述情况,在开发中是不是不好维护?
当然不好维护啦,谁知道除了lab,如果其他的控件是否有用到了这个dataText,并且做了修改,那lab不就吃屁了,多不安全。
综上所述:直接用copy修饰,简单又安全还易于维护,不用操心外部数据怎么改变,数据拿到手,你就赶快走。
*/
[dataText appendFormat:@"我要修改内容啦"];
为什么修饰属性没有mutableCopy关键字?
首先,关于属性的拷贝关键字只存在cooy,不存在mutableCopy。 其次,严格来讲mutableCopy操作是只给Foundation框架自带的一些类去做事情的,只有他们才有资格产生可变或不可变的权利,具备区分可变或不可变的能力,如:NSString、NSArray、NSDictionary等;其他类是没有的,不具备区分可变或不可变的能力,如:自定义类、UIKit相关类等 最后,属性必须保证是通用的,属性可以定义任意类型的对象或变量,什么东西都有可能,如:可以是自定义的Person或Car对象,可以是某些字符串、某些数值变量等等,所以为了保证通用性,属性不能用mutableCopy修饰。
assign
诞生于MRC,表示单纯地赋值(默认属性),没有引用计数的操作。
- 适用于修饰非指针类型,一般是用于修饰基础类型(如:OC基础类型NSInterger、BOOL等;C数据类型int、float、double、char等)。
- 不能修饰对象,如果用于修饰对象,是非强指针,是不安全的,因不会改变此对象的引用计数,就不会持有该对象,而且当该对象销毁时也不会自动将此属性值(指针)置为nil,会形成野指针,因坏内存访问会导致崩溃。
什么是野指针?
野指针:不是NULL指针,是指向"垃圾"内存(不可用内存)的指针,指向的地址是我们不可知的,是随机的,非常危险的玩意。
使用野指针可能会产生的三种后果:
- 指向不可访问的地址;
- 危害:触发段错误。
- 指向一个可用的,但是没有明确意义的空间;
- 危害:程序可以正确运行,但通常这种情况下,我们就会认为我们的程序是正确的没有问题的,然而事实上就是有问题存在,说不定什么时候这块空间就被使用。
- 指向一个可用的,而且正在被使用的空间;
- 危害:如果我们对这样一个指针进行引用,对其所指向的空间内容进行了修改,但是实际上这块空间正在被使用,那么这个时候该指针指向的内容突然被改变,当然就会对程序的运行产生影响,因为正在被使用的空间内容已经被野指针修改掉。已经不是我们所想要内容了。通常这样的程序都会崩溃,或者数据被损坏。
unsafe_unretained
诞生于MRC,表示不安全的,没有引用计数的操作。
- 适用于修饰非指针类型,一般是用于修饰基础类型。
- 不能修饰对象,如果用于修饰对象,是非强指针,是不安全的,因不会改变此对象的引用计数,就不会持有该对象,而且当该对象销毁时也不会自动将此属性值(指针)置为nil,会形成野指针,因坏内存访问会导致崩溃。
- 和assign类似,都可以修饰基础类型,而修饰对象时又都是不安全的。
- MRC环境下,用来解决循环引用问题。
- ARC环境下,解决循环引用使用weak取代。
weak
诞生于ARC,是弱引用(弱指针),没有引用计数的操作。
- 只适用于指针类型,修饰对象时,是弱引用(弱指针),表示不持有该对象,不会进行retain操作,也就不会增加该对象的引用计数,而且当该对象销毁时会自动将此属性值(指针)置为nil,因会将当前指向该对象的指针指向nil,故不会形成野指针,不会导致崩溃。
- 多用于代理委托(delegate)。
- 一般只称weak为弱引用(弱指针)。
weak 弱指针是存放在什么位置?
弱指针是存放在对象的SideTable 的 weak_table_t weak_table散列表中。 引用计数可以直接存储在isa的指针中,当isa不够存储时,会存储在SideTable的RefcountMap refcnts散列表中。
weak 弱指针的底层实现原理是什么?怎么办到一旦指向的对象被销毁,弱引用(指针)会被置为nil?
对象销毁,底层dealloc究竟做了什么?
当对象销毁时,也就是调用dealloc时,首先,会从SideTable取出对象中的弱引用表(weak_table),把弱引用表中存储的弱引用(弱指针)清空掉,也就是将表中弱指针的都置为nil;其次,如果SideTable中引用计数表(refcnts)还有引用计数存在,也会将引用计数表(refcnts)的数据都擦除掉,也就是引用计数清零。
ARC都帮我们做了什么?
LLVM(编译器) + Runtime,ARC环境是LLVM(编译器)与Runtime运行时系统相互协作的一个环境。 LLVM(编译器)会自动生成release和retain,自动进行内存管理。 而像弱引用这样的存在,是需要Runtime来支持的,在运行时的过程中,当对象销毁时,会动态的对该对象的弱引用进行清空操作。
strong & weak & unsafe_unretained 之间的区别?
strong修饰结果图:
weak修饰结果图:
unsafe_unretained修饰结果图:
虽然上面展示的是 __strong、__weak、__unsafe_unretained修饰局部变量的结果,但是换成strong、weak、unsafe_unretained修饰属性也是一样的特点,主要是关注关键字本身的特点是什么。
读写权限类
readOnly
表示只读,编译器只会自动生成getter方法,不会自动生成setter方法,即只能访问,不能赋值, (但外部还是可以用KVC去赋值)。
readWrite
表示可读可写(默认属性),编译器会自动生成setter和getter方法。