[iOS] 属性修饰符之copy及atomic/readwrite

刚开始的时候我其实分不太清property和ivar,后来才知道property属性=成员变量+set+get方法,也就是property是对外的,成员变量ivar是对内的。

之后对于property的各个属性也是非常迷,只知道要用nonatomic和weak/strong,所以一起来探讨一下属性的各个可爱的修饰符是干啥的吧~

首先我们先看一下有哪些修饰符:

类别 修饰符
读写权限相关 readonly, readwrite
原子性/读写安全 nonatomic, atomic
内存管理 strong, weak, assign, copy, unsafe_unretained, retain

读写修饰符

如题头所述,property默认是会生成get和set方法的,例如: (注意不可以同时覆写get和set方法哦,这样会报错的哦,当然如果你覆写setter没有用到ivar实例变量那么是OK的,否则就会有找不到实例变量的错误)

@property (nonatomic) NSString *intent;

- (NSString *)intent {
    return _intent;
}

- (void)setIntent:(NSString *)intent {
    _intent = intent;
}

当外边读取xxx.intent的时候其实是调用的intent方法,调用xxx.intent=xxx的时候是调用的setIntent方法。即使是内部自己调用self.intent也是酱紫的。所以读写权限限制就是通过控制getter和setter实现滴。

① readwrite:同时生成getter和setter方法,属性可以读写(默认)
② readonly:只生成getter方法,属性只读(内部修改时需要调用_intent,不可以用self.intent)

Something interesting:

  1. 如果用readonly在.h文件中声明一个property,然后.m文件中用readwrite声明同一个property,则外面不可以设置这个属性,而自身在.m里面可以用self.xxx设置这个属性。
  2. 用readonly在.h文件中声明property,然后在.m中自己定义setXXX方法,并且把这个方法在.h文件中声明,则外面仍旧可以设置这个属性。

readwrite和readonly其实就是控制了setter和getter方法的对外可见性,你可以把你的setter或者getter写到.h里面强制让他们对外的。

安全修饰符

这两个修饰符大家应该都很熟悉,并且大多数时候用的是nonatomic,为什么atomic是原子性,即保证操作的完整性(例如现实生活中,电脑断电了,文档要么是完全保存了,要么是一点都没有保存的状态,不产生中间状态,即原子性),但大家都不用atomic嘞?

① atomic:原子性操作,给getter/setter加锁,但相比不加锁有性能损耗(默认)
 - (void)setIntent:(NSString *)intent {
    @synchronized (self) {
        _intent = intent;
    }
}
② nonatomic:不加锁,读写不安全,一般如果没有多线程问题都用非原子性,因为性能损耗小

这里做个实验重写setter:

@property (atomic, copy) NSString *intent;

- (void)testAtomic {
    dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
        self.intent = @"hhh";
    });
}

- (void)setIntent:(NSString *)intent {
    NSLog(@"setIntent start");
    _intent = [intent copy];
    NSLog(@"setIntent end");
}

输出:
2020-06-17 06:59:17.450497+0800 Example1[1199:26236] setIntent start
2020-06-17 06:59:17.450645+0800 Example1[1199:26236] setIntent end
2020-06-17 06:59:17.458051+0800 Example1[1199:26236] setIntent start
2020-06-17 06:59:17.458062+0800 Example1[1199:26549] setIntent start
2020-06-17 06:59:17.458071+0800 Example1[1199:26547] setIntent start
2020-06-17 06:59:17.458110+0800 Example1[1199:26650] setIntent start
2020-06-17 06:59:17.458214+0800 Example1[1199:26236] setIntent end
……

并且setter还会报warning:Writable atomic property 'intent' cannot pair a synthesized getter with a user defined setter

可以看出就是atomic的如果你重写setter/getter就不OK啦,它的synchronized是写在setter/getter内的。


  • BUT: atomic只能保证读写操作的完整,不能保证线程安全
    (这里是借鉴参考文章的哈)

