iOS开发基础76-属性修饰符:从 assign 到 weak 的底层原理与最佳实践
iOS 属性修饰符完全解析:从 assign 到 weak 的底层原理与最佳实践
属性修饰符是 iOS 开发中每天都在写的东西,但很多人只是凭记忆用,并不完全理解每个修饰符背后的原理和适用场景。比如为什么 NSString 用 copy 而不是 strong?为什么 delegate 必须用 weak?atomic 到底是不是线程安全?weak 指针为什么能自动置 nil?本文将系统梳理所有属性修饰符,深入底层原理,并给出最佳实践和常见坑。
一、@property 概述
1. @property 的本质
@property 是 Objective-C 的语法糖,编译器会自动为你生成三样东西:
- 成员变量(下划线开头,如
_name) - getter 方法(如
- (NSString *)name;) - setter 方法(如
- (void)setName:(NSString *)name;)
// 你写的:
@property (nonatomic, copy) NSString *name;
// 编译器自动生成的(大致等价):
{
NSString *_name; // 成员变量
}
- (NSString *)name { // getter
return _name;
}
- (void)setName:(NSString *)name { // setter(copy 修饰符的实现)
if (_name != name) {
[_name release]; // MRC
_name = [name copy];
}
}
属性修饰符就是用来告诉编译器:这个成员变量怎么管理内存、getter/setter 怎么生成、是否线程安全、读写权限如何。
2. 修饰符的分类
| 分类 | 修饰符 | 作用 |
|---|---|---|
| 内存管理 | assign、retain、strong、copy、weak、unsafe_unretained | 控制属性的内存管理方式 |
| 线程安全 | atomic、nonatomic | 控制 getter/setter 是否加锁 |
| 读写权限 | readwrite、readonly | 控制是否生成 setter |
| 方法名 | getter=、setter= | 自定义 getter/setter 方法名 |
| 可空性 | nonnull、nullable、null_unspecified、null_resettable | 标记属性是否可以为 nil(用于 Swift 桥接) |
二、内存管理类修饰符(重点)
1. assign
一句话解释:简单赋值,不改变引用计数,不持有对象。
适用场景:
- 基本数据类型:
NSInteger、CGFloat、BOOL、int、float、double等 - C 数据类型:
CGRect、CGPoint、CGSize等结构体 - 枚举类型
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) BOOL isHidden;
@property (nonatomic, assign) CGRect frame;
setter 实现(大致等价):
- (void)setAge:(NSInteger)age {
_age = age; // 直接值拷贝,不涉及引用计数
}
⚠️ 重要警告:assign 不要用于对象类型!
assign 用于对象时,只是简单的指针赋值,不增加引用计数,对象释放后指针不会自动置 nil,会变成野指针,访问时导致 EXC_BAD_ACCESS 崩溃。
// ❌ 危险!assign 用于对象
@property (nonatomic, assign) NSString *name; // 不要这么写!
// 对象释放后,name 指针变成野指针,访问会崩溃
MRC 时代 assign 可以用于对象(就是 unsafe_unretained 的效果),但 ARC 下对象绝对不要用 assign,用 weak。
2. retain / strong
一句话解释:持有对象,引用计数 +1,确保对象不被释放。
- retain:MRC 时代的写法,ARC 下已被 strong 替代。
- strong:ARC 下的写法,和 retain 效果一样,引用计数 +1。
适用场景:大多数 NSObject 及其子类对象(除了有可变子类的不可变类型用 copy、可能循环引用的用 weak)。
@property (nonatomic, strong) UIView *contentView;
@property (nonatomic, strong) NSArray *dataList;
@property (nonatomic, strong) Person *person;
setter 实现(大致等价):
- (void)setPerson:(Person *)person {
if (_person != person) {
[_person release]; // 释放旧对象(MRC,ARC 自动处理)
_person = [person retain]; // 持有新对象,引用计数 +1(MRC,ARC 自动处理)
}
}
3. copy
一句话解释:赋值时调用对象的 copy 方法,创建一个不可变副本,持有副本而不是原对象。
适用场景:有可变子类的不可变类型,最常见的是:
NSString/NSMutableStringNSArray/NSMutableArrayNSDictionary/NSMutableDictionaryNSSet/NSMutableSetNSData/NSMutableData
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSArray *items;
@property (nonatomic, copy) NSDictionary *config;
setter 实现(大致等价):
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name copy]; // 调用 copy,创建不可变副本
}
}
为什么 NSString 要用 copy?(高频面试题)
因为 NSString 有一个可变子类 NSMutableString。如果用 strong,外部传入一个 NSMutableString,属性持有的是同一个对象,外部修改这个可变字符串时,属性的值也会跟着变,导致不可预期的问题。
// ❌ 用 strong 的问题
@property (nonatomic, strong) NSString *name;
NSMutableString *mutableName = [NSMutableString stringWithString:@"张三"];
self.name = mutableName; // strong 持有同一个对象
NSLog(@"%@", self.name); // 张三
[mutableName appendString:@"李四"]; // 外部修改可变字符串
NSLog(@"%@", self.name); // 张三李四!属性的值被偷偷改了!
// ✅ 用 copy 解决
@property (nonatomic, copy) NSString *name;
NSMutableString *mutableName = [NSMutableString stringWithString:@"张三"];
self.name = mutableName; // copy 创建了一个不可变副本
NSLog(@"%@", self.name); // 张三
[mutableName appendString:@"李四"]; // 外部修改不影响副本
NSLog(@"%@", self.name); // 张三,安全!
copy 和 mutableCopy 的区别:
| 方法 | 对不可变对象(如 NSString) | 对可变对象(如 NSMutableString) |
|---|---|---|
copy |
浅拷贝(引用计数 +1,不创建新对象) | 深拷贝(创建新的不可变对象) |
mutableCopy |
深拷贝(创建新的可变对象) | 深拷贝(创建新的可变对象) |
不可变对象的
copy是浅拷贝(因为已经不可变,不需要真的拷贝),但对外表现为"持有一个不可变对象",语义上是安全的。
⚠️ copy 不要用于可变对象!
// ❌ 错误!copy 修饰可变对象
@property (nonatomic, copy) NSMutableArray *dataList;
self.dataList = [NSMutableArray arrayWithObject:@"1"];
[self.dataList addObject:@"2"]; // 崩溃!copy 返回的是不可变的 NSArray,不能调用 addObject
可变对象应该用 strong:
// ✅ 正确
@property (nonatomic, strong) NSMutableArray *dataList;
4. weak
一句话解释:不持有对象(引用计数不 +1),对象释放后指针自动置为 nil,不会野指针。
适用场景:
- delegate:防止循环引用(最常见)
- target:如
NSTimer的 target(但 iOS 10+ 推荐用 block API) - 子视图引用父视图(视图层级中父视图已经强引用子视图,子视图不需要再强引用父视图)
- Block 内的 weakSelf(防止循环引用)
- xib/storyboard 连线的 UI 控件(视图已经被父视图持有,用 weak 即可)
@property (nonatomic, weak) id<MyDelegate> delegate;
@property (nonatomic, weak) UIView *parentView;
@property (nonatomic, weak) UILabel *titleLabel; // xib 连线
weak 的核心特性:
- 不增加引用计数(不持有对象)
- 对象释放后,weak 指针自动置为 nil(这是 weak 和 assign/unsafe_unretained 的本质区别)
- 给 nil 发消息在 OC 中是安全的(不会崩溃),所以 weak 指针访问是安全的
// weak 示例
@property (nonatomic, weak) NSObject *weakObj;
NSObject *strongObj = [[NSObject alloc] init];
self.weakObj = strongObj; // weak 不持有,引用计数不变
NSLog(@"%@", self.weakObj); // 有值
strongObj = nil; // 强引用消失,对象释放
NSLog(@"%@", self.weakObj); // (null),自动置 nil,不崩溃
weak 的底层原理:详见后文"weak 底层原理深入"章节。
5. unsafe_unretained
一句话解释:和 weak 类似,不持有对象,但对象释放后指针不会自动置 nil,会变成野指针,访问会崩溃。
unsafe_unretained 是 iOS 5 之前没有 weak 时的替代品,现在基本不用了。assign 用于对象时效果和 unsafe_unretained 一样。
@property (nonatomic, unsafe_unretained) id delegate; // 不推荐,用 weak
名字里的 "unsafe" 就是提醒你:不安全,对象释放后会野指针。除非有特殊需求,否则一律用 weak。
6. 内存管理修饰符对比表
| 修饰符 | 引用计数 | 对象释放后指针 | 适用场景 | 线程安全 |
|---|---|---|---|---|
assign |
不变 | 不置 nil(野指针) | 基本数据类型、结构体、枚举 | - |
retain/strong |
+1 | - | 大多数对象 | - |
copy |
+1(副本) | - | 有可变子类的不可变类型(NSString/NSArray/NSDictionary) | - |
weak |
不变 | 自动置 nil | delegate、target、子视图引用父视图、xib 控件 | - |
unsafe_unretained |
不变 | 不置 nil(野指针) | 基本不用(iOS 5 前替代 weak) | - |
三、线程安全类修饰符
1. atomic(默认)
一句话解释:getter/setter 加锁,保证属性的原子性读写,多线程下不会出现读写竞争导致的数据错乱。
atomic 是默认值,不写 nonatomic 就是 atomic。
@property (atomic, strong) NSString *name; // atomic 可以省略
@property (strong) NSString *name; // 等价于上面,默认 atomic
atomic 的底层原理:
atomic 的 setter/getter 内部会加一把锁(旧版用自旋锁 OSSpinLock,新版用互斥锁 pthread_mutex 或 os_unfair_lock),保证同一时刻只有一个线程能访问属性。
// atomic setter 大致等价于:
- (void)setName:(NSString *)name {
@synchronized(self) { // 加锁(实际用的是更轻量的锁,不是 @synchronized)
if (_name != name) {
_name = [name copy];
}
}
}
// atomic getter 大致等价于:
- (NSString *)name {
@synchronized(self) {
return _name;
}
}
⚠️ 重要:atomic 不是绝对线程安全!
atomic 只保证属性的读写是原子的(不会读到一半被写),但不保证整个对象的线程安全。
// atomic 只保证 name 属性的读写是原子的
@property (atomic, strong) NSMutableArray *list;
// 但这样仍然线程不安全:
// 线程 A: [self.list addObject:@"1"];
// 线程 B: [self.list removeAllObjects];
// atomic 只保证 getter 返回的 list 指针是完整的,但 list 内部的操作不是原子的
要保证真正的线程安全,需要自己加锁(@synchronized、NSLock、dispatch_semaphore 等)。
2. nonatomic
一句话解释:不加锁,性能更好,但多线程下可能出现读写竞争。
@property (nonatomic, strong) NSString *name;
为什么 iOS 开发都用 nonatomic?(高频面试题)
- 性能:atomic 加锁有性能开销,虽然不大,但在频繁访问的属性(如 UI 控件、列表数据)上累积起来有影响。
- atomic 不能保证真正的线程安全:如上所述,atomic 只保证读写原子性,不保证对象操作的线程安全,所以该加锁还是要自己加。
- UI 操作都在主线程:iOS 的 UI 操作必须在主线程,大多数属性访问都在主线程,没有多线程竞争,atomic 的锁是浪费。
- 业界惯例:苹果官方示例和绝大多数第三方库都用 nonatomic,已经是事实标准。
结论:iOS 开发中,除非有特殊需求,否则一律用
nonatomic。
3. atomic vs nonatomic 对比表
| 对比项 | atomic(默认) | nonatomic |
|---|---|---|
| 加锁 | 是 | 否 |
| 性能 | 较低(有锁开销) | 较高 |
| 读写原子性 | 保证 | 不保证 |
| 线程安全 | 不保证(只保证读写原子) | 不保证 |
| iOS 常用 | 不常用 | 常用(几乎都用) |
四、读写权限类修饰符
1. readwrite(默认)
一句话解释:同时生成 getter 和 setter,外部可以读写。
readwrite 是默认值,不写就是 readwrite。
@property (nonatomic, readwrite, strong) NSString *name; // readwrite 可省略
@property (nonatomic, strong) NSString *name; // 等价
2. readonly
一句话解释:只生成 getter,不生成 setter,外部只能读不能写。
@property (nonatomic, readonly, strong) NSString *name;
// 外部只能读:
NSString *name = self.name; // ✅
// self.name = @"张三"; // ❌ 编译报错,没有 setter
readonly 的常见用法:对外只读,对内可写
在 .h 头文件中声明为 readonly,在 .m 实现文件的 class extension(类扩展)中重新声明为 readwrite,这样外部不能修改,内部可以修改:
// Person.h
@interface Person : NSObject
@property (nonatomic, readonly, copy) NSString *name; // 对外只读
- (instancetype)initWithName:(NSString *)name;
@end
// Person.m
@interface Person () // 类扩展(class extension)
@property (nonatomic, readwrite, copy) NSString *name; // 对内可写
@end
@implementation Person
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (self) {
_name = [name copy]; // 内部可以直接赋值
}
return self;
}
@end
这是封装的常用技巧,保证属性只能被类内部修改,外部只能读取。
五、方法名类修饰符
1. getter=
一句话解释:自定义 getter 方法名。
最常见的用法是 BOOL 类型属性,用 is 开头的 getter,更符合英文习惯:
@property (nonatomic, assign, getter=isHidden) BOOL hidden;
// 生成的 getter 是 isHidden,不是 hidden
BOOL hidden = [view isHidden]; // ✅
// BOOL hidden = view.hidden; // 点语法也可以,编译器会转成 isHidden
其他常见用法:
@property (nonatomic, assign, getter=isSelected) BOOL selected;
@property (nonatomic, assign, getter=isEnabled) BOOL enabled;
@property (nonatomic, assign, getter=isEditing) BOOL editing;
@property (nonatomic, assign, getter=isActive) BOOL active;
2. setter=
一句话解释:自定义 setter 方法名。
setter= 用得很少,一般不需要自定义 setter 名。某些特殊场景(如 KVO 手动管理、兼容旧代码)可能用到:
@property (nonatomic, strong, setter=setMyName:, getter=myName) NSString *name;
// 生成的 setter 是 setMyName:,getter 是 myName
[self setMyName:@"张三"];
NSString *name = [self myName];
一般情况下不要用
setter=,保持默认的setXxx:命名即可,否则会破坏 KVC/KVO 的命名约定。
六、可空性修饰符(Nullability)
可空性修饰符是 iOS 9 引入的,用于标记属性/参数/返回值是否可以为 nil,主要是为了和 Swift 桥接(Swift 有 Optional 类型,需要知道 OC 的值是否可能为 nil)。
1. nonnull
一句话解释:值不能为 nil。
@property (nonatomic, copy, nonnull) NSString *name; // name 不能为 nil
2. nullable
一句话解释:值可以为 nil。
@property (nonatomic, copy, nullable) NSString *nickname; // nickname 可以为 nil
3. null_unspecified
一句话解释:未指定是否可以为 nil(默认值,桥接到 Swift 后是隐式解包可选类型 IUO)。
不写可空性修饰符时默认就是 null_unspecified,一般不需要显式写。
4. null_resettable
一句话解释:setter 可以传 nil(重置为默认值),但 getter 永远返回非 nil(有默认值)。
最典型的例子是 UIView 的 backgroundColor:
@property (nonatomic, strong, null_resettable) UIColor *backgroundColor;
// 可以 setBackgroundColor:nil(重置为默认颜色),但 backgroundColor 永远返回非 nil
这个修饰符用得很少,了解即可。
5. 可空性的快速声明
如果一个类的所有属性/方法都非 nil,可以在 .h 文件中用 NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END 包裹,默认所有值都是 nonnull,需要可空的单独标记 nullable:
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name; // 默认 nonnull
@property (nonatomic, copy, nullable) NSString *nickname; // 显式标记 nullable
- (instancetype)initWithName:(NSString *)name; // 参数默认 nonnull
@end
NS_ASSUME_NONNULL_END
这是苹果推荐的写法,系统框架的头文件都是这么写的。
七、weak 底层原理深入(重点+高频面试题)
1. weak 为什么能自动置 nil?
weak 指针的自动置 nil 不是魔法,是 Objective-C 运行时(Runtime)维护了一张弱引用表,记录了所有指向某个对象的 weak 指针地址。当对象释放时,Runtime 遍历这张表,把所有指向该对象的 weak 指针都置为 nil。
2. SideTable 和 weak_table
Objective-C Runtime 用 SideTable 来管理对象的引用计数和弱引用表。SideTable 中有一个 weak_table_t 类型的弱引用表:
// 简化的 SideTable 结构
struct SideTable {
spinlock_t slock; // 锁
RefcountMap refcnts; // 引用计数表
weak_table_t weak_table; // 弱引用表
};
// 简化的 weak_table 结构
struct weak_table_t {
weak_entry_t *weak_entries; // 弱引用条目数组(散列表)
size_t num_entries; // 条目数量
// ...
};
// 每个 weak_entry 对应一个对象,记录了所有指向它的 weak 指针
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被引用的对象地址
union {
struct {
weak_referrer_t *referrers; // 指向该对象的 weak 指针地址数组
// ...
};
// ...
};
};
简单来说:
- 全局有很多个
SideTable(按对象地址哈希分配,减少锁竞争)。 - 每个
SideTable里有一张weak_table(弱引用散列表)。 weak_table里的每个weak_entry对应一个被 weak 引用的对象。weak_entry里记录了所有指向该对象的 weak 指针的地址。
3. weak 引用的注册过程
当你写 __weak id weakObj = strongObj; 时,Runtime 会:
- 找到
strongObj对应的SideTable。 - 在
weak_table中查找或创建strongObj对应的weak_entry。 - 把
weakObj这个指针的地址(不是值)添加到weak_entry的 referrers 数组中。
NSObject *strongObj = [[NSObject alloc] init];
__weak NSObject *weakObj = strongObj;
// Runtime 做的事:
// 1. 找到 strongObj 对应的 SideTable
// 2. 在 weak_table 中注册 strongObj 对应的 weak_entry
// 3. 把 &weakObj(weakObj 指针的地址)加入 weak_entry.referrers
4. 对象释放时 weak 指针的清理过程
当对象的引用计数变为 0,开始 dealloc 时,Runtime 会:
- 找到对象对应的
SideTable。 - 从
weak_table中取出对象对应的weak_entry。 - 遍历
weak_entry中的所有 weak 指针地址,把每个指针的值置为 nil。 - 从
weak_table中移除这个weak_entry。
strongObj = nil; // 引用计数变为 0,开始 dealloc
// Runtime 做的事:
// 1. 找到 strongObj 对应的 SideTable
// 2. 取出 weak_entry,遍历所有 weak 指针地址
// 3. 把每个 weak 指针的值置为 nil(weakObj = nil)
// 4. 移除 weak_entry
NSLog(@"%@", weakObj); // (null),已经被 Runtime 自动置 nil
这就是 weak 指针能自动置 nil 的完整原理。
5. weak 的性能开销
因为 weak 需要维护弱引用表,涉及散列表的查找、插入、删除,所以:
- weak 变量的赋值比 strong 慢(需要注册到 weak_table)。
- 对象释放时需要清理 weak 表(有额外开销)。
- 但这些开销在大多数场景下可以忽略,不要因为性能而不用 weak。
注意:局部变量的
__weak也会注册到 weak 表,所以在大量循环中不要频繁创建__weak局部变量,可以在循环外创建一次。
八、copy 的深入理解(重点+高频面试题)
1. 哪些类用 copy,哪些用 strong?
| 类型 | 推荐修饰符 | 原因 |
|---|---|---|
NSString |
copy |
有可变子类 NSMutableString,防止外部修改 |
NSArray |
copy |
有可变子类 NSMutableArray,防止外部修改 |
NSDictionary |
copy |
有可变子类 NSMutableDictionary,防止外部修改 |
NSSet |
copy |
有可变子类 NSMutableSet |
NSData |
copy |
有可变子类 NSMutableData |
NSAttributedString |
copy |
有可变子类 NSMutableAttributedString |
NSMutableString |
strong |
可变对象不能用 copy(copy 返回不可变对象) |
NSMutableArray |
strong |
同上 |
NSMutableDictionary |
strong |
同上 |
| 自定义模型对象 | strong |
一般没有可变子类,用 strong |
UIView 及其子类 |
strong |
视图对象用 strong |
UIViewController |
strong |
控制器对象用 strong |
Block |
copy |
Block 需要 copy 到堆(详见 Block 专题文章) |
NSTimer |
strong |
计时器对象用 strong(但注意循环引用) |
简单记忆法:有可变子类的不可变类型用 copy,其他对象用 strong,Block 用 copy,delegate 用 weak,基本类型用 assign。
2. copy 修饰可变对象的坑
// ❌ 致命错误:copy 修饰可变数组
@property (nonatomic, copy) NSMutableArray *dataList;
self.dataList = [NSMutableArray arrayWithObject:@"1"];
NSLog(@"%@", self.dataList); // ("1")
[self.dataList addObject:@"2"];
// 崩溃!-[__NSArrayI addObject:]: unrecognized selector
// 因为 copy 返回的是不可变的 NSArray,不是 NSMutableArray
正确写法:可变对象用 strong。
// ✅ 正确
@property (nonatomic, strong) NSMutableArray *dataList;
3. 自定义类支持 copy
如果你的自定义类需要支持 copy,需要实现 NSCopying 协议的 copyWithZone: 方法:
@interface Person : NSObject <NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person
- (id)copyWithZone:(NSZone *)zone {
Person *copy = [[Person allocWithZone:zone] init];
copy.name = self.name;
copy.age = self.age;
return copy;
}
@end
这样 Person 对象就可以用 copy 修饰符了(虽然一般自定义类用 strong 即可)。
九、修饰符的组合使用
1. 常见的正确组合
// 字符串
@property (nonatomic, copy) NSString *name;
// 数组
@property (nonatomic, copy) NSArray *items;
// 字典
@property (nonatomic, copy) NSDictionary *config;
// 可变数组
@property (nonatomic, strong) NSMutableArray *dataList;
// 视图
@property (nonatomic, strong) UIView *contentView;
// 基本类型
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, assign) BOOL isHidden;
// delegate
@property (nonatomic, weak) id<MyDelegate> delegate;
// Block
@property (nonatomic, copy) void (^completion)(void);
// 只读属性
@property (nonatomic, readonly, copy) NSString *readonlyName;
// BOOL 自定义 getter
@property (nonatomic, assign, getter=isSelected) BOOL selected;
// xib 连线的控件
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
2. 冲突/错误的组合
| 错误组合 | 原因 | 正确写法 |
|---|---|---|
assign + 对象类型 |
对象释放后野指针 | weak |
copy + 可变对象(NSMutableArray 等) |
copy 返回不可变对象,调用可变方法崩溃 | strong |
weak + 基本数据类型 |
weak 只能用于对象 | assign |
strong + NSString(不推荐) |
可能被外部可变字符串修改 | copy |
atomic + iOS UI 属性 |
性能浪费,且不保证线程安全 | nonatomic |
readonly + 又在 .h 写 setter= |
矛盾,readonly 不生成 setter | 去掉 setter= |
weak + 自己创建的对象 |
创建后立即释放,weak 指针为 nil | strong |
十、常见错误和最佳实践
错误 1:assign 用于对象导致野指针崩溃
// ❌ 错误
@property (nonatomic, assign) NSString *name;
@property (nonatomic, assign) id delegate;
// 对象释放后,指针不置 nil,访问崩溃 EXC_BAD_ACCESS
解决:对象用 strong(需要持有)或 weak(不需要持有,如 delegate)。
错误 2:delegate 用 strong 导致循环引用
// ❌ 错误
@property (nonatomic, strong) id<MyDelegate> delegate;
// self 强引用 delegate,delegate 往往强引用 self,形成循环引用
解决:delegate 一律用 weak。
// ✅ 正确
@property (nonatomic, weak) id<MyDelegate> delegate;
错误 3:NSString 用 strong 被外部修改
// ❌ 不推荐
@property (nonatomic, strong) NSString *name;
// 外部传入 NSMutableString 后修改,会影响属性值
解决:NSString 用 copy。
错误 4:可变对象用 copy 崩溃
// ❌ 错误
@property (nonatomic, copy) NSMutableArray *list;
// copy 返回不可变 NSArray,调用 addObject 崩溃
解决:可变对象用 strong。
错误 5:Block 用 strong 导致栈 Block 问题
// MRC 下错误,ARC 下 strong 和 copy 效果一样但推荐 copy
@property (nonatomic, strong) void (^block)(void);
解决:Block 用 copy(约定俗成,MRC/ARC 都安全)。
错误 6:xib 连线用 strong 还是 weak?
xib/storyboard 连线的控件,视图已经被父视图强引用,控件内部不需要再强引用,用 weak 即可:
// ✅ 推荐
@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UIButton *submitButton;
苹果官方文档和示例都用 weak。用 strong 也不会错(多一层引用),但 weak 更合理。
最佳实践总结表
| 场景 | 推荐修饰符 | 原因 |
|---|---|---|
| 基本数据类型(NSInteger/CGFloat/BOOL/结构体/枚举) | nonatomic, assign |
值类型,不需要引用计数 |
| NSString/NSArray/NSDictionary 等不可变类型 | nonatomic, copy |
防止可变子类修改 |
| NSMutableString/NSMutableArray 等可变类型 | nonatomic, strong |
copy 会返回不可变对象 |
| 自定义模型对象/UIView/UIViewController | nonatomic, strong |
没有可变子类,直接持有 |
| delegate/target | nonatomic, weak |
防止循环引用 |
| Block | nonatomic, copy |
需要 copy 到堆 |
| xib 连线控件 | nonatomic, weak |
已被父视图持有 |
| 对外只读属性 | nonatomic, readonly |
封装,外部不能修改 |
| BOOL 类型 | nonatomic, assign, getter=isXxx |
符合英文命名习惯 |
| 几乎所有属性 | nonatomic |
性能好,atomic 不保证线程安全 |
十一、Swift 版本对照
Swift 的属性修饰符和 OC 有很大不同,因为 Swift 有自己的内存管理和类型系统。
1. 内存管理
// strong(默认)
var name: String = ""
// weak(必须是可选类型,因为对象释放后为 nil)
weak var delegate: MyDelegate?
// unowned(类似 OC 的 unsafe_unretained,不自动置 nil,访问已释放对象会崩溃)
unowned let owner: Person
// 基本数据类型(值类型,不需要内存管理修饰符)
var age: Int = 0
var height: CGFloat = 0.0
var isHidden: Bool = false
2. copy
Swift 中没有 copy 修饰符,因为 Swift 的 String、Array、Dictionary 都是值类型(值语义),赋值时自动拷贝,不会被外部修改:
var name: String = "张三" // 值类型,赋值即拷贝
var items: [String] = [] // 值类型
var config: [String: Any] = [:] // 值类型
如果是 OC 的对象类型(如 NSString、NSArray),在 Swift 中使用时需要注意,建议尽量用 Swift 原生的值类型。
3. 读写权限
// readwrite(默认)
var name: String
// readonly(用 let,常量)
let name: String = "张三"
// 对外只读,对内可写(private set)
public private(set) var name: String
4. 原子性
Swift 没有 atomic/nonatomic,Swift 的属性默认不是原子的。需要线程安全时自己用锁或 DispatchQueue 串行队列。
5. 可空性
Swift 有原生的 Optional 类型:
var name: String // 非 nil(对应 OC 的 nonnull)
var nickname: String? // 可选,可以为 nil(对应 OC 的 nullable)
var height: Double! // 隐式解包可选(对应 OC 的 null_unspecified)
6. 自定义 getter/setter
var isHidden: Bool {
get {
return _isHidden
}
set {
_isHidden = newValue
}
}
// 只写 getter(计算属性,只读)
var isAdult: Bool {
return age >= 18
}
// 属性观察器
var name: String {
willSet {
print("将要设置为 \(newValue)")
}
didSet {
print("已经从 \(oldValue) 变为 \(name)")
}
}
十二、常见问题排查
问题 1:属性修改后不生效 / KVO 不触发
可能原因:
- 直接访问成员变量
_name = xxx,不经过 setter,KVO 不触发。 - 属性是
readonly,没有 setter。 - 自定义了 setter 但没调用 KVO 的
willChangeValueForKey:/didChangeValueForKey:。
解决:
- 修改属性用
self.name = xxx(走 setter),不要直接_name = xxx。 - 如果需要手动触发 KVO,调用
willChangeValueForKey:和didChangeValueForKey:。
问题 2:EXC_BAD_ACCESS 崩溃(野指针)
可能原因:
- 对象属性用了
assign,对象释放后指针不置 nil。 unsafe_unretained修饰的对象被释放。- Block 内用了
__unsafe_unretained的 self。
解决:对象用 strong 或 weak,不要用 assign/unsafe_unretained。
问题 3:循环引用导致内存泄漏
可能原因:
- delegate 用了
strong。 - Block 属性内引用了 self,且没有用 weak。
- NSTimer 的 target 是 self,没有 invalidate。
解决:delegate 用 weak,Block 内用 __weak typeof(self) weakSelf = self;,NSTimer 在页面消失时 invalidate。
问题 4:可变数组属性调用方法崩溃
可能原因:属性用了 copy,copy 返回不可变对象。
解决:可变对象(NSMutableArray/NSMutableString/NSMutableDictionary)用 strong。
问题 5:属性值被外部偷偷修改
可能原因:NSString/NSArray/NSDictionary 用了 strong,外部传入可变对象后修改,属性值跟着变。
解决:有可变子类的不可变类型用 copy。
问题 6:atomic 属性多线程下仍出问题
原因:atomic 只保证读写原子性,不保证对象操作的线程安全。
解决:需要真正线程安全时,自己加锁(@synchronized、NSLock、dispatch_semaphore、串行 DispatchQueue 等)。
十三、总结
- @property 本质:编译器自动生成成员变量、getter、setter,修饰符控制生成方式。
- 内存管理修饰符:
assign:基本数据类型/结构体/枚举,值拷贝,不用于对象。strong/retain:持有对象,引用计数 +1,大多数对象用这个。copy:创建不可变副本,用于 NSString/NSArray/NSDictionary 等有可变子类的不可变类型,以及 Block。weak:不持有,对象释放后自动置 nil,用于 delegate/target/xib 控件,防止循环引用。unsafe_unretained:不持有且不自动置 nil(野指针),基本不用。
- 线程安全:
atomic(默认,加锁,性能差,只保证读写原子性不保证线程安全)、nonatomic(不加锁,性能好,iOS 几乎都用)。 - 读写权限:
readwrite(默认,有 getter 和 setter)、readonly(只有 getter,常配合 .m 类扩展重新声明为 readwrite 实现对外只读对内可写)。 - 方法名:
getter=isXxx(BOOL 类型常用)、setter=(很少用)。 - 可空性:
nonnull/nullable/null_unspecified/null_resettable,用于 Swift 桥接,常用NS_ASSUME_NONNULL_BEGIN/END批量声明。 - weak 底层原理:Runtime 维护 SideTable 中的 weak_table 弱引用散列表,记录所有 weak 指针地址,对象 dealloc 时遍历表把所有 weak 指针置 nil。
- copy 核心:防止可变子类修改属性值;不可变对象的 copy 是浅拷贝(引用计数+1),可变对象的 copy 是深拷贝(创建新不可变对象);可变对象绝对不能用 copy。
- 最佳实践口诀:基本类型 assign,字符串数组字典 copy,可变对象 strong,delegate weak,Block copy,xib 控件 weak,几乎都加 nonatomic,对外只读用 readonly。

浙公网安备 33010602011771号