property与多线程赋值问题

681 阅读3分钟

ios中通过@property可以自动生成setter和getter方法,使用及其方便,这里就介绍属性操作那点事,以及多线程操作过程存在的隐患过程,了解其可以提高代码质量

@property的getter和setter方法介绍

查看objc的getter和setter需要用到objc源码来查看,这里面会贴出代码介绍

getter

getter方法获取属性内容源码如下所示,其通过

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }
    
    //非原子性属性操作,直接通过对象+偏移量获取到对应的属性内容
    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
    
    //原子性操作,需要用到spinlock_t自旋锁,与setter通用一个锁PropertyLocks[slot],以保证读写的原子性
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    //返回对象
    return objc_autoreleaseReturnValue(value);
}

setter

setter要比getter方法要复杂不少,其中的atomic、nonatomic、retain、copy等都在这里处理,如下所示

可以发现,都是调用了reallySetProperty的方法,唯一区别就是原子性与是否copy,且方法调用非线程安全

//默认的属性的setProperty方法,默认为atomic,且根据情况能copy就copy
oid objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, 
    id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
//atomic的retain操作
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}
//nonatomic的retain操作
void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, false, false);
}
//atomic的copy操作
void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, true, false);
}
//nonatomic的copy操作
void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, false, true, false);
}

reallySetProperty方法介绍了setter方法的主要逻辑,且其并非线程安全,如下所示

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }
    
    1.
    //初始化旧值
    id oldValue;
    id *slot = (id*) ((char*)self + offset);
    
    2.
    //根据copy属性copy内容
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        //新值和旧值是一个直接结束
        if (*slot == newValue) return;
        //对新值进行retain操作
        newValue = objc_retain(newValue);
    }
    
    3.
    //非原子性操作nonatomic直接取出旧值,附上新值
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        //原子性操作,与setter公用一个锁PropertyLocks[slot],以保证读写操作的原子性
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    
    4.
    //对旧值进行release
    objc_release(oldValue);
}

void 
objc_release(id obj)
{
    // 如果是taggedPointer直接结束
    if (obj->isTaggedPointerOrNil()) return;
    //否则直接release
    return obj->release();
}

多线程操作属性问题

上面的内容看着没有什么问题,可是由于不是线程安全,下面加入多线程 -- 案例代码

为了方便描述多线程操作过程中的问题,上面将reallySetProperty方法执行操作大致分为了4步

下面介绍问题出现的步骤:

多线程意味着同时会有多个线程执行同一个函数,操作同一个对象

多线程操作过程中,可能会同时执行1、2、3步骤,即同时指向了同一个老对象oldValue,赋值新对象newValue给slot,因此此过程一定几率会出现新值赋值 *slot = newValue; 执行了多次,新值不知道赋予是哪一个后执行,因此不知道是哪一个

至此,最多出现数据赋值更新问题,然而进行到objc_release方法释放时,旧值oldValue指向的地址仍然是同一个地址老对象oldValue,此时多个线程同时执行完毕到步骤4,调用objc_release(oldValue)对同一个oldValue,多次释放,因此这个oldValue会被多次释放,从而导致野指针访问,出现崩溃

最后

至此,介绍了属性的读写操作、原子性操作以及多线程操作中出现的问题,理解了就又是充满希望的一天