假设有一个atomic的属性 "name",如果线程 A 调 [self setName:@"A"],线程 B 调 [self setName:@"B"],线程 C 调 [self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。

但是,如果有另一个线程 D 同时在调 [name release],那可能就会crash,因为release不受getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。

如果 name 属性是 nonatomic 的,那么上面例子里的所有线程 A、B、C、D都可以同时执行,可能导致无法预料的结果。如果是atomic的,那么 A、B、C 会串行,而 D 还是并行的。

这里有一个小脑洞,如何用GCD来实现一个数组的安全读写呢?
个人意见哈: 通过gcd的栅栏任务,每次写的时候都抛一个栅栏。
但这样是不是完全安全呢?

内存管理修饰符

关于内存的修饰符真的好多,每次看到都很头大= =||

① copy:常用于修饰有mutable子类的类型的变量,例如NSString/NSArray/NSDictionary,防止外面赋值是mutable数据改变以后导致里面的数据也被改了。

P.S. block块也可能会用copy修饰,具体为啥一会儿讨论哈

- (void)setIntent:(NSString *)intent {
      if (_intent != intent) {
        [_intent release];
        _intent = [intent copy];
     }
}

既然恰巧碰到了copy,我们就来一起看一下copy干了啥吧~
以NSString的copy为例先来看下它做了神马

NSString *intentUnmuteable = @"First";
self.intent = intentUnmuteable;
NSLog(@"intent对象地址:%p intent对象指针地址:%p intentUnmuteable对象地址:%p intentUnmuteable对象指针地址:%p", _intent, &_intent, intentUnmuteable, &intentUnmuteable);

输出
inten对象地址t:0x10a4be158 
intent对象指针地址:0x7f828a41e1d0 
intentUnmuteable对象地址:0x10a4be158  //和intent对象的地址相同
intentUnmuteable对象指针地址:0x7ffee57416f8

可以看出NSString的copy其实并没有把传入的NSString copy一个新的对象,然后赋值给属性,只是单纯的把传入的NSString的指针拷贝过来了。

如果这时候改变intentUnmuteable的值,_intent是不会被改变的,因为NSString的内容是不可变的,改intentUnmuteable的内容其实是将intentUnmuteable指向的String地址变了,_intent仍旧指向旧String的地址,故而对于不mutable的变量,苹果的copy只拷贝指针,指向的内容是不可变的。

接下来我们来考虑一下对于mutable的string是肿么做的呢?

NSMutableString *intentUnmuteable = [NSMutableString stringWithFormat:@"First"];
self.intent = intentUnmuteable;
NSLog(@"intent对象:%@ intent对象地址:%p intent对象指针地址:%p intentUnmuteable对象地址:%p intentUnmuteable对象指针地址:%p",_intent, _intent, &_intent, intentUnmuteable, &intentUnmuteable);

[intentUnmuteable setString:@"Second"];
NSLog(@"intent对象:%@ intent对象地址:%p intent对象指针地址:%p intentUnmuteable对象地址:%p intentUnmuteable对象指针地址:%p",_intent, _intent, &_intent, intentUnmuteable, &intentUnmuteable);

输出
intent对象:First 
intent对象地址:0xc7f16fa1554c8719 
intent对象指针地址:0x7fd42a40fd30 
intentUnmuteable对象地址:0x600002c8a400  //这次和intent指向的地址不一样啦
intentUnmuteable对象指针地址:0x7ffee32056f8

intent对象:First 
intent对象地址:0xc7f16fa1554c8719 
intent对象指针地址:0x7fd42a40fd30 
intentUnmuteable对象地址:0x600002c8a400 
intentUnmuteable对象指针地址:0x7ffee32056f8

所以如果赋给属性的值是mutable的,copy会自动深拷贝一个String,即将可变字符串的字面量拷过来,放入一个新的string地址存起来,不会和赋值的指针指向同一个地址。

如果你重写了set方法,即使设置了copy也是没用的,以及如果你用_intent=xxx直接赋值,其实copy也没有生效哦


这里顺路我们看一下copy这件事儿,涉及了深拷贝浅拷贝以及拷贝协议等……

(1) 首先,什么对象可以copy嘞?

只有实现了NSCopying协议的对象才可以拷贝,否则会crash的哦

@interface Student() 

@property (strong, nonatomic) NSString* name;

@end

@implementation Student
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    return self;
}
@end

是不是看起来非常的简单~ 那如果这样子做,我们的copy会只拷贝指针还是整个对象呢?让我们来试验一下

Student *stu2 = [[Student alloc] init];
stu2.name = @"Jack";
self.stu1 = [stu2 copy];
NSLog(@"stu1对象:%@ stu1对象地址:%p stu1对象指针地址:%p stu2对象地址:%p stu2对象指针地址:%p",_stu1, _stu1, &_stu1, stu2, &stu2);

stu2.name = @"Rose";
NSLog(@"stu1对象name:%@ stu1对象地址:%p stu1对象指针地址:%p stu2对象地址:%p stu2对象指针地址:%p",_stu1.name, _stu1, &_stu1, stu2, &stu2);

