「OC」源码学习——属性关键字
「OC」源码学习——属性关键字
属性的声明
@property (nonatomic, strong) NSString *name;
其实属性就是成员变量和自动编写的存取方法
属性的作用
读写类的关键字
- readonly,只读只生成相应的 getter 方法,以及带下划线的实例变量;
@property ( readonly ) int age;
- readwrite,生成 setter 、getter 方法,以及带下划线的实例变量;
@property ( readwrite ) int age;
原子性操作类关键字
1. atomic
(默认)
-
作用:保证属性访问的原子性
-
实现原理:
- (id)object {@synchronized(self) {return _object;} }
-
特点:
- 线程安全但性能较低
- 不能完全避免线程问题
2. nonatomic
-
作用:非原子访问,提高性能
-
特点:
- 访问速度比 atomic 快 10-20 倍
- 需要开发者自行处理线程安全
-
使用场景:
@property (nonatomic) NSString *name; // 推荐在单线程环境使用
内存管理关键字
strong
(默认)
-
作用:创建强引用,增加对象的引用计数
-
适用类型:对象类型
-
实现原理:
- (void)setObject:(id)newObject {[newObject retain]; // 保留新对象[_object release]; // 释放旧对象_object = newObject; // 赋值 }
-
使用场景:
@property (strong) NSString *name; // 推荐用于对象属性
weak
-
作用:创建弱引用,不增加引用计数
-
适用类型:对象类型
-
特点:
- 对象释放时自动置 nil
- 避免循环引用
-
实现原理:
// 运行时自动注册到 weak_table objc_storeWeak(&_weakRef, object);
-
使用场景:
@property (weak) id delegate; // 代理模式
copy
-
作用:创建对象的副本
-
适用类型:实现了 NSCopying 协议的对象
-
实现原理:
- (void)setName:(NSString *)name {[_name release];_name = [name copy]; // 调用 copy 方法 }
-
使用场景:
@property (copy) NSString *uniqueID; // 保护不可变对象
assign
-
作用:直接赋值,不进行内存管理
-
适用类型:基本数据类型、C 结构体
-
风险:对象类型使用可能造成野指针
-
使用场景:
@property (assign) NSInteger count; // 基本数据类型
unsafe_unretained
-
作用:类似 assign和weak的结合,但用于对象类型,不添加引用计数,比weak性能高些
-
风险:对象释放后不置 nil
-
使用场景:
@property (unsafe_unretained) NSObject *legacyRef; // 兼容旧代码
可空性关键字
nullable
-
作用:表示属性可以为 nil
-
使用场景:
@property (nullable) NSString *optionalName;
nonnull
-
作用:表示属性不应为 nil
-
使用场景:
@property (nonnull) NSString *requiredID;
@property在runtime中的实现
struct property_t {const char *name; // 属性名称const char *attributes; // 属性特性字符串
};
特性字符串格式:"T[类型],[特性1],[特性2],...,V[变量名]"
示例分析:
// 声明
@property (nonatomic, copy) NSString *name;// 对应的 attributes 字符串:
"T@\"NSString\",C,N,V_name"
代码 | 含义 | 说明 |
---|---|---|
T… | 类型 | 后面跟着类型编码 |
C | copy | 使用 copy 语义 |
& | retain/strong | 使用引用计数保留 |
N | nonatomic | 非原子性 |
R | readonly | 只读 |
W | weak | weak 引用 |
设置@property的函数——reallySetProperty
static inline void reallySetProperty(id self, // 目标对象(属性所属的实例)SEL _cmd, // 设置方法的选择器(通常为 setXxx:)id newValue, // 要设置的新值ptrdiff_t offset, // 实例变量在对象内存中的偏移量bool atomic, // 是否原子操作bool copy, // 是否使用 copy 语义bool mutableCopy // 是否使用 mutableCopy 语义
)
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{// 1. 处理特殊情况:offset为0表示要设置的是isa指针(即改变对象的类)if (offset == 0) {object_setClass(self, newValue);return;}id oldValue; // 用于保存旧值,以便后续释放// 2. 计算实例变量在对象内存中的位置(slot指针)id *slot = (id*) ((char*)self + offset);// 3. 根据属性修饰符处理新值(copy/mutableCopy/retain)if (copy) {// 执行copy操作:调用copyWithZone:方法创建不可变副本newValue = [newValue copyWithZone:nil];} else if (mutableCopy) {// 执行mutableCopy操作:调用mutableCopyWithZone:方法创建可变副本newValue = [newValue mutableCopyWithZone:nil];} else {// 如果不是copy/mutableCopy,则进行retain操作(strong修饰)// 先检查新旧值是否相同,避免不必要的操作if (*slot == newValue) return; // 相同则直接返回// 对新值进行retain(增加引用计数)newValue = objc_retain(newValue);}// 4. 根据原子性要求(atomic/nonatomic)执行赋值操作if (!atomic) {// 非原子操作:直接赋值oldValue = *slot; // 保存旧值*slot = newValue; // 将新值赋给实例变量} else {// 原子操作:需要加锁保证线程安全// 4.1 获取与当前slot关联的自旋锁spinlock_t& slotlock = PropertyLocks[slot];// 4.2 加锁slotlock.lock();// 4.3 在锁保护下赋值oldValue = *slot;*slot = newValue;// 4.4 解锁slotlock.unlock();}// 5. 释放旧值(在retain/copy/mutableCopy之后,旧值不再被引用)objc_release(oldValue);
}
Protocol 中的属性
- 在 Protocol 中使用 @property 只会自动生成 setter 和 getter 方法声明,不会自动生成其实现。
- 我们在 Protocol 中使用属性的目的,是希望遵守我协议的对象能实现该属性。
Category 中的属性
- 在 Category 使用 @property 也是只会自动生成 setter 和 getter 方法的声明,不会自动生成其实现。
- 如果我们真的需要给 category 增加属性的实现,需要借助于运行时的两个函数:
objc_setAssociatedObject
、objc_getAssociatedObject
。即关联对象
Atomic对线程安全的作用
看到reallySetProperty
之中,对于Atomic之中的处理
if (!atomic) {oldValue = *slot; *slot = newValue; } else {spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();oldValue = *slot;*slot = newValue;slotlock.unlock();}
其实我们可以看到,这个原子锁就是在调用setter/getter方法时,使用自旋锁进行加锁,即使用atomic修饰的作用在于:单个读/写操作的原子性,保证属性值的读取或写入操作是完整的
但是仍然存在一些问题:
1. 复合操作问题
// 线程1
if (self.atomicCounter > 0) {self.atomicCounter -= 1; // 非原子操作
}// 线程2
self.atomicCounter = 100;
问题分析:
- 虽然每个单独的读写是原子的
- 但
检查值->修改值
的组合操作不是原子的 - 线程2可能在检查后修改值
2. 多属性一致性问题
// 账户转账示例
@interface Account : NSObject
@property (atomic) double balanceA;
@property (atomic) double balanceB;
@end// 线程1:转账操作
account.balanceA -= 100;
account.balanceB += 100;// 线程2:读取总额
double total = account.balanceA + account.balanceB;
问题分析:
- 每个属性单独是原子的
- 但两个属性的组合操作不是原子的
- 可能读到中间状态:balanceA已扣款但balanceB未加款