先前已经在iOS的学习过程中学习了一些属性关键词的简单用法,今天这篇博客来更加深入探讨iOS中的属性关键字以及温习深拷贝与浅拷贝
OC对象(集合类型和非集合类型)有2种拷贝方式,分别为浅拷贝和深拷贝。
深拷贝包括了单层深拷贝和完全深拷贝。具体后面会有讲到
浅拷贝和深拷贝的区别:
因为我们的自定义对象没有copyWithZone:和mutableCopyWithZone:两个方法,需要遵守SCopying和NSMutableCopying协议来实现这两个方法,也已经在上面的链接中具体实现
在这里我们讨论的深拷贝的类型都是对于容器类对象来讲的,因为对于非容器类对象我们并没有单层深拷贝与完全深拷贝的区分
实现方式:
问题(1). 对象的mutableCopy是深拷贝,那为什么更改dataArray2,dataArray3也发生了改变?
dataArray3 = [dataArray2 mutableCopy];
这段代码实现的是单层深拷贝,dataArray3是dataArray2深拷贝得到的数组,但是对于array2数组中的对象array仅仅对其进行了浅拷贝,因此更改array2时array3随之改变
问题(2). 如何解决这个问题?
因为更改的对象非immutable对象,所以使用initWithArray:copyItems:方法可以实现完全深拷贝可以轻松解决这个问题
dataArray3 = [[NSMutableArray alloc] initWithArray:dataArray2 copyItems:YES];
使用initWithArray:copyItems:实现深拷贝仅能产生一层深拷贝,再多就没有办法实现深拷贝了,所以就需要用到解档与归档
dataArray3 = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:dataArray2]];
属性用于封装对象中的数据,属性的本质是 ivar + setter + getter。
可以用 @property 语法来声明属性。@property 会帮我们自动生成属性的 setter 和 getter 方法的声明。
帮我们自动生成 setter 和 getter 方法的实现以及合成实例变量。
告诉编译器不用自动进行 @synthesize,你会在运行时再提供这些方法的实现,无需产生警告,但是它不会影响 @property 生成的 setter 和 getter 方法的声明。
@dynamic ivar;
以前我们需要手动对每个 @property 添加 @synthesize,而在 iOS 6 之后 LLVM 编译器引入了 property autosynthesis,即属性自动合成。换句话说,就是编译器会自动为每个 @property 添加 @synthesize。
但是这样看来似乎@synthesize就没有什么用了,其实不然:
这里说到了协议,那么我们再来讨论一下Category是否能够添加使用@synthesize来合成成员变量以及实现setget方法呢?
答案是并不行,在我们的Category中我们可以声明属性,但不能声明成员变量
因为分类不是类,我们可以通过@property声明属性,但是并不能声明成员变量
Category是用来扩展现有类的功能的,而不是创建全新的类或修改类的内部实现。因此,Category不允许添加实例变量(成员变量)或使用@synthesize来合成成员变量。
以下笔者将一下为什么Category不能添加实例变量和使用@synthesize的原因:
使用 @synthesize 关键字会合成属性的实例变量以及属性的 getter 和 setter 方法
这是我们从代码使用的层面上分析Category不能使用@synthesize的原因,接下来我们从底层结构的层面分析一下原因:
分类是在运行时去把分类中的方法添加到类的方法列表里,类的底层其实是结构体,分类可以添加属性,不能添加成员变量,因为结构体声明后不能加成员变量,而属性是结构体里的一个列表(rw:property_array_t / ro: property_list_t),就是可以加的。
而分类当中不能添加成员变量,只能通过关联对象间接实现分类有成员变量的效果,这个知识点笔者还没能理解,后面学到了加以补充
原子操作:属性是否有原子性可以理解为线程是否安全。
原子性,加同步锁,默认修饰符。 使用atomic会损耗性能,也不一定保证线程安全。如果保证线程安全需要使用其他锁机制。
非原子性,不实用同步锁。 声明属性时基本设置为nonatomic。使用nonatomic能够提高访问性能。
这里笔者还没学到锁的相关知识,等学到了再加以补充
读写权限不写时默认为 readwrite
属性拥有setter方法和getter方法
仅有getter方法
内存管理相关的关键词我们也可以理解为setter相关控制符,setter相关的修饰符表明setter方法应该如何实现
相同点:对对象的引用计数没有影响,即都是弱引用。
不同点:
weak只能修饰OC对象(如UIButton、UIView等)。
assign可以修饰基本数据类型(NSInteger、NSUInteger、CGFloat、NSTimeInterval、int、float、BOOL等)和OC对象(如果用assign修饰OC对象,对象销毁时可能会产生悬垂指针,从而出现crash,不要这么做)。
注意: 使用assign修饰OC对象可能会导致程序crash,所以assign最好只用来修饰基本数据类型。
我们这里详细解释一下保留新值,释放旧值,再设置新值:
setter 方法的实现是 release 旧值,copy 新值
一般用于 block、NSString、NSArray、NSDictionary 等类型。使用 copy 或 strong 修饰 block 其实都一样,用 copy 是为了和 MRC 下保持一致的写法;用于 NSString、NSArray、NSDictionary 是为了保证赋值后是一个不可变对象,以免遭外部修改而导致不可预期的结果。
当你使用copy特性来声明一个属性时,这意味着当设置属性时,属性会复制传入的对象,而不仅仅是保持对传入对象的引用。这通常用于确保属性拥有自己的独立副本,以避免不经意的更改。
下面是一个示例:
@property (nonatomic, copy) NSString *name;
在上面的代码中,name属性使用了copy特性。当你设置name属性时,传入的字符串会被复制,而不是保留对原始字符串的引用。这意味着如果原始字符串在后续被修改,name属性的值不会受到影响。
合成的存取方法会自动处理这个行为。例如,如果你设置self.name = @“Alice”,name属性会将传入的字符串@"Alice"复制为自己的副本,而不仅仅是保存对@"Alice"的引用。
如果属性声明中指定了copy特性,合成方法会使用类的copy方法,这里注意:属性并没有mutableCopy特性。即使是可变的实例变量,也是使用copy特性,正如方法 copyWithZone:的执行结果。所以,按照约定会生成一个对象的不可变副本。
再通俗一点的理解就是用strong修饰属性并对其进行赋值时可以理解为指针拷贝,而用copy修饰时可以理解为内容拷贝
我们来给出代码的例子:
NSMutableString *otherName = [[NSMutableString alloc] initWithString:@"Jack"];
Person *person = [[Person alloc] init];
person.name = otherName;
person.age = 23;
[otherName appendString:@" and Mary"];
NSLog(@"person.name = %@",person.name);
NSLog(@"%p, %p", person.name, otherName);
我们分别打印属性以及属性的地址与原始对象的地址:
可以看到我们的属性是可以被修改的,并且属性的地址与原始对象的地址相同,说明用strong修饰属性时,属性指向原对象的内存地址,同时使该对象引用计数加1
NSMutableString *otherName = [[NSMutableString alloc] initWithString:@"Jack"];
Person *person = [[Person alloc] init];
person.name = otherName;
person.age = 23;
[otherName appendString:@" and Mary"];
NSLog(@"person.name = %@",person.name);
NSLog(@"person.name = %@",otherName);
NSLog(@"%p, %p", person.name, otherName);
可以看到我们属性指向的地址与原始对象的地址不同,并且我们的属性指向的内存空间的数据并未被修改,但是原对象的数据被修改了,说明用copy进行修饰属性对其进行赋值时会创建一块新的内存空间,属性指向新创建的内存空间,本质其实就是内容拷贝(深拷贝)
@property属性用copy修饰不可变对象,用strong修饰可变对象。
在声明一个属性时,尽量使用Foundation框架的数据类型,使代码的数据类型更统一。
基本类型和Foundation数据类型的对应关系如下:
推荐按照下面的格式来定义属性
@property (nonatomic, readwrite, copy) NSString *name;
属性的修饰符应该按照上面的顺序排列:原子操作、读写权限、内存管理。
对于基本数据类型:atomic、readwrite、assign
对于普通的Objective-C对象:atomic、readwrite、strong
读写权限
readwrite:可读可写,默认修饰符。会自动生成getter和setter。
readonly:只读。只会生成getter而不生成setter。
原子操作:属性是否有原子性可以理解为线程是否安全。
atomic:原子性,加同步锁,默认修饰符。
使用atomic会损耗性能,也不一定保证线程安全。如果保证线程安全需要使用其他锁机制。
nonatomic:非原子性,不实用同步锁。
声明属性时基本设置为nonatomic。使用nonatomic能够提高访问性能。
这里设计到了一些锁的知识,还没有学到,后面学到再进行补充
MRC时期:使用assign,这样不会造成循环引用,但是需要手动释放。
ARC时期:最好使用weak,如果使⽤了assign需要⼿动释放。如果没写释放逻辑,当⻚面销毁的时候,很可能出现delegate对象无效,导致程序crash。
@property (nonatomic, copy) NSMutableArray *mutableArray;
如果我们对属性进行增删改等操作时,程序会崩溃,因为当我们对属性进行赋值时,copy复制的是一个不可变的NSArray对象
具体分析:不应该使用copy关键字来修饰可变对象。
copy修饰的属性会在内存里拷贝一份对象,即两个指针指向不同的内存地址。
Foundation框架提供的可变对象类型都已实现了NSCopying协议,所以使用copy方法返回的都是不可变对象。
本题中,用copy关键字修饰了可变数组,那么当对该属性赋值时会得到一个NSArray类型的不可变数组。
因为是NSArray类型,即是不可变的数组类型,所以如果对属性进⾏了可变数组的增删改功能都会导致crash。
所以正确写法如下
@property (nonatomic, strong) NSMutableArray *mutableArray;
如果一定要用copy,我们需要重写setter方法,因为对当属性用copy修饰进行赋值时,会自动调用setter方法执行copyWithZone:方法,返回的是不可变对象,所以我们需要对待吗进行如下修改:
// .h文件
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
// 重写setter⽅法 使_mutableArray变为可变的copy
- (void)setMutableArray:(NSMutableArray *)mutableArray {
_mutableArray = [mutableArray mutableCopy];
}
- (void )viewDidLoad {
[super viewDidLoad];
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1, @2, nil]; self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
NSLog(@"self.mutableArray:%@", self.mutableArray);
}
输出:
self.mutableArray:( 2
)
copy修饰的属性会在内存里拷贝一份对象,即两个指针指向不同的内存地址。
Foundation框架提供的对象类型都已实现了NSCopying协议,所以使用copy方法返回的都是不可变对象。
即使源对象是可变对象(实现属性所用的对象是mutable),copy后的对象也不会随之改变。
确保了对象不会无意间被改动。
strong修饰的属性是对属性进行了强引用,即两个指针会指向同一个内存地址。
如果源对象可变,strong修饰的对象也会随之改变。
属性关键字的细节还有很多很多,后面学到更多的知识会加以补充