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

不管你是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];
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容