修饰词的作用
个人觉得是为了对各种对象进行内存管理而产生的。
Objective-C 中的 assign、strong、weak、copy 等修饰词(属性关键字)主要是为了解决 iOS 内存管理(Memory Management)
1、内存的存储位置(堆、栈、全局区、常量区)
1.1、所有 alloc, new, copy, mutableCopy 创建的对象都会存储在堆上:
1 | UIViewController *vc = [[UIViewController alloc] init]; |
1.2、基本数据类型(如 int、float、char、BOOL、double)通常存储在栈上:
1 | void testFunction() { |
但如果基本数据类型是作为对象的属性,且对象在堆上,那么它们也在堆上:
1 | @interface MyObject : NSObject |
1.3、普通 NSString 在常量区:
1 | NSString *str = @"Hello, Objective-C!"; |
堆栈
1 | - (void)testBlock { |
2、内存的管理
Strong: 创建的时候引用计数即为1,后续该对象本其他对象持有的话则引用计数会再增加1,如:
1 | @property (nonatomic, strong) Person *person1; |
delloc/引用计数怎么变为0的?
页面退出(pop、dismiss)的时候,路由栈不再持有页面self。从而页面self的retainCount==0,继而触发self的delloc(请记住是这个因果关系)。
delloc是self的引用计数为0而触发的,而不是self被销毁而触发的或者说delloc的时候self还未被真正销毁,所以你可以在delloc中调用self。那self的真正销毁是self的delloc执行完之后(在其内部调用super delloc后)。
1、self触发delloc的时候,会依次对持有的每个数据(person1、person2)进行relase操作。如果release后person对象的引用计数为0,则person也会被释放。如果person还被其他引用,如UserManager则只是引用计数减去1而已。(题外话:self的释放关注的是self自己被不被其他对象持有,而不是self持有的对象是否全都被释放)
2、因为self执行delloc的时候self并未被真正销毁,所以当页面退出的时候,如果timer的block中又未持有self的时候(一般用weakSelf),你可以在delloc中执行 [timer invalidate] 来将 timer 从Runloop中移除。
1 | - (void)delloc { |
delloc流程代码示例见: https://github.com/dvlproad/CJOptimizeProject/blob/main/CJOptimizeProject/TSDemo_Optimize/Leak/TSDellocViewController.m
为什么weak能避免循环引用?
1 | @interface ViewController : UIViewController |
使用修饰符来解决内存问题,只使用weak和strong不够吗?为什么还要assign和copy等?
非对象类型(如 int、float)根本没有引用计数的概念,它们是值类型,不是引用类型。所以在设计上增加
新的修饰符来处理更合适。而其他的weak、strong、copy等都有引用计数的概念,只是会不会改变引用计数而已。
另外在 Objective-C 中:
• 对象类型(NSObject 及其子类)是存储在堆(Heap)上的指针,需要 ARC 进行引用计数管理。
• 非对象类型(int、float、BOOL 等)是存储在栈(Stack)或者全局数据区,不需要 ARC 进行管理。
而copy修饰符主要是为了功能的考虑,让其拷贝对象,保证内容不变,即让属性存储的是原始值的拷贝,而不是原来的引用。防止可变对象被修改。不然操作的时候可能不小心操作。
| 关键字 | 适用场景 | 是否增加 retainCount | 备注 |
|---|---|---|---|
| assign | 基本数据类型、struct、C 指针 | ❌ 不会 | 适用于 int、float、BOOL、struct 等 |
| strong | 默认对象引用 | ✅ 会 | 适用于普通对象 |
| weak | 避免循环引用 | ❌ 不会 | 对象释放后自动置 nil,适用于 delegate、IBOutlet 等 |
| copy | 不希望对象内容被修改 | ✅ 会 | 适用于 NSString、NSArray、NSDictionary 等 |
🚨 问题:多个线程对不同属性进行读写
✅ atomic 只能保证单个属性的 getter/setter 是线程安全的。⚠️ 但 atomic 并不是真正的多线程安全!
❌ 多个属性的操作(如 firstName 和 lastName 一起更新)不会自动是原子性的,会导致数据不一致。
🛠 解决方案:使用 @synchronized、GCD dispatch_barrier_async 等手段确保完整性。
🔍 线程 A & 线程 B 的并发访问
1 | @interface Person : NSObject |
⚠️ 可能出现的问题:
• 线程 B 读取 firstName 时,线程 A 可能只完成了 firstName = @”Alice”,但 lastName 还未更新,此时打印可能会是:
1 | Alice (null) |
🛠 解决方案:将整个过程加锁,使得整个赋值操作是线程安全的,不会出现部分属性更新的问题。
1 | // 方法1: 通过 @synchronized 使多个属性的操作成为原子性** |
一、Copy
| 修饰符 | 作用 | 返回类型 | |
|---|---|---|---|
| copy | 复制为 不可变对象 | NSString、NSArray、NSDictionary | |
| mutableCopy | 复制为 可变对象 | NSMutableString、NSMutableArray、NSMutableDictionary |
如何让自己的类用
copy修饰符若想令自己所写的对象具有拷贝功能,则需实现
NSCopying协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
具体步骤:
1.需声明该类遵从NSCopying协议
2.实现NSCopying协议的方法,具体区别戳这里
NSCopying协议方法为:
1 | - (id)copyWithZone:(NSZone *)zone { |
1、浅拷贝和深拷贝的区别
浅层复制:只复制指向对象的指针,而不复制引用对象本身。
深层复制:复制引用对象本身。意思就是说我有个A对象,复制一份后得到A_copy对象后,
对于浅复制来说,A和A_copy指向的是同一个内存资源,复制的只不过是是一个指针,对象本身资源还是只有一份,那如果我们对A_copy执行了修改操作,那么发现A引用的对象同样被修改,这其实违背了我们复制拷贝的一个思想。
而对于深复制就好理解了,内存中存在了两份独立对象本身。通俗的话将就是:
浅复制好比你和你的影子,你完蛋,你的影子也完蛋;
深复制好比你和你的克隆人,你完蛋,你的克隆人还活着。
2、 iOS - Copy 与 MutableCopy
iOS - Copy 与 MutableCopy
①copy:因为copy默认返回的是不可变的,所以当我们对一个不可变的字符串进行copy的时候,我们只是拷贝了它的指针(浅拷贝)。当我们对一个可变的字符串进行拷贝的时候,因为类型转变了,我们需对其进行深拷贝。
②mutableCopy:默认返回的是一个可变的对象,适用于可变的对象,例如NSMutableString,NSMutableArray,NSMutableDictionary、etc。 无论对于可变的字符串还是不可变的字符串进行mutableCopy,系统都默认进行深拷贝。
(1)、copy到底是深拷贝还是浅拷贝?
当我们对一个不可变对象(NSString类型)使用copy关键字的时候,系统是不会产生一个新对象,因为原来的对象已经是不能修改的,拷贝出来的对象也是不能修改的,那么既然两个都不可以修改,所以这两个对象永远也不会影响到另一个对象(符合我们说的“修改新(旧)对象,不影响旧(新)对象”原则),系统为了节省内存,所以就不会产生一个新的对象了。那么问题来了,copy到底是深拷贝还是浅拷贝?答:是否是深浅拷贝,是否创建新的对象,是由程序运行的环境所造成的,并不是一概而论。
(2)、这个写法会出什么问题@property (nonatomic, copy) NSMutableArray *mutableArray;
添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。原因:copy修饰符代表赋值结果是要返回一个不可变的值,即是NSArry类。
self.mutableArray = xxx;在copy的修饰下执行的是 self.mutableArray = [xxx copy];进行了浅拷贝,得到的是一个xxx的副本,且该副本是一个不可变的数组。导致在运行的时候,其实你的mutableArray已经是NSArray类了。从而在添加,删除,修改数组内元素的时候,程序会因为找不到对应的方法而崩溃。
3、为什么NSArray用copy修饰、NSMutableArray用strong修饰
把NSArray用strong修饰,会导致NSArray可能会被修改。
把NSMutableArray用copy修饰有时就会crash,因为copy后的数组变成了不可变数组NSArray.当你对不可变数组NSArray进行增删改操作的时候就会crash,
举例如下:
1 | ①NSMutableArray用copy属性造成的crash: |
当修饰可变类型的属性时,如NSMutableArray、NSMutableDictionary、NSMutableString,用strong。
当修饰不可变类型的属性时,如NSArray、NSDictionary、NSString,用copy。
4、模型数组深拷贝
通常需要实现对模型的拷贝都需要先实现NSCopying、 NSMutableCopying协议。注意:如果是数组使用拷贝操作是不会对数组内实现copy协议的对象进行深拷贝的。
参考文章:iOS 模型数组深拷贝
1、最笨的方法就是通过遍历逐个拷贝元素
1 | NSMutableArray *array = [NSMutableArray array]; |
2、也有人使用归档解档实现数组内部元素拷贝
3、这么好用的一个方法现在才发现(推荐)
1 | - (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag |
1 | NSArray <Person *>*deepCopyAry = [[NSArray alloc]initWithArray:dataSourceAry copyItems:YES]; |
4、分析NSString、NSMutableString等类的copy、mutableCopy
在语言文章中,我们已经说明对于语句
NSString *obj = [[NSData alloc] init];obj在编译时是NSString的类型;运行时是NSData类型的对象。由此得出的结论是,不要被编译时的类型蒙蔽,还要看实际运行时的类型。
(1)、分别对NSString、NSMutableString进行copy、mutableCopy生成的类型是什么?
你可以片面理解为
copy是[NSString alloc],所以生成的都是不可变的;mutableCopy是[NSMutableString alloc],所以生成的都是可变的;
所以以下代码的结果,即为代码中的注释一样
1
2
3
4
5
6
7
8
9 >NSString *str1 = @"test001";
>NSMutableString *str2 = [str1 copy];
>//编译时,str2是NSMutableString类型。因为是把str2声明为可变字符串,所以str2即为声明的可变字符串
>//运行时,str2是NSString类型。因为是copy,所以不管str1是可变不可变,str2都是不可变字符串
>NSMutableString *str3 = [str1 mutableCopy];
>//编译时,str3是NSMutableString类型。因为是把str3声明为可变字符串,所以str3即为声明的可变字符串
>//运行时,str3是NSMutableString类型。因为是mutableCopy,所以不管str1是可变不可变,str3都是可变字符串
(2)、分别对NSString、NSMutableString进行copy、mutableCopy操作,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
copy:对[string copy]是浅拷贝,即不会开辟新地址,而对[mutableString copy]是深拷贝,会开辟新地址。
mutableCopy:不管是对[string mutableCopy],还是对[mutableString mutableCopy],都是深拷贝,都会开辟新地址。
1 | - (void)testCopy { |
结论:分别对NSString、NSMutableString进行copy、mutableCopy操作,只有NSString copy是浅拷贝。
(3)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?(附:NSString用copy修饰是为什么)
1 | @property (nonatomic, copy) NSString *copyedStr; |
可以看出,
①将NSString变量赋值给用copy、strong修饰的NSString属性的时候,不管是strong还是copy属性的对象,其指向的地址都是同一个,即为string指向的地址。
1 | @property (nonatomic, copy) NSString *copyedStr; |
②将NSMutableString变量赋值给用copy、strong修饰的NSString属性的时候,
此时copy属性字符串copyedMutableStr
1 | @property (nonatomic, copy) NSString *copyedMutableStr; |
深拷贝了testMutableStr字符串,并让copyedMutableStr对象指向这个字符串(即copyedMutableStr和testMutableStr只是对象值一样,但不是同一个了)。所以此时,我们如果去修改testMutableStr字符串的话,可以看到,我们用@property (nonatomic, copy) NSString *copyedMutableStr;修饰的copyedMutableStr能够不会因为其赋值源testMutableStr的改变而改变,也就保证了安全性。
而strong属性字符串strongMutableStr
1 | @property (nonatomic, strong) NSString *strongMutableStr; |
因为strongMutableStr与testMutableStr是指向同一对象,所以strongMutableStr的值也会跟随着改变;
综上:所以,在声明NSString属性时,到底是选择strong还是copy,可以根据实际情况来定。不过,一般我们将对象声明为NSString时,都不希望它改变(包括不希望赋值后,其他原来的值的改变会改变到它),所以大多数情况下,我们建议用copy,以免因可变字符串的修改导致的一些非预期问题。使用copy来修饰无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本,这样更安全。
(4)、将NSString、NSMutableString变量赋值给用copy、strong修饰的NSMutableString属性的时候,是否会开辟新地址,即是属于深拷贝还是浅拷贝?
1 | @property (nonatomic, copy) NSMutableString *copyedStr; |
2.自定义类如何让它具有copy功能?
遵守NScoping协议,实现copywithzone方法即可.
(5)、自己代码实现copy修饰符,应该怎么写????
4、NSCoding和NSCopy
(1)、NSCoding的作用
很多时候我们都需要将对象序列化,比如将一个对象存入到NSUserDefault 里面去的时候,由于NSUserDefault支持存入的类型有限制,所以很多时候我们需要将NSObject类型的对象转换成NSData再存入进去。
(2)、NSCopy
当你要进行对象拷贝的时候需要遵循NSCopy协议
1 | - (id)copyWithZone:(NSZone *)zone { |
5、@synthesize和@dynamic区别
在声明property属性后,有2种实现选择
- @synthesize
编译器期间,让编译器自动生成getter/setter方法。当有自定义的存或取方法时,自定义会屏蔽自动生成该方法
- @dynamic
告诉编译器,不自动生成getter/setter方法,避免编译期间产生警告,然后由自己实现存取方法,或存取方法在运行时动态创建绑定:主要使用在CoreData的实现NSManagedObject子类时使用,由Core Data框架在程序运行的时动态生成子类属性