输出:
stu1对象: 
stu1对象地址:0x600000340b60 
stu1对象指针地址:0x7fee0770b388 
stu2对象地址:0x600000340b60  //和stu1的地址一致
stu2对象指针地址:0x7ffeef5246f0

stu1对象name:Rose    //和stu2的name都被修改了
stu1对象地址:0x600000340b60 
stu1对象指针地址:0x7fee0770b388 
stu2对象地址:0x600000340b60 
stu2对象指针地址:0x7ffeef5246f0

看来自定义一个NSCopying对象做copy动作会浅拷贝,只拷贝指针,但这不是一定的哦!其实copy动作会深拷贝还是浅拷贝自定义对象,是由我们如何实现copyWithZone决定的。

如果你希望执行copy时可以深拷贝,可以尝试以下的方式:

- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    Student *stu = [[Student alloc] init];
    stu.name = self.name;
    return stu;
}

此时再执行之前的代码会发现self.stu1和局部变量stu2指向的object的地址不一样了

stu1对象name:Jack  //执行了stu2.name=@"Rose"后的输出
stu1对象地址:0x6000009490f0 
stu1对象指针地址:0x7f96fbc190b8 
stu2对象地址:0x6000009490d0 
stu2对象指针地址:0x7ffee10386f0
现在来思考一个事儿,如果student的name属性是NSMutableString呢?

其实是一个道理,如果我们在实现NSCopying的时候只是简单的让stu.name = self.name,那么如果name是NSMutableString,当传进来的student的name被改变的时候,由于我们拷贝的对象的name和传入的student的name指向的是同一个String,所以self.stu1的name也变了。

如果不希望发生这样的事情,可以用stu.name = [self.name mutableCopy]就可以啦,因为NSMutableString实现NSCopying协议的时候并不是简单的返回self,而是将字面值新建了一个String返回的~

注意的一点,当Student有子类时,需要这样写Student *stu = [[[self class] allocWithZone] init];
因为copyWithZone会被继承,当子类使用copy方法时,最终会调用到父类的copyWithZone方法

(2) Array的copy是什么样子的呢?

先来看一下NSArray的copy是什么样子的~

@property (nonatomic, copy) NSArray *arr1;

NSArray *arr2 = @[[NSMutableString stringWithFormat:@"1"], @"2", @"3"];
self.arr1 = arr2;
NSLog(@"arr1对象:%@ arr1对象地址:%p arr1对象指针地址:%p arr2对象地址:%p arr2对象指针地址:%p",_arr1, _arr1, &_arr1, arr2, &arr2);

[[arr2 objectAtIndex:0] setString:@"4"];
NSLog(@"arr1对象:%@ arr1对象地址:%p arr1对象指针地址:%p arr2对象地址:%p arr2对象指针地址:%p",_arr1, _arr1, &_arr1, arr2, &arr2);

输出:
arr1对象:(1, 2, 3) 
arr1对象地址:0x60000199f330 
arr1对象指针地址:0x7ffd36418280 
arr2对象地址:0x60000199f330  //和arr1相同
arr2对象指针地址:0x7ffeecd416c8

arr1对象:(4, 2, 3) 
arr1对象地址:0x60000199f330 
arr1对象指针地址:0x7ffd36418280 
arr2对象地址:0x60000199f330 
arr2对象指针地址:0x7ffeecd416c8

和NSString类似,因为对象本身不Mutable,copy操作只会复制指针,所以arr1和arr2指向的是同一个对象,也就是当arr2里面的对象改变时,arr1的内容也改变了。

那么NSMutableArray是神马样子呢?可以预见它应该会找一个新的地址,把array里面的每个item复制(复制指针)到新的地址。我们来尝试一下~

@property (nonatomic, copy) NSMutableArray *arr1;

NSMutableArray *arr2 = [NSMutableArray arrayWithObjects:[NSMutableString stringWithFormat:@"1"], @"2", @"3", nil];
self.arr1 = arr2;
NSLog(@"arr1对象:%@ arr1对象地址:%p arr1对象指针地址:%p arr2对象地址:%p arr2对象指针地址:%p",_arr1, _arr1, &_arr1, arr2, &arr2);

[[arr2 objectAtIndex:0] setString:@"4"];
NSLog(@"arr1对象:%@ arr1对象地址:%p arr1对象指针地址:%p arr2对象地址:%p arr2对象指针地址:%p",_arr1, _arr1, &_arr1, arr2, &arr2);

[arr2 addObject:@"6"];
NSLog(@"arr1对象:%@ arr1对象地址:%p arr1对象指针地址:%p arr2对象地址:%p arr2对象指针地址:%p",_arr1, _arr1, &_arr1, arr2, &arr2);

