什么是属性
属性是OC语言中的一个机制,我们在OC中用@property
来声明一个属性,其实@property
是一种语法糖,编译器会自动为你的实例变量生成setter
和getter
方法。
所以你在获取这个属性值(NSLog(@"%@",self.name);)和设置属性值(self.name = @"dasheng")的时候其实是调用了getter
和setter
方法获取和设置实例值。
一般这个编译器帮你生成的实例变量就是你的属性名前面加个下划线。当然你也可直接@synthesize
定义这个实例变量名,早期的时候其实这个变量名是需要你手动定义的,编译器并不会帮你定这个变量名,也就是你一定要写@synthesize
,只是现在默认帮你把这一步也省了。只有你自己需要修改一下变量名的时候才需要调用@synthesize
,下面是一个例子:
@interface ViewController ()
@property(nonatomic, copy)NSString *name;
@end
@implementation ViewController
//声明实例变量名
@synthesize name = _realName;
- (void)viewDidLoad {
[super viewDidLoad];
//通过setter方法给实例变量设置值
self.name = @"dasheng";
NSLog(@"%@",self.name);
NSLog(@"%@",_realName);
}
输出值都是:dasheng
当然我们也可以自己定义setter
和getter
方法,比如:
- (NSString *)name{
return @"apple";
}
- (void)setName:(NSString *)name{
_realName = @"banana";
}
那么我们根据self.name
获取跟设置的值都不会变了
NSLog(@"%@",self.name);
NSLog(@"%@",_realName);
self.name = @"dasheng";
NSLog(@"%@",self.name);
NSLog(@"%@",_realName);
//输出
apple
(null)
apple
banana
属性的存储
Runtime下的实现
我们知道类在OC中是objc_class的结构体指针,这个结构体如下所示:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
我们主要关注其中的
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
这几部分。
objc_ivar_list
是该类的成员变量链表,我们上面说过属性其实就是帮你生成一个getter
和setter
方法,最后方法里操作的那个成员变量其实也就是存储在这个成员变量链表里面的。而getter
和setter
也就是存储在objc_method_list
里面。
下面是ivars
和methodLists
存储的指针对应的结构体:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
所以说整个属性的生成过程在runtime中分为以下几步:
- 创建该属性,设置其objc_ivar,通过偏移量和内存占用就可以方便获取。
- 生成其getter和setter。
- 将属性的ivar添加到类的ivar_list中,作为类的成员变量存在。
- 将getter和setter加入类的method_list中。之后可以通过直接调用或者点语法来使用。
- 将属性的描述添加到类的属性描述列表中。
获取成员变量和属性
当然C/C++中也提供了对应的函数可以取到成员变量和属性:
//获取整个成员变量链表
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );
//获取属性链表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
class_copyIvarList函数,返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针(只是class_copyPropertyList)。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。
举个例子:
@interface Person: NSObject{
NSString *name;
}
@property(nonatomic, copy)NSString *age;
@end
unsigned int count = 0;
Ivar *members = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
Ivar ivar = members[i];
const char *memberName = ivar_getName(ivar);
NSLog(@"变量名 = %s",memberName);
}
free(members);
objc_property_t *properties =class_copyPropertyList([Person class], &count);
for (int i = 0; i<count; i++)
{
objc_property_t property = properties[i];
const char* char_f =property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:char_f];
NSLog(@"属性名 = %@",propertyName);
}
free(properties);
//输出
变量名 = name
变量名 = _age
属性名 = age
属性修饰符
我们在声明属性时都会同时声明它的一个修饰符,用来表明它的一个操作行为等。
修饰符 | 作用 |
---|---|
readwrite | 属性可读可写,生成getter+setter,默认属性 |
readonly | 属性只读,只生成getter |
nonatomic | 非原子属性,提高性能但线程不安全 |
atomic | 原子属性,线程安全但可能降低性能 |
assign | 简单赋值,不更改引用计数,用于修饰基础类型的数据(NSInteger)和C语言类型数据(int,float,double,char,bool) |
strong | 强引用,持有对象,引用计数+1 |
weak | 弱引用,不持有对象,不增加引用计数 |
copy | 深拷贝 |
原子性修饰符
原子性操作符只有atomic和nonatomic两种修饰符,系统默认的修饰符是atomic。
nonatomic:非原子属性,线程不安全的,效率高
atomic:原子属性,线程安全的,效率相对低。
atomic属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题.
atomic属性内部的锁称为自旋锁。
自旋锁表示如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。
所以说自旋锁相对于互斥锁是效率比较高,但是很耗资源。大部分情况下我们都会使用nonatomic
,提高效率,减少资源的消耗。
我们给nonatomic
写setter和getter已经很熟悉了,但是又要怎么给atomic
写setter和getter方法呢,要点就是需要加锁,以下是一个对应的代码:
@property (copy, atomic) NSString *name;
- (NSString *)name {
NSString *name;
@synchronized (self) {
name = _name;
}
return name;
}
- (void)setName:(NSString *)name {
@synchronized(self) { //加锁同步
if (![_name isEqualToString:name]) {
_name = name;
}
}
}
assign和weak
weak
与assign
都表示了一种“非持有关系”(nonowning relationship),也称弱引用,在使用时不会增加被引用对象的引用计数。weak
在引用的对象被销毁后会被指向nil。而assign
不会被置nil。
assign
跟 unsafe_unretained
其实差不多,如果用assign
来修饰对象,当assign指向的对象被释放时,assign就成了一个悬空指针,也就是说它会指向一块无效内存,这时你给这个assign修饰的属性发送消息时就会发生崩溃(也有可能不崩溃,取决于你发送消息的时候那块内存还是否有效)。
所以我们一般不用assign修饰对象,因为用assign修饰的指针会成为悬空指针导致错误。
那为什么用assign来修饰基本数据类型呢?因为基本数据类型是被分配到栈上的,栈的内存会由系统自己自动处理,不会造成悬空指针。
总结:assign
用于修饰基础类型的数据(NSInteger)和C语言类型数据(int,float,double,char,bool) ,而weak
只能用于修饰对象。
strong和copy
使用strong和copy时都会使引用对象引用计数+1。但是使用copy修饰的属性在某些情况下赋值的时候会创建对象的副本,也就是深拷贝。
我上面说某些情况下,实际上不一定都会形成深拷贝,下面我会以字符串类型来说明。
关于copy的深入
为什么声明NSString属性要使用copy
我们在声明一个NSString属性时,其内存相关的特性,我们有两种选择:strong和copy。一般我们都会使用copy,但是为什么使用copy你知道吗?
稍微了解一点的人可能就会觉得这不就是深拷贝和浅拷贝嘛,使用copy就是深拷贝,使用strong就是浅拷贝。
然而真的是这样吗?下面我们来写一个例子:
@interface TestStringClass ()
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, copy) NSString *copyedString;
@end
- (void)test {
NSString *originString = [NSString stringWithFormat:@"abc"];
self.strongString = originString;
self.copyedString = originString;
NSLog(@"origin string: %p, %p", originString, &originString);
NSLog(@"strong string: %p, %p", _strongString, &_strongString);
NSLog(@"copy string: %p, %p", _copyedString, &_copyedString);
}
你觉得输出会是什么呢?指针地址肯定是不一样的。普通的想法是认为strong
是浅拷贝,copy
是深拷贝。那么_strongString
和originString
的内存地址是一样的,_copyedString
的内存地址是不一样的。
下面我们来看看实际输出是什么样的:
2015-08-30 14:37:50.573 test[19357:5912951] origin string: 0xa000000006362613, 0x7fff50bfbc48
2015-08-30 14:37:50.574 test[19357:5912951] strong string: 0xa000000006362613, 0x7fe44961d790
2015-08-30 14:37:50.574 test[19357:5912951] copy string: 0xa000000006362613, 0x7fe44961d798
好像跟我们想的不一样?内存地址都是一样的。
下面我们把NSString换成NSMutableString看看,将
NSString *originString = [NSString stringWithFormat:@"abc"];
改为:
NSMutableString *originString = [NSMutableString stringWithFormat:@"abc"];
输出结果:
2015-08-30 14:51:46.119 test[20229:5955951] origin string: 0x7fc27b47ff60, 0x7fff5c14cc48
2015-08-30 14:51:46.120 test[20229:5955951] strong string: 0x7fc27b47ff60, 0x7fc27b6433a0
2015-08-30 14:51:46.120 test[20229:5955951] copy string: 0xa000000006362613, 0x7fc27b6433a8
我们看到originString
跟_strongString
内存是一样的,_copyedString
内存地址是不一样的。
我们现在来想一下原因,当我们使用
NSString
的时候其实是不希望他改变的,那么我们一般情况下是使用copy
,希望他进行深拷贝,那源字符串修改就不会影响到_copyedString
了。但是如果源字符串也是NSString
不可变的呢,那其实就算是浅拷贝也不会有什么影响了。
所以系统可能就在当源字符串为不可变类型时,你属性的内存特性为copy
其实也只进行浅拷贝。当源属性为可变类型时,才进行深拷贝。
所以我们建议在使用
NSString
属性时使用copy
,避免可变字符串的修改导致的一些非预期问题。
上面这句话我们会常常看到,那么很多人问我了,这种情况什么情景下会出现呢?
我这里举一个最简单的例子,有个ViewController
他刚进来的时候有个原价,这是一个原价那当然是不可变咯。
@interface GoodsViewController : UIViewController
@property(nonatomic, strong)NSString *orginPrice;
@end
NSMutableString *_mutablePrice = [NSMutableString stringWithFormat:@"100"];
GoodsViewController *goodsVC = [[GoodsViewController alloc] init];
goodsVC.orginPrice = _mutablePrice;
[self.navigationController pushViewController:goodsVC animated:YES];
我们先假设orginPrice
为strong
:
我们已经进入GoodsViewController
,这个商品的原价就是100,我们不希望他发生改变。这时可能哪里发了个通知,_mutablePrice
加100
变成了200
。
而GoodsViewController
也接收到了通知,准备把orginPrice
加100
变为200
。但是这时候因为是strong
只是浅拷贝,orginPrice
在_mutablePrice
变为200
的那一刻已经改为200
,这时如果你再加100
,其实orginPrice
就变成300
了,这就不是我们想看到的了。
那如果orginPrice
为copy
呢:
这时发生了深拷贝,_mutablePrice
的改变跟orginPrice
没有关系了,所以不用担心产生上面那样的问题。
为什么声明NSMutableString属性不能用copy
因为使用copy
就是深拷贝了一个不可变的NSString
对象。这时如果对这个对象进行可变操作,会产生崩溃。
@property(nonatomic, copy)NSMutableString *copyString;
//这句产生崩溃
[copyString appendString:@"齐滇大圣"];
等价于
NSMutableString *mutableString = [[NSMutableString alloc] initWithFormat:@"我是"];
NSMutableString *copyString = [mutableString copy];
//这句产生崩溃
[mystring appendString:@"齐滇大圣"];
如何让自己的类用 copy 修饰符
若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
NSCopying
协议中的声明的方法只有一个- (id)copyWithZone:(NSZone *)zone
。当我们的类实现了NSCopying
协议,通过类的对象调用copy
方法时,copy
方法就会去调用我们实现的- (id)copyWithZone:(NSZone *)zone
方法,实现拷贝功能。实现代码如下所示:
@implementation PersonModel {
NSString *_nickName;
}
- (id)copyWithZone:(NSZone *)zone{
PersonModel *model = [[[self class] allocWithZone:zone] init];
model.firstName = self.firstName;
model.lastName = self.lastName;
//未公开的成员
model->_nickName = _nickName;
return model;
}
NSMutableCopying
中对于的声明方法为- (id)mutableCopyWithZone:(NSZone *)zone
。跟NSCopying
的区别就是返回的对象是否是可变类型。
下面我们来写个例子看看如何运用:
PersonModel *person1 = [[PersonModel alloc] init];
person1.firstName = @"郑";
PersonModel *person2 = person1;
person2.firstName = @"吴";
NSLog(@"%@",person1.firstName);
输出值:吴
因为这个person1对象根本没有被深拷贝,所有person2改变的时候,person1也被改变了。
我们修改代码如下:
PersonModel实现NSCopying
协议
@interface PersonModel : NSObject<NSCopying>
@property(nonatomic, copy)NSString *firstName;
@end
@implementation PersonModel
- (id)copyWithZone:(NSZone *)zone{
PersonModel *person = [[[self class] allocWithZone:zone] init];
person.firstName = _firstName;
return person;
}
@end
PersonModel *person1 = [[PersonModel alloc] init];
person1.firstName = @"郑";
PersonModel *person2 = [person1 copy];
person2.firstName = @"吴";
NSLog(@"%@",person1.firstName);
输出值:郑