iOS开发基础76-属性修饰符:从 assign 到 weak 的底层原理与最佳实践

iOS 属性修饰符完全解析:从 assign 到 weak 的底层原理与最佳实践

属性修饰符是 iOS 开发中每天都在写的东西,但很多人只是凭记忆用,并不完全理解每个修饰符背后的原理和适用场景。比如为什么 NSString 用 copy 而不是 strong?为什么 delegate 必须用 weak?atomic 到底是不是线程安全?weak 指针为什么能自动置 nil?本文将系统梳理所有属性修饰符,深入底层原理,并给出最佳实践和常见坑。


一、@property 概述

1. @property 的本质

@property 是 Objective-C 的语法糖,编译器会自动为你生成三样东西:

  1. 成员变量(下划线开头,如 _name)
  2. getter 方法(如 - (NSString *)name;)
  3. 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 / NSMutableString
  • NSArray / NSMutableArray
  • NSDictionary / NSMutableDictionary
  • NSSet / NSMutableSet
  • NSData / 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 的核心特性:

  1. 不增加引用计数(不持有对象)
  2. 对象释放后,weak 指针自动置为 nil(这是 weak 和 assign/unsafe_unretained 的本质区别)
  3. 给 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?(高频面试题)

  1. 性能:atomic 加锁有性能开销,虽然不大,但在频繁访问的属性(如 UI 控件、列表数据)上累积起来有影响。
  2. atomic 不能保证真正的线程安全:如上所述,atomic 只保证读写原子性,不保证对象操作的线程安全,所以该加锁还是要自己加。
  3. UI 操作都在主线程:iOS 的 UI 操作必须在主线程,大多数属性访问都在主线程,没有多线程竞争,atomic 的锁是浪费。
  4. 业界惯例:苹果官方示例和绝大多数第三方库都用 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 会:

  1. 找到 strongObj 对应的 SideTable。
  2. 在 weak_table 中查找或创建 strongObj 对应的 weak_entry。
  3. 把 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 会:

  1. 找到对象对应的 SideTable。
  2. 从 weak_table 中取出对象对应的 weak_entry。
  3. 遍历 weak_entry 中的所有 weak 指针地址,把每个指针的值置为 nil。
  4. 从 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 不触发

可能原因:

  1. 直接访问成员变量 _name = xxx,不经过 setter,KVO 不触发。
  2. 属性是 readonly,没有 setter。
  3. 自定义了 setter 但没调用 KVO 的 willChangeValueForKey: / didChangeValueForKey:。

解决:

  • 修改属性用 self.name = xxx(走 setter),不要直接 _name = xxx。
  • 如果需要手动触发 KVO,调用 willChangeValueForKey: 和 didChangeValueForKey:。

问题 2:EXC_BAD_ACCESS 崩溃(野指针)

可能原因:

  1. 对象属性用了 assign,对象释放后指针不置 nil。
  2. unsafe_unretained 修饰的对象被释放。
  3. Block 内用了 __unsafe_unretained 的 self。

解决:对象用 strong 或 weak,不要用 assign/unsafe_unretained。

问题 3:循环引用导致内存泄漏

可能原因:

  1. delegate 用了 strong。
  2. Block 属性内引用了 self,且没有用 weak。
  3. 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。

posted @ 2016-03-26 14:30  Mr.陳  阅读(364)  评论(0)    收藏  举报