美文网首页
重写Property的set方法,跟我犯同样错误?

重写Property的set方法,跟我犯同样错误?

作者: 我叫阿水 | 来源:发表于2018-08-16 23:42 被阅读371次

不管你是iOS新手还是老鸟,property这个东西是iOSer再熟悉不过的东西了。而关于property的相关知识点,诸如property = _ivar + set方法 + get方法这些,网上相关文章很多,也写得很详细,这里不会去做解释。

这篇文章是我今晚在学习的时候,发现的一个细节点,而且还是自己很长一段时间以来犯的错误,竟无从察觉...😂 所以记录下来,给自己提个醒,加深下印象。当然,也希望能对跟我一样慢知的童鞋,有所帮助。😁😆

抛砖引玉

先来看下面这个demo代码

@interface Person: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, assign) float weight;
@end

@implementation Person
- (void)setName:(NSString *)name {
    _name = name;
    // do something
}
@end

回想一下,你平时重写set方法的时候,是不是这样写?这样写会有什么问题?(注意name的property attribute)

顺藤摸瓜

我们都知道,编译器会帮我们生成上面三个property对应的实例变量,以及三对set/get方法,那么我们不妨来看看,系统为我们提供的默认实现是怎样的?

static NSString * _I_Person_name(Person * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct Person, _name), (id)name, 0, 1); }

static NSNumber * _I_Person_eyes(Person * self, SEL _cmd) { return (*(NSNumber **)((char *)self + OBJC_IVAR_$_Person$_eyes)); }
static void _I_Person_setEyes_(Person * self, SEL _cmd, NSNumber *eyes) { (*(NSNumber **)((char *)self + OBJC_IVAR_$_Person$_eyes)) = eyes; }

static float _I_Person_weight(Person * self, SEL _cmd) { return (*(float *)((char *)self + OBJC_IVAR_$_Person$_weight)); }
static void _I_Person_setWeight_(Person * self, SEL _cmd, float weight) { (*(float *)((char *)self + OBJC_IVAR_$_Person$_weight)) = weight; }

这里稍作解释下,我们知道,通过alloc生成一个类的实例对象,比如上面的Person,那么在内存上就会分配一块实例对象的内存空间,里面存放着isa以及三个ivar的值,而self也就是这块内存空间的首地址;那么上面的OBJC_IVAR_$_Person$_name就很好理解了,就是_name相对于这块内存空间的偏移量,其他的也是如此;而__OFFSETOFIVAR__(struct Person, _name)这个函数作用,其实求偏移量;

extern "C" unsigned long int OBJC_IVAR_$_Person$_name __attribute__ ((used, section ("__DATA,__objc_ivar"))) = __OFFSETOFIVAR__(struct Person, _name);

可能细心的童鞋就会发现了,nameset方法实现,是跟其他两个不一样的。ageweight很好理解,就是通过偏移量获取到相应内存的地址,然后直接把新的值设置进去。但name的set方法里面却是调用了一个叫objc_setProperty的函数,前面的几个参数我们都很好理解跟猜到,但后面的0跟1,又是什么意思?
跳到该函数的申明,我们才发现,原来是代表atomicshouldCopy

OBJC_EXPORT void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

顺带提一下,还有几个跟它相似的函数,提供了参数默认实现的版本

OBJC_EXPORT void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) OBJC_AVAILABLE(10.8, 6.0, 9.0, 1.0);
OBJC_EXPORT void objc_setProperty_nonatomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) OBJC_AVAILABLE(10.8, 6.0, 9.0, 1.0);
OBJC_EXPORT void objc_setProperty_atomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) OBJC_AVAILABLE(10.8, 6.0, 9.0, 1.0);
OBJC_EXPORT void objc_setProperty_nonatomic_copy(id self, SEL _cmd, id newValue, ptrdiff_t offset) OBJC_AVAILABLE(10.8, 6.0, 9.0, 1.0);

其实有点不懂,上面name的set方法实现,直接调用objc_setProperty_nonatomic_copy 不就好了?

如果你去看它们的实现,最终都是调用同一个函数实现,完整实现如下:

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); // 释放旧值
}

来到这里,结合前面我们看到nameset方法实现,当我们为name赋值新的值时,系统就会copy一份新值赋值给name,最后释放掉原先的旧值。这也是当我们的property的设置copy的attribute时,才会出现的操作,其他诸如strong assign之类的,都是直接把新值写入相应的内存地址.

勿忘初心

那么回到一开始的问题,那样重写set方法会出现什么问题?相信不少童鞋也都能理解到了。
我们先来看下,那些写,最终的实现是怎样的?

static void _I_Person_setName_(Person * self, SEL _cmd, NSString *name) {
    (*(NSString **)((char *)self + OBJC_IVAR_$_Person$_name)) = name;
}

很不幸,并没有如我们所希望的,先copy再赋值,而是直接把值附给了_name,这将会导致一个问题,也是我们用copy的初衷想避免的问题

NSMutableString *name = [[NSMutableString alloc] initWithString:@"你为什么叫阿水?"];
Person *person = [[Person alloc] init];
person.name = name;
NSLog(@"前值 person.name: %@", person.name);
[name appendString:@"我不知道,他们给我起的名"];
NSLog(@"后值 person.name: %@", person.name);

// 打印如下:
前值 person.name: 你为什么叫阿水?
后值 person.name: 你为什么叫阿水?我不知道,他们给我起的名

这样会导致,外界传进来的那个值发生改变时,我们的name也跟着变了,而我们初衷就是要避免这种情况的发生。

所以,正确的写法应该是我们自己主动去copy一次

- (void)setName:(NSString *)name {
    _name = [name copy];
}

好了,到这里就结束了。
其实很简单的一个知识点,啰啰嗦嗦硬是撸了这么多(别以为我不知道你是为了凑字数的-. -),其实也就是把我从发现问题到倒去验证的整一个过程记录下来,毕竟好记性不如烂笔头,写下来以后忘了自己还能看看,哈哈😆

相关文章

网友评论

      本文标题:重写Property的set方法,跟我犯同样错误?

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