iOS 修饰词 详解

作者: 郡王丶千夜 | 来源:发表于2018-03-16 17:19 被阅读1154次

    前言:

    最近公司在扩招,做为公司仅有的唯一一个首席iOS开发工程师(手动滑稽),我不得不硬着头皮上阵。

    然后却发现很多人的水平和年限严重不符,公司招的人都是3年+以上经验的人,然而这些人中有一半连修饰词的作用也说的模棱两可,加上自己水平也不高,对以后的职业生涯产生了严重的危机感,遂决定以后每周希望能写一篇有价值的文章,与君共勉,今天就说说iOS常见的几个修饰词。

    一、readOnly,readWrite

    readOnly:

    根据字面意思,大家都很容易知道是“只读”的意思,意味着只生成了getter方法,而没有生成setter方法,如果这时候调用setter方法,会报一个Assignment to readonly property错误

    PS:这里顺便说一下self.和_的区别

    self.就是调用property自动生成的getter和setter方法,而_则是直接去调用实例变量(property会自动生成一个实例变量,如果你重写了getter与setter方法,property自动生成的实例变量就无效了,需要手动去申明一个实例变量或者用@@synthesize).

    回到正题,那么这意味着我们就完全没办法去修改readOnly的属性了吗?不然,如果你尝试一下setValue:forKey:,你就会发现竟然改变成功了,amazing,让我们来看看代码:

    @interface MyFirstClass : NSObject
    
    @property (nonatomic, copy, readonly) NSString * string;
    
    @end
    
    
    #import "MyFirstClass.h"
    
    @implementation MyFirstClass
    
    - (instancetype) init{
    
        self = [super init];
        if (self) {
            _string = @"来改变我啊";
        }
    
        return self;
    }
    
    @end
    
    - (void)viewDidLoad {
    
        [super viewDidLoad];
    
        MyFirstClass * class = [MyFirstClass new];
        NSLog(@"string === %@", class.string);
        [class setValue:@"改变了" forKey:NSStringFromSelector(@selector(string))];
        NSLog(@"string === %@", class.string);
    }
    Log如下:
    2018-03-16 11:08:58.932303+0800 PropertyDesc[5681:445705] string === 来改变我啊
    2018-03-16 11:08:58.932454+0800 PropertyDesc[5681:445705] string === 改变了
    

    而如果这个时候在MyFirstClass里加入

    @implementation MyFirstClass
    
    - (instancetype) init{
        self = [super init];
        if (self) {
            _string = @"来改变我啊";
        }
        
        return self;
    }
    
    + (BOOL) accessInstanceVariablesDirectly{
        return NO;
    }
    
    @end
    在运行,boom,系统会报以下错误
    2018-03-16 11:19:21.619747+0800 PropertyDesc[6016:478446] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<MyFirstClass 0x6040000088f0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key string.'
    

    没有找到当前要赋值的属性,那么accessInstanceVariablesDirectly究竟是什么呢,我们打开苹果的官方文档找到Key-Value Coding Programming Guide

    963476FC-4E02-4196-BA64-302E16F5E8AD.png
    在这里可以看到,如果这个方法设为YES,访问器就会去寻找名称为_<key>, _is<Key>, <key>, or is<Key>的成员变量,如果为NO,就会跳到第6步,第6步就会报[valueForUndefinedKey:]错误。
    总结:
    readOnly并不能完全保证只读,我们可以通过KVC尝试去修改其值。
    PS:有兴趣的小伙伴可以尝试去修改别人的SDK,包括苹果爸爸的

    readWrite:

    这个实在没什么可说的,默认的修饰词就是readWrite,代表可读可写

    二、nonatomic、atomic

    我们先看一下官方文档


    苹果官网对两者的说法

    atomic:

    默认的属性修饰词,按官方文档上说即使从不同的线程通过getter或setter方法去访问属性也能完全的获取到或设置值,从这里也可以看出,atomic并不是线程安全的,因为atomic只能保证通过setter和getter方法能获取到一个完整的value,如果A线程在getter,B、C线程在setter,可能A获取到的值是BC执行之后的值,也可能是BC线程执行完之前的值,也可能是B C线程任何一个线程执行完的值。
    因此atomic的伪代码大概如下:

    - (void)setString:(NSString *)string{
        @synchronized(self) {
            if (_string != string) {
                [_string release];//MRC
                _string = [string copy];
            }
        }
    }
    
    - (NSString *) string{
        @synchronized(self) {
            return _ string;
        }
    }
    

    nonatomic:

    相对而言,通过nonatomic修饰的属性,并没有做锁的操作,多线程同时进行setter/getter操作,并不能保证得到一个完整的value,所以相对atomic来说nonatomic修饰的属性访问速度更快,而且平时对线程安全我们更倾向于使用信号量、NSLock和synchronized去控制线程安全,他们都能保证代码块的原子性,所以几乎所有的属性都用nonatomic去修饰。

    三、assign、weak与strong

    assign:

    一般来说,我们都用assign去修饰OC的基本数据类型,but why?
    因为assign并不会使对象的引用计数加1,也就是说如果用assign去修饰一个对象,这个对象会立即被释放,重要的是assgin在被释放的时候是不会自动置为nil,还是保留对象的指针地址,会形成野指针,这个时候向其发送消息就会崩溃,简单实验的代码如下:

    @interface MySecondClass : NSObject
    
    @property (nonatomic, assign) NSMutableArray * array;
    
    @end
    
    - (void) testMethodTwo{
        MySecondClass * class = [[MySecondClass alloc] init];
        self.secondClass = class;
        self.secondClass.array = [NSMutableArray array];
        [self.secondClass.array addObject:@(0)];
    }
    

    在运行到最后一步的时候程序会崩溃报EXC_BAD_ACCESS的错误,如果打断点的话会发现在执行到这步的时候array还是有地址的。

    weak:

    如果把上面的代码
    @property (nonatomic, assign) NSMutableArray * array;换成
    @property (nonatomic, weak) NSMutableArray * array;
    

    这个时候程序并不会崩溃,如果你打个断点的话会发现array被自动置为nil,而OC的特性使得像nil发送消息并不会崩溃,这就是weak和assgin最大的区别,此外weak必须用于修饰对象,这和他自动置为nil相关,如果强行使用weak修饰基本数据类型,编译器会报一个大大的红色错误!

    strong:

    strong的作用和assign和weak恰恰相反,strong也是属性默认的修饰词,代表着被修饰的对象引用计数+1

    如果把上面的代码
    @property (nonatomic, assign) NSMutableArray * array;换成
    @property (nonatomic, strong) NSMutableArray * array;
    
    self.secondClass.array = [NSMutableArray array];
    

    最后一句代码可以解释为[NSMutableArray array]创造了一个对象A,此时A的引用计数为1,self.secondClass.array做为对象B,把A赋值给B的时候,A的引用计数加1,此时A的引用计数为2,B指向了A,然后编译器会自动对A进行释放操作(因为是局部变量),A的引用计数-1。在拥有B的对象不释放的时候,A的引用计数永远不可能为0,除非你手动释放或者把B指向一个新的对象,这样A永远不会被释放,这就是所谓的强引用。

    四、copy与mutableCopy

    苹果官网对对象拷贝的说法

    在说copy与mutableCopy之前我们先看看官方文档对深拷贝与浅拷贝的阐释,如下

    深拷贝:

    对象拷贝 - 重新申请一片内存保留这个对象,与原对象之间没有半点关系。

    浅拷贝:

    指针拷贝 - 实际上相当于引用计数+1,被拷贝的和拷贝的引用同一个对象。
    接下来我们分两个方面做测试:

    1.对非集合类对象的copy操作,以NSString为例

    对immutableObject做copy操作
        NSString * string = [NSString stringWithFormat:@"1"];
        NSString * copyString = [string copy];
        NSString * mutableCopyString = [string mutableCopy];
        NSLog(@"string:%p", string);
        NSLog(@"copyString:%p", copyString);
        NSLog(@"mutableCopyString:%p", mutableCopyString);
    

    Log如下:

    2018-03-19 15:51:38.785253+0800 PropertyDesc[10283:759804] string:0xa000000000000311
    2018-03-19 15:51:38.785435+0800 PropertyDesc[10283:759804] copyString:0xa000000000000311
    2018-03-19 15:51:38.785518+0800 PropertyDesc[10283:759804] mutableCopyString:0x608000055150
    

    可以看出对string和copyString的地址是一样的,而mutableCopyString则不同。

    对mutableObject做copy操作
        NSMutableString * string = [NSMutableString stringWithFormat:@"1"];
        NSString * copyString = [string copy];
        NSString * mutableCopyString = [string mutableCopy];
        NSLog(@"string:%p - %@", string, string);
        NSLog(@"copyString:%p - %@", copyString, copyString);
        NSLog(@"mutableCopString:%p - %@", mutableCopyString, mutableCopyString);
        [string appendString:@",2"];
        NSLog(@"copyString:%p - %@", copyString, copyString);
        NSLog(@"mutableCopString:%p - %@", mutableCopyString, mutableCopyString);
    

    Log如下:

    2018-03-19 15:51:38.785670+0800 PropertyDesc[10283:759804] string:0x60400005a940 - 1
    2018-03-19 15:51:38.785784+0800 PropertyDesc[10283:759804] copyString:0xa000000000000311 - 1
    2018-03-19 15:51:38.785834+0800 PropertyDesc[10283:759804] copyString:0xa000000000000311 - 1
    2018-03-19 15:51:38.785891+0800 PropertyDesc[10283:759804] mutableCopyString:0x60400005a910 - 1
    2018-03-19 15:51:38.786037+0800 PropertyDesc[10283:759804] mutableCopyString:0x60400005a910 - 1
    

    可以看出对string与copyString、mutableCopyString三者的地址都是不同的。
    即使改变了原string的value,copyString与mutableCopystring也没有改变,这与下文对集合类对象得出的结论正好相反。

    结论:

    对 immutableObject进行 copy 操作是指针拷贝,mutableCopy 操作时对象拷贝。
    对 mutable Object进行 copy 和 mutableCopy 都是对象拷贝。简单的表格图如下:

    Object Handle Result
    immutableObject copy 指针拷贝
    immutableObject mutableCopy 深拷贝
    mutableObject copy 深拷贝
    mutableObject mutableCopy 深拷贝

    2.对集合类对象的copy操作

    对immutableObject做copy操作
        NSArray * array = [NSArray arrayWithObject:@"1"];
        NSArray * copyArry = [array copy];
        NSMutableArray * mutableCopyArray = [array mutableCopy];
        
        NSLog(@"array:%p", array);
        NSLog(@"copyArry:%p", copyArry);
        NSLog(@"mutableCopyArray:%p", mutableCopyArray);
    

    Log如下

    2018-03-19 15:51:38.786167+0800 PropertyDesc[10283:759804] array:0x6000000094c0
    2018-03-19 15:51:38.786278+0800 PropertyDesc[10283:759804] copyArray:0x6000000094c0
    2018-03-19 15:51:38.786385+0800 PropertyDesc[10283:759804] mutableCopyArray:0x600000240030
    

    可以看出array与copyArray的地址是一样的,而mutableCopyArray则不同。

    对mutableObject做copy操作
        NSMutableString * string = [NSMutableString stringWithFormat:@"1"];
        NSMutableArray * array = [NSMutableArray arrayWithObject:string];
        NSArray * copyArry = [array copy];
        NSMutableArray * mutableCopyArray = [array mutableCopy];
        
        NSLog(@"array:%p", array);
        NSLog(@"copyArry:%p", copyArry);
        NSLog(@"mutableCopyArray:%p", mutableCopyArray);
        [array addObject:@"2"];
        [string appendString:@"1"];
        
        NSLog(@"array:%p - %@", array, array);
        NSLog(@"copyArry:%p - %@", copyArry, copyArry);
        NSLog(@"mutableCopArray:%p - %@", mutableCopyArray, mutableCopyArray);
    

    Log如下

    2018-03-26 13:36:38.786499+0800 PropertyDesc[10283:759804] array:0x600000240150
    2018-03-26 13:36:38.786600+0800 PropertyDesc[10283:759804] copyArry:0x6000000095f0
    2018-03-26 13:36:38.786698+0800 PropertyDesc[10283:759804] mutableCopyArray:0x6000002400f0
    2018-03-26 13:36:38.786865+0800 PropertyDesc[10283:759804] array:0x600000240150 - (
        11,
        2
    )
    2018-03-26 13:36:38.787018+0800 PropertyDesc[10283:759804] copyArry:0x6000000095f0 - (
        11
    )
    2018-03-26 13:36:38.787142+0800 PropertyDesc[10283:759804] mutableCopyArray:0x6000002400f0 - (
        11
    )
    
    

    What??不管是copy还是mutableCopy我们可以看到当我们修改了string的值后,数组中的值都变了,但是在 [array addObject:@"2"];的时候两个复制出来的数组的对象并没有变化?
    这里我们要提到一个新概念 不完全深拷贝(也有人说是单层深拷贝 )------ 即虽然新开辟了内存地址,但是存放在内存上的值(也就是数组里的元素仍然指向原数组元素值,并没有另外复制一份),所以说上文中的array和mutableCopArray类似于有一个或多个相交点的相交链表。
    而且我们也可以看到不管是copy还是mutableCopy都是不完全深拷贝,三者的地址都是不一样的。

    结论:

    对immutableObject做copy是指针拷贝,做mutableCopy是不完全深拷贝。
    对mutableObject做copy或mutableCopy都是不完全深拷贝。
    有趣的是,这与上文的结论有类似之处。简单的表格图如下:

    Object Handle Result
    immutableObject copy 指针拷贝
    immutableObject mutableCopy 不完全深拷贝
    mutableObject copy 不完全深拷贝
    mutableObject mutableCopy 不完全深拷贝

    并且如果打个断点可以发现对任何对象做copy操作返回的是一个不可变的对象,对任何对象做mutableCopy返回的是一个可变的对象(有兴趣的请自行验证)。

    五、是用copy还是strong?

    通过上述对copy与strong的描述,copy和strong最终都会返回一个引用计数为1的对象,所以单单从内存管理上说copy和strong并无任何区别,但是copy始终会返回一个不可变对象,他的值是不会被修改的。
    而strong不同,被strong修饰的对象,可能会被可变对象赋值,从而在外部导致不可预料的被更改的情况。
    总而言之,是否使用copy或strong还是根据具体场景来定,这里还有个性能优化的小技巧,如果copy的是可变的对象,会对它做一次完全深拷贝/不完全深拷贝,性能上是肯定不如strong直接引用计数+1来的快。

    六、结尾

    这次的修饰词讲解到此就告一段落了,然后想一下这周写什么样的文章。

    相关文章

      网友评论

      • Hello_kid:copy和strong最终都会返回一个引用计数为1的对象,
        这句怎么理解呢?copy返回一个引用计数为1的,
        strong应该是在对象的应用计数加1吧 我的理解;
      • KinKen:你好,请问那些官方阐释文档在哪里看的,我只能找到那些API介绍,可以给一个link吗,先谢谢了!
        KinKen:@郡王丶千夜 谢谢
        郡王丶千夜:https://developer.apple.com/library/archive/navigation/ 基本上都能在这里找到,或者去apple developer搜索
      • doubleJJ:感谢分享,对于深拷贝和浅拷贝还是有点混乱,看来我还是需要消化一下
      • 萧城x:坐标哪里?
        郡王丶千夜:@Jason_cjc 南京:smile:
      • ad27526bec9d:😘😘😘😘😘😘
      • vicktor_Liu:很不错哦!~\(≧▽≦)/~
        郡王丶千夜:谢谢,这周写的Crash收集与分析已经补充完整了,希望去支持下:stuck_out_tongue_closed_eyes:

      本文标题:iOS 修饰词 详解

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