文章目录
- 前言
- 1. 属性关键字的分类
- 2. 内存管理关键字
- 2.1 weak
- 2.2 assgin
- weak和assgin的区别
- 2.3 strong
- 2.4 copy关键字
- copy关键字和strong的区别
- 注意
- 2.5 多种copy模式:copy 和 mutableCopy 对 容器对象 进行操作
- 2.6 问题总结
- 3. 线程安全的关键字 (nonatomic, atomic)
- 3.1 nonatomic关键字
- 3.2 atomic关键字
- 4. 修饰变量的关键字 (const,static,extern)
- 4.1 常量const
- 4.2 常量修饰符(const)和宏定义(define)的区别:
- 4.3 static
- 4.4 常量extern
- 4.4 static与const联合使用
- 4.5 extern与const联合使用
前言
属性关键字尤为重要, 总结复习。
属性关键字就是用来修饰属性的关键字,保证了程序的正常运行。
1. 属性关键字的分类
- 内存管理有关的关键字:
weak
,assgin
,strong
,retain
,copy
; - 线程安全的关键字:
monatomic
,atomic
- 访问权限的关键字:
readonly
,readwrite
。 - 修饰变量的关键字:
const
,static
,extern
。
2. 内存管理关键字
2.1 weak
weak经常用来OC对象类型的数据,修饰的对象在释放之后,指针地址会自动被置nil,这是弱引用的表现
- ⚠️在ARC的环境下,为了避免循环引用,delegate往往也是weak修饰。在MRC下用assgin修饰。当某个对象不在拥有strong类型的指针指向的时候,对象就会被释放,即使还有weak指针类型指向他,weak指针也会被清除。
2.2 assgin
- assgin常用于非指针变量,用于修饰基础数据类型和C的数据类型 & id类型的数据。用于基本数据类型进行复制操作。
- asssgin不会修改引用计数,也可以用来修饰对象一般不建议如此,因为assgin修饰的对象被释放之后指针的地址还存着,成为了一个没有指向的野指针(垂悬指针)。
⚠️:assgin修饰的基本类型都是基本数据类型,基本数据类型分配在栈上的,栈上的变量是系统自动进行管理,不会造成野指针以及:MRC下的delegate往往assgin,此操作是为了deletage和self等自身产生循环引用。
eg:当对象A通过retain持有了B,B的delegate对象是A,如果都是强引用则导致互相持有无法正确的释放,造成循环引用。
weak和assgin的区别
- 修饰的对象不同:weak修饰OC对象类型的数据,assgin修饰的基本数据变量。
- 引用计数:二者都不会增加引用计数。
- 释放后的结果不同:weak修饰的对象释放之后指针自动为nil避免访问野指针crash,assgin修饰的对象释放之后指针仍然存在,成为野指针。
- 修饰delegate:MRC下assgin, ARC下weak,二者都是为了避免循环引用。
2.3 strong
strong是最常用的修饰符,主要用来修饰OC对象类型的数据:(NSNumber,NSString,NSArray、NSDate、NSDictionary、模型类等)。 strong是强引用,在ARC下等于retain,这一点区别于weak。
strong是我们通常所说的指针拷贝(浅拷贝),内存地址保持不变,只是产生了一个新的指针,新指针和引用对象的指针指向同一个内存地址,没有生成新的对象,多了一个指向该对象的指针。
⚠️:由于使用的是一个内存地址,当该内存地址存储的内容发生变更的时候导致属性也跟着变更。
2.4 copy关键字
同样用于修饰OC对象类型的数据,同时在MRC时期用来修饰block,因为MRC时期block要从栈区copy到堆区。现在的ARC系统自动给我们做了这个操作。也就是现在使用strong或者copy修饰block都可以。
copy和strong相同点在于都是属于强引用,引用计数 + 1,但是copy修饰的对象是内存拷贝,在引用的时候会生成一个新的内存地址和指针,和引用对象完全同,也不会因为引用属性的变更而改变。
copy关键字和strong的区别
copy:内存拷贝-深拷贝,内存地址不同,指针地址也不同。
storng: 指针拷贝-浅拷贝,内存地址不变,指针地址不同。
声明两个copy属性,两个strong属性,分别为可变和不可变类型:
@property(nonatomic,strong)NSString * Strstrong;
@property(nonatomic,copy)NSString * Strcopy;
@property(nonatomic,copy)NSMutableString * MutableStrcopy;
@property(nonatomic,strong)NSMutableString * MutableStrstrong;
1. 不可变对象对属性进行赋值,查看strong修饰和copy修饰的区别
// 不可变对象对属性进行赋值,查看strong修饰和copy修饰的区别
- (void)testNormal {
NSString * OriginalStr = @"我已经开始测试了";
//对 不可变对象赋值 无论是 strong 还是 copy 都是原地址不变,内存地址都为(0x10c6d75c0),生成一个新指针指向对象(浅拷贝)
self.Strcopy = OriginalStr;
self.Strstrong = OriginalStr;
self.MutableStrcopy = OriginalStr;
self.MutableStrstrong = OriginalStr;
// 内容
NSLog(@"原字符串=>%@\n normal:copy=>%@=====strong=>%@\nMutable:copy=>%@=====strong=>%@",OriginalStr,_Strcopy,_Strstrong,_MutableStrcopy,_MutableStrstrong);
// 内存地址
NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p",OriginalStr,_Strcopy,_Strstrong,_MutableStrcopy,_MutableStrstrong);
// 指针地址
NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p",&OriginalStr,&_Strcopy,&_Strstrong,&_MutableStrcopy,&_MutableStrstrong);
}
}
由上面可以看出,strong修饰的对象,在引用一个对象的时候,内存地址都是一样的,只有指针地址不同,copy修饰的对象也是如此。
为什么呢?不是说copy修饰的对象是生成一个新的内存地址嘛?这里为什么内存地址还是原来的呢?
因为,用不可变对象对属性进行赋值,无论是strong还是copy,都是一样的,原内存地址不变,生成了新的指针地址。
2. 可变对象对属性进行赋值,查看strong和copy的区别
// 可变对象对属性进行赋值,查看strong和copy的区别
- (void)testMutable {
NSMutableString * OriginalMutableStr = [NSMutableString stringWithFormat:@"我已经开始测试了"];
//对 不可变对象赋值 无论是 strong 还是 copy 都是原地址不变,内存地址都为(0x10c6d75c0),生成一个新指针指向对象(浅拷贝)
self.Strcopy = OriginalMutableStr;
self.Strstrong = OriginalMutableStr;
self.MutableStrcopy = OriginalMutableStr;
self.MutableStrstrong = OriginalMutableStr;
[OriginalMutableStr appendFormat:@"改变了"];
// 内容
NSLog(@"原字符串=>%@\n normal:copy=>%@=====strong=>%@\nMutable:copy=>%@=====strong=>%@",OriginalMutableStr,_Strcopy,_Strstrong,_MutableStrcopy,_MutableStrstrong);
// 内存地址
NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p",OriginalMutableStr,_Strcopy,_Strstrong,_MutableStrcopy,_MutableStrstrong);
// 指针地址
NSLog(@"原字符串=>%p\n normal:copy=>%p=====strong=>%p\nMutable:copy=>%p=====strong=>%p",&OriginalMutableStr,&_Strcopy,&_Strstrong,&_MutableStrcopy,&_MutableStrstrong);
在上面的结果可以看出,strong修饰的属性内存地址依然没有改变,但是copy修饰的属性内存值产生了变化。
由此得出结论:
对可变对象赋值 strong 是原地址不变,引用计数+1(浅拷贝)。 copy是生成一个新的地址和对象生成一个新指针指向新的内存地址(深拷贝)
3. 此时改变OriginalMutableStr的值
结果:
结果:
- 是strong 修饰的属性,跟着进行了改变
- 当改变了原有值的时候,由于OriginalMutableStr是可变类型,是在原有内存地址上进行修改,无论是指针地址和内存地址都没有b改变,只是当前内存地址所存放的数据进行改变。由于 strong 修饰的属性虽然指针地址不同,但是指针是指向原内存地址的,所以会跟着 OriginalMutableStr 的改变而改变。
- ⚠️ 不同于strong,copy修饰的类型不仅指针地址不同,而且指向的内存地址也和OriginalMutableStr 不一样,所以不会跟着 OriginalMutableStr 的改变而改变。
注意
- 使用self.Strcopy 和 _Strcopy 来赋值也是两个不一样的结果,因为后者没有调用 set 方法,而 copy 和 strong 之所以会产生差别就是因为在 set 方法中,copy修饰的属性: 调用了 _Strcopy = [Strcopy copy] 方法。
2.5 多种copy模式:copy 和 mutableCopy 对 容器对象 进行操作
在对容器对象(NSArray)进行copy操作时,分为多种:
- copy:仅仅进行了指针拷贝
- mutableCopy:进行内容拷贝,这里的单层指的是完成了NSArray对象的深copy,而未对其容器内对象进行处理使用(NSArray对象的内存地址不同,但是内部元素的内存地址不变)
2.6 问题总结
- 浅拷贝和深拷贝的区别?
- 浅拷贝只是对 内存地址的复制,两个指针指向同一个地址,增加被拷贝对象的引用计数,没有发生新的内存分配。
深拷贝:目标对象指针和源对象指针,指向两片内容相同的内存空间。
2个特点:不会增加被拷贝对象的引用计数,产生了新内存分配,出现了2块内存。 - 总结区别:
-
- 浅拷贝增加引用计数,不产生新的内存。
-
- 深拷贝不增加引用计数,会新分配内存
- copy关键字影响了对象的可变和不可变属性吗?
- 可变对象(mutable)copy和mutableCopy都是深拷贝
- 不可变对象(immutable)的copy是浅拷贝,mutableCopy是深拷贝
- copy方法返回的都是不可变对象,若被拷贝对象是可变对象,返回的也是不可变对象。
- NSMutableArray用copy修饰会出现什么问题?
出现调用可变方法不可控问题,会导致程序崩溃。给Mutable 被声明为copy修饰的属性赋值, 过程描述如下:
- 如果赋值过来的是NSMutableArray对象,会对可变对象进行copy操作,拷贝结果是不可变的,那么copy后就是NSArray
- 如果赋值过来的是NSArray对象, 会对不可变对象进行copy操作,拷贝结果仍是不可变的,那么copy之后仍是NSArray。
- 所以不论赋值过来的是什么对象,只要对NSMutableArray进行copy操作,返回的对象都是不可变的。
那原来属性声明的是NSMutableArray,可能会调用了add或者remove方法,拷贝后的结果是不可变对象,所以一旦调用这些方法就会程序崩溃(crash)
- 那你说说strong和weak的区别是?
- strong 表示指向并拥有该对象。**其修饰的对象引用计数会加1.**该对象只要引用计数不为0则不会被销毁。当然强制将其置为nil也可以销毁它。
- weak 表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无需手动设置,该对象会自行在内存中销毁。
- 如何理解的atomic的线程安全呢,有没有什么隐患?
- 保证setter和getter存取方法的线程安全(仅仅对setter和getter方法加锁)。
- atomic对一个数组,进行赋值或获取,是可以保证线程安全的。但是如果进行数组进行操作,比如给数据加对象或移除对象,是不在atomic的保证范围。
- weak属性修饰的变量,如何实现在变量没有强引用后自动置为 nil ?
- runtime 维护了一个weak_table_t 弱引用表 ,用于存储指向某一个对象的所有weak指针。weak表其实是一个哈希表,
key是所指对象的地址,value是weak指针的地址的数组。 - 在对象回收的时候,根据对象的地址将所有weak指针地址的数组,遍历数组把其中的数据置为nil
3. 线程安全的关键字 (nonatomic, atomic)
3.1 nonatomic关键字
nonatomic:非原子操作,不加锁,线程执行快,但是多个线程访问同一个属性可能产生crash
3.2 atomic关键字
atomic原子操作:加锁,保证setter和getter存取方法的线程安全(仅仅对setter和getter方法加锁)。因为线程加锁,别的线程访问当前属性的时候会先执行完属性当前的操作。
⚠️注意:atomic只针对属性的 getter/setter 方法进行加锁,所以安全只是针对getter/setter方法来说,并不是整个线程安全,因为一个属性并不只有 setter/getter 方法,例:(如果一个线程正在getter 或者 setter时,有另外一个线程同时对该属性进行release操作,如果release先完成,会造成crash)
4. 修饰变量的关键字 (const,static,extern)
4.1 常量const
常量修饰符,表示不可变,可以用来修饰右边的基本变量和指针变量(放在谁的前面修饰谁(基本数据变量p,指针变量*p))。
- const 类型 * 变量名a:可以改变指针的指向,不能改变指针指向的内容。 const放在 * 号的前面约束参数,表示*a只读。只能修改地址a,不能通过a修改访问的内存空间
int x = 12;
int new_x = 21;
const int *px = &x;
px = &new_x; // 改变指针px的指向,使其指向变量y
- 类型 * const 变量名:可以改变指针指向的内容,不能改变指针的指向。 const放后面约束参数,表示a只读,不能修改a的地址,只能修改a访问的值,不能修改参数的地址
复制代码int y = 12;
int new_y = 21;
int * const py = &y;
(*py) = new_y; // 改变px指向的变量x的值
4.2 常量修饰符(const)和宏定义(define)的区别:
使用宏和常量所占用的内存差别不大,宏定义的是常量,常量都放在常量区,只会生成一份内存。
缺点:
- 编译时刻:宏是预编译(编译之前处理),const是编译阶段 。导致使用宏定义过多的话,随着工程越来越大,编译速度会越来越慢
宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。
优点:
- 宏能定义一些函数,方法。 const不能。
4.3 static
定义所修饰的对象只能在当前文件访问,不能通过extern来引用
默认情况下的全局变量 作用域是整个程序(可以通过extern来引用) 被static修饰后仅限于当前文件来引用 其他文件不能通过extern来引用
- static修饰全局变量:只能在本文件中访问,修改全局变量的作用域,生命周期不会改。避免重复定义全局变量(单例模式)
- static修饰局部变量:
- 有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用的时候该变量已经有值。这时就应该指定该局部变量为静态变量,用关键字 static 进行声明。
- 延长局部变量的生命周期(没有改变变量的作用域,只在当前作用域有用),程序结束才会销毁。
⚠️注意:当在对象A里这么写static int i = 10;
当A销毁掉之后 这个i还存在
当再次alloc init一个A的对象之后 在新对象里 依然可以拿到i = 90
除非杀死程序 再次进入才能得到i = 0。
局部变量只会生成一份内存,只会初始化一次。把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在
static修饰局部变量还可以延长变脸的生命周期。
- (void)test{
// static修饰局部变量1
static int age = 0;
age++;
NSLog(@"%d",age);
}
-(void)test2{
// static修饰局部变量2
static int age = 0;
age++;
NSLog(@"%d",age);
}
[self test];
[self test2];
[self test];
[self test2];
[self test];
[self test2];
打印 1 1 2 2 3 3
由此可见 变量生命周期延长了,作用域没有变
4.4 常量extern
只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量。
查找优先级: 先在当前文件查找有没有全局变量,没有找到,才会去其他文件查找。
#import "JMProxy.h"
@implementation JMProxy
int ageJMProxy = 20;
@end
@implementation TableViewController
- (void)viewDidLoad {
[super viewDidLoad];
extern int ageJMProxy;
NSLog(@"%d",ageJMProxy);
}
@end
⚠️ extern不能用于定义变量。
4.4 static与const联合使用
声明一个静态的全局只读常量。开发中声明的全局变量,有些不希望外界改动,只允许读取。
iOS中staic和const常用使用场景,是用来代替宏,把一个经常使用的字符串常量,定义成静态全局只读变量.
// 开发中经常拿到key修改值,因此用const修饰key,表示key只读,不允许修改。
static NSString * const key = @"name";
// 如果 const修饰 *key1,表示*key1只读,key1还是能改变。
static NSString const *key1 = @"name";
4.5 extern与const联合使用
在多个文件中经常使用的同一个字符串常量,可以使用extern与const组合
extern与const组合:只需要定义一份全局变量,多个文件共享
@interface Person : NSObject
extern NSString * const nameKey = @"name";
@end
#import "ViewController.h"
@interface ViewController ()
@end
NSString * const nameKey; // 必须用xonst才能访问到 extern与const组合组合修饰的全局变量