输出:
arr1对象:(1, 2, 3) 
arr1对象地址:0x600001dbde00 
arr1对象指针地址:0x7fec7cc27910 
arr2对象地址:0x600001d8bab0  // 和arr1不一样了
arr2对象指针地址:0x7ffeeb68c6e8

arr1对象:(4, 2, 3) 
arr1对象地址:0x600001dbde00 
arr1对象指针地址:0x7fec7cc27910 
arr2对象地址:0x600001d8bab0 
arr2对象指针地址:0x7ffeeb68c6e8

arr1对象:(4, 2, 3) 
arr1对象地址:0x600001dbde00 
arr1对象指针地址:0x7fec7cc27910 
arr2对象地址:0x600001d8bab0 
arr2对象指针地址:0x7ffeeb68c6e8

和预想的一样,arr1和arr2的地址并不一样,但是如果改arr2里面的元素为什么arr1也会跟着变呢?因为arr1在拷贝的时候只是arr2里面所有元素的指针拷过来了,如果改指针指向内容,自然arr1也就跟着变了,那如果我们在arr2里面增删元素,arr1是不会变的,因为数组的地址不一样啦。

这里我们发现数组的拷贝会拷贝每个元素的指针,这个是由元素决定的还是由于数组的NSCopying里面就是这样规定的呢?

我们猜测数组对元素的处理可能有两种,即NSMutableArray的copyWithZone数组的实现可能是:
NSMutableArray *arrCopy = [NSMutableArray array];

  1. 循环self.objects然后直接 [arrCopy addObject:obj]
  2. 循环self.objects然后直接 [arrCopy addObject:[obj copy]]

下面我们做个试验,用Student对象来看一下~
让Student实现NSCopying并返回一个新的Student对象,即深拷贝

Student *stu = [[Student alloc] init];
stu.name = @"Ying";
NSMutableArray *stuArr2 = [NSMutableArray arrayWithObjects:stu, nil];
self.stuArr1 = stuArr2;
NSLog(@"stuArr1对象:%@ stuArr1对象地址:%p stuArr1对象指针地址:%p stuArr2对象地址:%p stuArr2对象指针地址:%p",_stuArr1, _stuArr1, &_stuArr1, stuArr2, &stuArr2);

stu.name = @"Iris";
NSLog(@"stuArr1的student名字:%@ stuArr2的student名字:%@", [_stuArr1 objectAtIndex:0].name, [stuArr2 objectAtIndex:0].name);

输出:
stuArr1对象:("") 
stuArr1对象地址:0x600000b75520 
stuArr1对象指针地址:0x7ffa98528df8 
stuArr2对象地址:0x60000072fae0 
stuArr2对象指针地址:0x7ffee3c056e0

stuArr1的student名字:Iris 
stuArr2的student名字:Iris

可以看出即使Student实现了新的NSCopying,NSMutableArray在拷贝的时候仍旧没有把每个元素拷贝一份新的,放入新的数组,只是单纯的拷贝指针,才造成了改了stuArr2数据后stuArr1内的数据也被修改了。所以NSMutableArray的拷贝实际上是写死拷贝的指针,并没有执行[object copy],单纯把原object add进新数组而已。

这个也非常正常,因为Array不能确保它里面的元素都实现了NSCopying,只拷贝指针是安全的。
关于copy和mutableCopy还有好多,下篇再写吧~

(3) 为啥block需要copy?

参考链接里面的文章写的很好啦,总结一下大概就是MRC时代block是在栈里面的,函数执行完就会被释放掉,即使用了strong也没有拷贝到堆区,只是增加了指向,使用时可能会有野指针crash。copy可以让block从栈区拷贝到堆区。

ARC下栈区会自动拷贝到堆区,所以其实只有两个区,全局区和堆区,其实不会出现野指针的问题,故而ARC用strong/copy没有太大区别。


关于copy的内容就此结束啦,还有一些属性会在下篇文里面介绍一下~ 希望不鸽~~

参考链接:
修饰符:https://www.jianshu.com/p/3cbc79424fb8
线程安全:https://www.jianshu.com/p/7288eacbb1a2
copy:https://www.jianshu.com/p/ac654c505079
深浅拷贝:https://www.jianshu.com/p/d0f1de45dfc6
NSCopying: https://www.jianshu.com/p/85a2ea9ee300
block的copy: https://www.jianshu.com/p/bf3798fe3f49

你可能感兴趣的:([iOS] 属性修饰符之copy及atomic/readwrite)