从@property (nonatomic, copy) NSS

作者: 二亮子 | 来源:发表于2016-05-20 00:39 被阅读2150次

    引言

    一般我们都会看到这样一条代码规范:

    NSString类型的属性一般用copy修饰,而不是用strong来修饰。

    这是为什么呢?

    举个例子

    @interface Person : NSObject
    @property (nonatomic, strong) NSString *name;
    @end
    
    ...
    NSMutableString *tempName = [[NSMutableString alloc] initWithString:@"hello"];
    NSLog(@"tempName:%p", tempName);
    
    Person *aPerson = [[Person alloc] init];
    aPerson.name = tempName;
    NSLog(@"aPerson.name:%@:%p", aPerson.name, aPerson.name);
    
    [tempName appendString:@" world"];
    NSLog(@"aPerson.name:%@:%p", aPerson.name, aPerson.name);
    
    //tempName:0x7fd093fa17e0
    //aPerson.name:hello:0x7fd093fa17e0
    //aPerson.name:hello world:0x7fd093fa17e0
    

    当一个对象(aPerson)的某个属性(name)的类型存在可变子类(NSMutableString: NSString)时, 赋值给该属性的对象(tempName),如果是可变的,修改该对象(tempName)的值,会影响到这个属性(name)的值,这显然不是我们希望的。

    从log中的内存地址信息可以看出:strong修饰的属性(name)并不会开辟新的内存,而是直接强引用已有的内存(tempName的内存)


    @interface Person : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    ...
    NSMutableString *tempName = [[NSMutableString alloc] initWithString:@"hello"];
    NSLog(@"tempName:%p", tempName);
    
    Person *aPerson = [[Person alloc] init];
    aPerson.name = tempName;
    NSLog(@"aPerson.name:%@:%p", aPerson.name, aPerson.name);
    
    [tempName appendString:@" world"];
    NSLog(@"aPerson.name:%@:%p", aPerson.name, aPerson.name);
    
    //tempName:0x7feb3951b6b0
    //aPerson.name:hello:0xa00006f6c6c65685
    //aPerson.name:hello:0xa00006f6c6c65685
    

    当把strong换成copy的时,从log中的内存地址信息我们得知,copy的时候会开辟新的内存,而此时修改tempName并不会对aPerson.name产生影响,这正是我们希望的。

    这个例子仅仅从表面说明了这个代码规范的正确性,还有一些东西需要我们去探索。

    编译器对copy的优化

    @interface Person : NSObject
    @property (nonatomic, copy) NSString *name;
    @end
    
    ...
    Person *aPerson = [[Person alloc] init];
    NSString *tempName = @"hello";
    NSLog(@"tempName:%p", tempName);
    aPerson.name = tempName;
    NSLog(@"aPerson.name:%p", aPerson.name);
        
    //tempName:0x10550a078
    //aPerson.name:0x10550a078
    

    把上面的例子和这个例子结合在一起我们能得出以下结论:

    1.当copy修饰的属性赋值时的对象是一个不可变对象的时候,不会发生内存的拷贝行为,发生的仅仅是指针的强引用。
    2.当copy修饰的属性赋值的对象是一个可变对象的时候才会发生内存的拷贝。


    @interface Person : NSObject
    @property (nonatomic, copy) NSMutableString *name;
    
    ...
    NSMutableString *tempName = [NSMutableString stringWithString:@"hello"];
    NSLog(@"tempName:%p", tempName);
    
    Person *aPerson = [[Person alloc] init];
    aPerson.name = tempName;
    NSLog(@"aPerson.name:%p", aPerson.name);
    
    [aPerson.name appendString:@" world"];
    NSLog(@"aPerson.name:%p", aPerson.name);
    
    //tempName:0x79839cb0
    //aPerson.name:0x7993e3b0
    //*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendString:'
    
    @end
    

    从该例子可以看出,虽然我们声明的name是NSMutableString,但实际上确是NSString,在appendString:时发生了crash说明了这一点。

    把上面的两个例子和这个例子结合在一起,能得出以下几个结论:

    1.当copy修饰的属性赋值时的对象是一个不可变对象的时候,不会发生内存的拷贝行为,发生的仅仅是指针的强引用。
    2.当copy修饰的属性赋值的对象是一个可变对象的时候才会发生内存的拷贝。
    3.即使copy修饰的属性是一个可变对象,发生了内存拷贝,但是其实拷贝出来的对象依然是不可变的,这一点要尤其注意。

    由此可以看出编译器对copy的优化分这三种情况。

    再来谈谈mutableCopy

    在谈mutableCopy之前,我们先简单的说说点语法,我们修饰属性时用到的copy修饰符,其实就是在对应的setter方法里赋值的时候调用copy方法,一个涉及到点语法的本质,这里不再细说了。

    之所以说这个,是因为mutableCopy并不是一个属性修饰符,研究它的时候,只能我们自己手动的调用mutableCopy方法。

    讲mutableCopy之前,再说最后一点:OC中原型的设计模式其实就是copy,我们可以对OC系统类的对象调用copy,来复制一个对象,复制逻辑就是上面总结的三条,当我们对非系统类的对象调用copy的时候是会crash的,原因是对象调用copy方法的前提是遵循NSCopying协议,实现copyWithZone:方法,这两步系统类都给我们做好了,我们的非系统类要想调用copy方法就必须自己实现这两步,要注意。

    NSString *mTest = @"hello";
    NSMutableString *mTest1 = [mTest mutableCopy];
    NSLog(@"mTest:%p", mTest);
    NSLog(@"mTest1:%p", mTest1);
    [mTest1 appendString:@" world"];
        
    NSMutableString *mTest2 = [NSMutableString stringWithString:@"Hi"];
    NSMutableString *mTest3 = [mTest2 mutableCopy];
    NSLog(@"mTest2:%p", mTest2);
    NSLog(@"mTest3:%p", mTest3);
    [mTest3 appendString:@" world"];
    
    //mTest:0xd403c
    //mTest1:0x7ae72820
    //mTest2:0x7c1680f0
    //mTest3:0x7c1743d0
    

    从这个例子中我们能总结出:

    1.当mutableCopy方法调用时,无论拷贝的是不可变对象,还是可变对象,内存拷贝都会发生。
    2.拷贝出来的对象永远是可变的。

    到这里大家可能已经掌握了copy、mutableCopy,但是还没有结束。

    没那么简单

    NSMutableString *mStr = [@"hell" mutableCopy]; //理解了mutableCopy之后,我们最好使用这种方式,也算是一个代码规范
    NSMutableArray *tempArr = [@[mStr] mutableCopy];
    NSLog(@"tempArr:%p", tempArr);
    NSLog(@"tempArr[0]:%p", tempArr[0]);
        
    NSMutableArray *tempArr1 = [tempArr mutableCopy];
    NSLog(@"tempArr1:%p", tempArr1);
    NSLog(@"tempArr1[0]:%p", tempArr1[0]);
    
    // tempArr:0x7c0422a0
    // tempArr[0]:0x7c237
    //tempArr1:0x7b6472f0
    //tempArr1[0]:0x7c237
    

    我们从这个例子可以惊奇的发现:

    如果copy的是一个系统的容器类对象(arr、dic、set),该容器类对象的确会被拷贝,但是他们里面的元素却是不进行拷贝的,是公用一块内存的,即使这个元素是可变的也不行,这里如果把copy换成是mutableCopy也是解决不了问题的,这个尤其要引起注意。

    如何让内存拷贝彻底发生,即使是一个容器对象内部的元素也是发生内存拷贝的?
    答:自己来实现,我们这里姑且叫做递归深拷贝。

    实现代码:

    @protocol GJRecursiveDeepCopy<NSObject>
     @required
    - (id)gjw_recursiveDeepCopy;
    @end
    
    @interface NSArray(GJRecursiveDeepCopy)<GJRecursiveDeepCopy>
    @end
    
    @interface NSDictionary(GJRecursiveDeepCopy)<GJRecursiveDeepCopy>
    @end
    
    @interface NSSet(GJRecursiveDeepCopy)<GJRecursiveDeepCopy>
    @end
    
    @implementation NSArray(GJRecursiveDeepCopy)
    
    - (id)gjw_recursiveDeepCopy {
        NSMutableArray *copyArr = [NSMutableArray arrayWithCapacity:self.count];
        [self enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            id copyValue;
            if ([obj respondsToSelector:@selector(gjw_recursiveDeepCopy)]) { 
               //Collection对象进行递归拷贝
                copyValue = [obj gjw_recursiveDeepCopy];
            } else if ([obj conformsToProtocol:@protocol(NSMutableCopying)]) { 
                //非Collection对象的NSObject对象
                copyValue = [obj mutableCopy];
            } else if ([obj conformsToProtocol:@protocol(NSCopying)]) { 
                //自定义的NSObject对象,自己实现了拷贝
                copyValue = [obj copy];
            }
            if (copyValue) {
                [copyArr addObject:copyValue];
            }
        }];
        return copyArr;
    }
    
    @end
    
    @implementation NSDictionary(GJRecursiveDeepCopy)
    
    - (id)gjw_recursiveDeepCopy {
        NSMutableDictionary *copyDic = [NSMutableDictionary dictionaryWithCapacity:self.count];
        [self enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            id copyValue;
            if ([obj respondsToSelector:@selector(gjw_recursiveDeepCopy)]) { 
                //Collection对象进行递归拷贝
                copyValue = [obj gjw_recursiveDeepCopy];
            } else if ([obj conformsToProtocol:@protocol(NSMutableCopying)]) { 
                //非Collection对象的NSObject对象
                copyValue = [obj mutableCopy];
            } else if ([obj conformsToProtocol:@protocol(NSCopying)]) { 
                //自定义的NSObject对象,自己实现了拷贝
                copyValue = [obj copy];
            }
            if (copyValue) {
                copyDic[key] = copyValue;
            }
        }];
        return copyDic;
    }
    
    @end
    
    @implementation NSSet(GJRecursiveDeepCopy)
    
    - (id)gjw_recursiveDeepCopy {
        NSMutableSet *copySet = [NSMutableSet setWithCapacity:self.count];
        [self enumerateObjectsUsingBlock:^(id  _Nonnull obj, BOOL * _Nonnull stop) {
            id copyValue;
            if ([obj respondsToSelector:@selector(gjw_recursiveDeepCopy)]) { 
                //Collection对象进行递归拷贝
                copyValue = [obj gjw_recursiveDeepCopy];
            } else if ([obj conformsToProtocol:@protocol(NSMutableCopying)]) { 
                //非Collection对象的NSObject对象
                copyValue = [obj mutableCopy];
            } else if ([obj conformsToProtocol:@protocol(NSCopying)]) { 
                //自定义的NSObject对象,自己实现了拷贝
                copyValue = [obj copy];
            }
            if (copyValue) {
                [copySet addObject:copyValue];
            }
        }];
        return copySet;
    }
    
    @end
    

    文件下载地址

    NSMutableString *mStr = [@"hell" mutableCopy]; //理解了mutableCopy之后,我们最好使用这种方式,也算是一个代码规范
    NSMutableArray *tempArr = [@[mStr] mutableCopy];
    NSLog(@"tempArr:%p", tempArr);
    NSLog(@"tempArr[0]:%p", tempArr[0]);
        
    NSMutableArray *tempArr1 = [tempArr gjw_recursiveDeepCopy];
    NSLog(@"tempArr1:%p", tempArr1);
    NSLog(@"tempArr1[0]:%p", tempArr1[0]);
    
    //tempArr:0x7866bb20
    //tempArr[0]:0x7866bac0
    //tempArr1:0x78772020
    //tempArr1[0]:0x78772350
    

    用gjw_recursiveDeepCopy拷贝后,发现容器里面的元素也发生了内存拷贝。

    后记

    关于深拷贝、浅拷贝的问题,程序猿们理解的都不太一样,这里不必执着于概念本身,理解了原理之后即使没有概念又怎样?

    欢迎大家和我交流沟通,若文章中有错误和纰漏,恳请指正,谢谢,如果感觉评论不方便欢迎微博私信。

    相关文章

      网友评论

      • 小湾子:👍面试被问到这个,表达不出来,都是用代码跟人解释,您这几句话总结的太好了,以后面试不带纸笔了
      • WKCaesar:亮哥遇见问题了, 如下
        #import <Foundation/Foundation.h>

        @interface MyCar : NSObject
        @property (nonatomic,copy) NSMutableString *myStr;
        @EnD

        #import "MyCar.h"
        @Implementation MyCar
        @EnD


        int main() {
        MyCar *car = [[MyCar alloc] init];
        NSMutableString *str1 = [NSMutableString stringWithString:@"123"];
        car.myStr = [str1 mutableCopy];
        NSLog(@"%@,%p:",str1,str1);
        NSLog(@"%@,%p:",car.myStr,car.myStr);

        [car.myStr appendString:@"qwe"]; //程序崩溃
        NSLog(@"%@,%p:",str1,str1);
        NSLog(@"%@,%p:",car.myStr,car.myStr);

        }

        内存确是拷贝了,但是走到这句就死了[car.myStr appendString:@"qwe"];
        2d899c5242bd:@颜如玉_黄金屋 mystr 的实际类型是不可变类型,用set 方法设置的时候调用了编译器生成的set 存方法,该方法会对传入参数执行copy 操作。
      • 小王子sl:你行的 亮子哥
      • 塔罗师_Michael:抢到一个小板凳:smile:

      本文标题:从@property (nonatomic, copy) NSS

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