从@property (nonatomic, copy) NSString *name;这个细思极恐的代码规范说起

引言

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

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拷贝后,发现容器里面的元素也发生了内存拷贝。

后记

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

欢迎大家和我交流沟通,若文章中有错误和纰漏,恳请指正,谢谢,如果感觉评论不方便欢迎微博私信。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容