不管你是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);
可能细心的童鞋就会发现了,name
的set
方法实现,是跟其他两个不一样的。age
跟weight
很好理解,就是通过偏移量获取到相应内存的地址,然后直接把新的值设置进去。但name的set方法里面却是调用了一个叫objc_setProperty
的函数,前面的几个参数我们都很好理解跟猜到,但后面的0跟1,又是什么意思?
跳到该函数的申明,我们才发现,原来是代表atomic
跟 shouldCopy
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); // 释放旧值
}
来到这里,结合前面我们看到name
的set
方法实现,当我们为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];
}
好了,到这里就结束了。
其实很简单的一个知识点,啰啰嗦嗦硬是撸了这么多(别以为我不知道你是为了凑字数的-. -)
,其实也就是把我从发现问题到倒去验证的整一个过程记录下来,毕竟好记性不如烂笔头,写下来以后忘了自己还能看看,哈哈😆