美文网首页iOS底层基础知识iOSiOS面试总结
copy,assign,strong,weak修饰符的深入理解

copy,assign,strong,weak修饰符的深入理解

作者: 毅想天开的小毅 | 来源:发表于2019-03-25 23:12 被阅读21次

前言:为了更深入的了解这些修饰符,特意写了个小测试,并展开深入的探究。如果文章有哪些地方有误,还请多多指点~
目录
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、不可变对象(NSStringNSArray等)要用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就不会发生这样的问题啦~

疑问:既然strongassignweaksetter方法是一样的,那么他们的特性(引用计数器+1等)一定不是在setter方法里做的,否则strongassignweak不就没区别了吗?
于是我们接着往下尝试直接给成员变量赋值的情况,看看这些修饰符是否直接影响成员变量

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、当修饰符为strongcopy时,实际上走的是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、strongcopy的引用计数器+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

相关文章

网友评论

    本文标题:copy,assign,strong,weak修饰符的深入理解

    本文链接:https://www.haomeiwen.com/subject/ebonmqtx.html