美文网首页ios
[iOS] 属性修饰符之copy及atomic/readwrit

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

作者: 木小易Ying | 来源:发表于2019-07-23 23:01 被阅读0次

    刚开始的时候我其实分不太清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() <NSCopying>
    
    @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对象:<Student: 0x600000340b60> 
    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<Student *> *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对象:("<Student: 0x600000b75550>") 
    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/readwrit

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