你如果参加过面试,80%的可能性会被问到过,@property的修饰符有哪些,区别又是什么;
既然想深入的了解我们就要知道它的来龙去脉,首先,我们的得知道什么是property。
Property
OC中称为属性,采用此属性,编译器会自动帮我们合成一个变量以及setter、getter方法,比如:
@property NSString * name;
则系统会默认帮我们合成一个 NSString * _name;的成员,同时帮我们为此成员变量写好了setter、getter方法,因此,property的本质就是:
property = ivar + setter + getter
我们再看一下runtime.c的源码里关于类的结构定义:
struct objc_class {
Class isa; // 指向metaclass
Class super_class ; // 指向其父类
const char *name ; // 类名
long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址
struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
struct objc_protocol_list *protocols; // 存储该类遵守的协议
}
我们重点看一下ivars 和 methodLists,他们就是保存成员和方法的结构体,property其实就是往对应的list里添加了一个变量或一对方法罢了。
上面我们提到是默认添加,但是我们总是面对不同的软件需求或则特立独行的风格,比如,我不想让它的变量为_name,可以么?我不想让它的设置方法为 - (void)setName:(NSString *)name;可以么?或则我想让它只读?
这时候就引出了一部分property相关的修饰符了,通过这些修饰符,我们告诉编译器,怎么帮我们合成成员和方法,主要的有:
内存相关
- strong
- copy
- weak
- retain
- assign
- unsafe_unretained
线程安全相关
- atomic
- nonatomic
读写权限
- readonly
- readwrite
存取方法样式
- setter=
getter=
下面我们分别讲解一下:
strong、weak、retain、assign
strong的英文含义为强悍,强壮,这里是强引用的意思,它和weak一样,是ARC里引进的,它主要的作用是让属性的引用计数加1,可以认为retain≈strong;而weak却只会引用,并不会让引用计数加1,如果变对象释放,再次调用该变量对象,就会出现崩溃;当对象dealloc时候,weak修饰的变量对象指针自动赋值为nil;assign与weak类似,只是assign不会在对象dealloc时候自动将对应的属性设置为nil;
以上都是理论:但是具体是怎么实现的呢?
我们用NSString * name 来解释(ARC下):
retain、strong:
- (void)setName:(NSString *) name{
_name = name;
}
上面的_name 对象其实修饰符默认为__strong,假设原来_name有值,那么原来的值指向的对象引用计数减一,新赋值的name对象计数加一;
@interface Test : NSObject
@property (nonatomic,strong) NSString *name;
@end
@implementation Test
@end
等效于
@interface Test : NSObject
{
__strong NSString * _name;
}
@end
@implementation Test
-(void)setName:(NSString *)name
{
_name = name;
}
//getter忽略
@end
我们开始说过property会合成一个ivar,这个ivar前面的修饰符,就是编译器通过@property括号里的内存修饰符而来。
再延生至MAC下,strong应该就变成:
-(void)setName:(NSString *)name
{
if(_name != name){
[_name release];
_name = [name retain];
}
}
或
-(void)setName:(NSString *)name
{
[name retain];
[_name release];
_name = name;
}
assign实现便是:
-(void)setName:(NSString *)name
{
_name = name;
}
那weak的实现是怎么样的呢?因为我们知道,他会在持有改属性的对象dealloc的时候自动将属性指针设置为nil,其实这个很好理解,因为每个实例的Class,Ivar list里的ivar,包含了成员变量的类型、名称,这个类型通过类型编码,转换成了一个char *的字符串表示,如上面的name会被编码为:
T@"NSString",&,N,V_name
那么,当遍历属性列时候,遇到weak修饰的对象,就设置为nil即可。
copy
这里我们单独将copy拿出来,这是为什么呢?因为它比较特别,特别在它使用的对象不同,结果也不同;从它的英文含义里,我们知道,它的意思是拷贝的意思,那拷贝的是指针还是指针指向的内容呢,如果你了解C++,这里就类似别名、引用拷贝,
@property (copy) NSString * name;
@property (copy) NSArray * fans;
@property (copy) NSMutableArray * books;
@implement
NSString *theName = @"iOS2班";
self.name = theName;
NSLog(@"%p %p",_name,theName);
NSArray * fans = @[@"老谢"];
self.fans=fans;
NSLog(@"%p %p",_fans,fans);
NSMutableArray * books =[@[ @"iOS开发进阶"] mutableCopy];
self.books = books;
NSLog(@"%p %p",_books,books);
@end
那上面这段代码,如果放在运行中,有什么问题呢?
2015-11-18 22:36:51.061 Demo[11559:2656983] 0x105c381a8 0x105c381a8
2015-11-18 22:36:51.063 Demo[11559:2656983] 0x7f8d29c28860 0x7f8d29c28860
2015-11-18 22:36:53.748 Demo[11559:2656983] 0x7f8d29c284b0 0x7f8d29c20b20
很明显,前两个不可变的对象,copy后,地址并未改变,而对于可变的对象,产生了一个新的数组对象,而且这个数组里面包含的对象并未改变。
我们在最后再加上这行代码:
NSLog(@"%@ %@",[_books class],[books class]);
运行结果为:
Demo[11609:2659420] __NSArrayI __NSArrayM
这个说明其实_books是一个不可变数组。
但是,我们明明在property中声明的对象为NSMutableArray * books;
这里完全是因为iOS系统的动态运行时,它并没有在编译的时候就确定你的对象类型,所以如果,你在代码后面执行譬如增删_books的方法,那肯定是要crash的。
刚才我们测试的都是iOS系统的类,假设我们用自定义的对象使用copy会如何呢?
ViewModel *model= [ViewModel new];
self.model=model;
这时候你一定会遇上你非常熟悉的朋友:
[ViewModel copyWithZone:]: unrecognized selector sent to instance…….
这是说我们没有实现copyWithZone方法啊,这是什么意思?
这个方法是NSCopying协议里的方法,也就是如果我们自定义的类想用copy修饰,那必须实现这个协议。
因此,我们总结下:
1、对于系统的不可变对象,拷贝的只是指针,引用计数加1;
2、对于系统的可变对象,生成一个新的指针,指针指向的内容如果是容器,那容器里的东西不变,如果是可变字符串,那就是一个字符相等的地址不同的字符串,而且新的指针都是指向不可变对象,内容的应用计数加1(字符串是生成一个新的字符串,新的计数为1,但是那种常量字符串不存在引用计数的概念,请区别)。
3、对于自定义对象,那你想怎么实现就怎么实现,怎么计数就怎么计数了,完全取决于你的实现。
线程安全相关
atomic、nonatomic:
原子性和非原子性,其实这是一个加锁和不加锁的区别,加锁消耗性能,不加锁的话如果在多线程中,就有可能导致存取数据有差错,比如:
当我在A线程中写该属性值时候,在还没写完全,线程切换到了B,B线程进行了读该属性,那么读取的属性肯定也是不完全的。
那么这时候如果加上锁的话,在A没写完的情况下,B线程就会等待,知道A写完解锁,B才能进入读。
从上面我们细想一下,如果上面的atomic所谓的线程安全其实并不安全,因为它只针对读写的方法,对于整体来说,它并不安全:
比如ABCD,多个线程都进行self.name= xxx;的赋值,然后显示到label中,这时候,就可能很随机了,因为线程执行时间是不确定的。另外,如果这个属性是容器的话,多个线程对这个容器进行读写,容器里的对象并不线程安全,安全的也只是属性的设置。
//太晚了,先睡觉吧。。。