前言:为了更深入的了解这些修饰符,特意写了个小测试,并展开深入的探究。如果文章有哪些地方有误,还请多多指点~
目录
1、初探copy,assign,strong,weak特性
2、重写属性的setter方法,探究setter本质
3、直接对成员变量赋值,探究原理
我们先来做一个实验。 分别创建四个属性
@property (nonatomic, strong) NSMutableArray *array_strong;
@property (nonatomic, copy) NSMutableArray *array_copy;
@property (nonatomic, assign) NSMutableArray *array_assign;
@property (nonatomic, weak) NSMutableArray *array_weak;
我们来看看执行如下操作会是什么结果
1、用系统的setter
方法,窥探这些修饰符
//res1
self.array_strong = [NSMutableArray array];
[self.array_strong addObject:@"0"];
//res2
self.array_copy = [NSMutableArray array];
[self.array_copy addObject:@"0"];
//res3
self.array_assign = [NSMutableArray array];
[self.array_assign addObject:@"0"];
//res4
self.array_weak = [NSMutableArray array];
[self.array_weak addObject:@"0"];
结果:
res1:self.array_strong
还是可变数组,其值为[@"0"]
。
res2:在[self.array_copy addObject:@"0"]
这一行崩溃,原因:-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x6000008a00b0
。此时self.array_copy
已经变为不可变数组了。
res3:在[self.array_assign addObject:@"0"]
这一行崩溃,原因EXC_BAD_ACCESS (code=1, address=0x60702efd550)
出现野指针。
res4:当在[self.array_weak addObject:@"0"]
这一行打印self.array_weak
,其值为nil
。
总结:
1、strong
修饰的对象引用计数器+1。
2、copy
我们都知道,
[不可变对象 copy]-->浅拷贝,得出不可变对象
[可变对象 copy]-->深拷贝,得出不可变对象
所以使用copy
属性时,不管你给我传的是可变对象还是不可变对象,我本身持有的都是不可变副本。引用计数器+1
3、assign
主要用于修饰基础数据类型(例如NSInteger
)和C数据类型(int
,float
,double
,char
等),也可以修饰对象。但当修饰对象时,对象释放后会出现野指针。引用计数器不+1。
4、weak
只能修饰对象,主要用于避免循环引用。引用计数器不+1。和assign
不同的是对象释放后会自动将指针置为nil
。
我们继续往下走,来验证这些特性。
2、重写setter
方法,探究setter
本质
那我们不用系统自动生成的setter
方法,用以下方式重写setter
方法又会如何?
- (void)setArray:(NSMutableArray *)array {
_array = array;
}
结果:我们发现,只有res2中,self.array_copy
从不可变数组变成了可变数组,其值为[@"0"]
,其他情况结果不变。
我们来探究一下原因,为什么重写了以copy
修饰的属性的setter
方法后,就不能保证self.array_copy
的不可变特性? 以下为系统生成的 setter
方法
static void _I_ViewController_setArray_copy_(ViewController * self, SEL _cmd, NSMutableString *array_copy) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _array_copy), (id)array_copy, 0, 1); }
static void _I_ViewController_setArray_assign_(ViewController * self, SEL _cmd, NSMutableString *array_assign) { (*(NSMutableString **)((char *)self + OBJC_IVAR_$_ViewController$_array_assign)) = array_assign; }
static void _I_ViewController_setArray_strong_(ViewController * self, SEL _cmd, NSMutableString *array_strong) { (*(NSMutableString **)((char *)self + OBJC_IVAR_$_ViewController$_array_strong)) = array_strong; }
以上代码我们可以看出,除了copy
外,其他修饰符属性的setter
方法内部都是直接给成员变量赋值的(OBJC_IVAR_$_ViewController$_array_strong
为偏移量,即通过偏移量找到成员变量的内存地址,直接赋值)。只有copy修饰的setter方法内部用了objc_setProperty
方法,追踪objc_setProperty
,我们发现,它实质上是调用了reallySetProperty
方法,我们看一下reallySetProperty
方法的实现
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) { // 直接设置self
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset); // 获取到旧值
if (copy) { // copy attribute
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else { // 除copy之外的其他attribute
if (*slot == newValue) return; // 如果新值跟旧值是同一个,直接return
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else { // 如果是atomic attribute,进行加锁
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue); // 释放旧值
}
所以copy
是经过copyWithZone
方法以后赋值的,其实就相当于 newValue = [newValue copy]
,即真正的对象是[[NSMutableArray array] copy]
,所以self.array_copy
为不可变数组。 但以上述方式重写了setter
方法后,copy
就和strong
没有区别了。
所以我们要注意:
1、在重写copy
修饰的setter
方法时,我们应尽量这样写:
- (void)setArray:(NSArray *)array {
_array = [array copy];
}
2、不可变对象(NSString
、NSArray
等)要用copy来修饰。如果用strong,那么这个属性就可能指向一个可变对象,如果这个可变对象更改了,会影响该属性。
举个例子
@interface ViewController ()
@property (nonatomic, strong) NSArray *array;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *marray = [NSMutableArray array];
[marray addObject:@"0"];
self.array = marray;
[marray addObject:@"1"];
}
打印结果:self.array [@"0",@"1"]
marray [@"0",@"1"]
大家可以自行验证,marray
的变化会影响self.array
,但我们明明想要的是不可变对象。 用copy
就不会发生这样的问题啦~
疑问:既然strong
,assign
,weak
的 setter
方法是一样的,那么他们的特性(引用计数器+1等)一定不是在setter
方法里做的,否则strong
,assign
,weak
不就没区别了吗?
于是我们接着往下尝试直接给成员变量赋值的情况,看看这些修饰符是否直接影响成员变量
3、不使用setter
方法,直接给成员变量赋值
_array_strong = [NSMutableArray array];
[_array_strong addObject:@"0"];
_array_copy = [NSMutableArray array];
[_array_copy addObject:@"0"];
_array_assign = [NSMutableArray array];
[_array_assign addObject:@"0"];
_array_weak = [NSMutableArray array];
[_array_weak addObject:@"0"];
结果:
res1和res2一样,对象为可变数组,其值为[@"0"]
;
res3出现野指针,res4对象释放后将指针置nil
。
可以看出copy
不再有copy
的特性,等价于strong
。
我们来看看对对象成员变量进行赋值,在runtime层的实现。
/**
对对象成员变量进行赋值,这是成员变量赋值在runtime层的实现
obj:需要赋值的对象
name:成员遍历那个的名称
value:性质
assumeStrong:需要设置的内存管理方式
*/
static ALWAYS_INLINE
void _object_setIvar(id obj, Ivar ivar, id value, bool assumeStrong)
{
//空值判断
if (!obj || !ivar || obj->isTaggedPointer()) return;
ptrdiff_t offset;
objc_ivar_memory_management_t memoryManagement;
//获取对象成员变量的指针偏移量和内存管理方式
_class_lookUpIvar(obj->ISA(), ivar, offset, memoryManagement);
//如果内存管理方式为未知
if (memoryManagement == objc_ivar_memoryUnknown) {
//而且指定了strong强引用的,修正memoryManagement
if (assumeStrong) memoryManagement = objc_ivar_memoryStrong;
//没指定为strong强引用的,修正为Unretained
else memoryManagement = objc_ivar_memoryUnretained;
}
//根据指针偏移量,获取成员变量指向的对象指针
id *location = (id *)((char *)obj + offset);
switch (memoryManagement) {
//弱引用,则将新值存放在弱引用weak表
case objc_ivar_memoryWeak: objc_storeWeak(location, value); break;
//强引用,则将新值存放在强引用的strong表
case objc_ivar_memoryStrong: objc_storeStrong(location, value); break;
//Unretained的,则直接赋值
case objc_ivar_memoryUnretained: *location = value; break;
//这种情况不可能发生
case objc_ivar_memoryUnknown: _objc_fatal("impossible");
}
}
从上述源码可以看出
1、当修饰符为strong
和copy
时,实际上走的是objc_storeStrong
方法
2、当修饰符为weak
时,实际上走的是objc_storeWeak
方法
3、当修饰符为assign
时,则为直接赋值
那么引用计数器+1的特性一定在objc_storeStrong
方法中体现,将指针置nil
的特性一定在objc_storeWeak
方法中体现。我们来看看这两个方法
objc_storeStrong
objc_storeStrong(id *location, id obj)
{
//将*location指向的对象赋值给prev
id prev = *location;
//如果新值和原来的值一致,则返回,无需继续操作
if (obj == prev) {
return;
}
//对新值引用计数加一
objc_retain(obj);
//将对象指针指向新值
*location = obj;
//释放旧值
objc_release(prev);
}
和猜测的一样,对新值引用计数器+1,释放旧值
objc_storeWeak
static id
storeWeak(id *location, objc_object *newObj)
{
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) { // 如果weak ptr之前弱引用过一个obj,则将这个obj所对应的SideTable取出,赋值给oldTable
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil; // 如果weak ptr之前没有弱引用过一个obj,则 oldTable = nil
}
if (haveNew) { // 如果weak ptr要weak引用一个新的obj,则将该 obj对应的SideTable取出,赋值给newTable
newTable = &SideTables()[newObj];
} else {
newTable = nil; // 如果weak ptr不需要引用一个新obj,则 newTable = nil
}
// 加锁操作,防止多线程中竞争冲突
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
// location 应该与 oldObj 保持一致,如果不同,说明当前的 location 已经处理过 oldObj 可是又被其他线程所修改
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized()) // 如果cls还没有初始化,先初始化,再尝试设置weak
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls; // 这里记录一下 previouslyInitializedClass, 防止改if分支再次进入
goto retry; // 重新获取一遍newObj,这时的newObj应该已经初始化过了
}
}
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location); // 如果weak_ptr之前弱引用过别的对象oldObj,则调用weak_unregister_no_lock,在oldObj的weak_entry_t中移除该weak_ptr地址
}
// Assign new value, if any.
if (haveNew) { // 如果weak_ptr需要弱引用新的对象newObj
// (1) 调用weak_register_no_lock方法,将weak ptr的地址记录到newObj对应的weak_entry_t中
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// (2) 更新newObj的isa的weakly_referenced bit标志位
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
// (3)*location 赋值,也就是将weak ptr直接指向了newObj。可以看到,这里并没有将newObj的引用计数+1
*location = (id)newObj; // 将weak ptr指向object
}
else {
// No new value. The storage is not changed.
}
// 解锁,其他线程可以访问oldTable, newTable了
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj; // 返回newObj,此时的newObj与刚传入时相比,weakly-referenced bit位置1
}
每个对象都有一个weak表,存储指向这个对象的所有weak指针。这个方法主要做了三件事
1、解除旧对象与 weak 指针的绑定weak_unregister_no_lock
,更新旧对象的weak表;
2、绑定新对象与weak指针weak_register_no_lock
,更新新对象的weak表;
3、直接赋值
总结:
1、strong
和copy
的引用计数器+1特性,体现在给成员变量赋值时,实际调用objc_storeStrong
方法,在该方法内会做objc_retain
操作。
2、用copy
修饰的属性,当使用系统的setter方法赋值时,得到的对象是不可变对象。 这一特性是因为setter
内部不是直接赋值,其根本调用了reallySetProperty
方法。
3、weak
引用计数器不加1,即在storeWeak
中并没有objc_retain操作。 每个对象都有一个weak表,存储指向这个对象的所有weak指针。释放时,调用clearDeallocating
函数。clearDeallocating
函数首先根据对象地址获取这个weak表,然后遍历这个数组把其中的数据设为nil,最后清理对象的记录。
4、assign
引用计数器不加1,当对象释放后,指向对象的指针会变成野指针,是因为assign
内部就是直接赋值,没有任何操作。
参考资料
https://blog.csdn.net/u013378438/article/details/82767947
https://blog.csdn.net/u013378438/article/details/82790332
https://www.jianshu.com/p/4259ea33c49e
网友评论