Copy和MutableCopy

一、从面向对象到Objective-C概览copy

  • 面向对象:

在面向对象的程序设计中,对象的copy就是创建一个已经存在的对象的copy。这种对象的创建的结果被称为原始对象的copy。copy是很基础的,但是也有其精巧的地方,并且可能造成巨大的消耗。有很多种方式可以copy对象,最常用的就是copy构造器和克隆。copy经常用于对象的修改、移动和保护。如果上述的几种应用都不需要,持有原始对象的引用就足够了,并不需要copy。

  • OC:

在OC中,copy和mutableCopy两个方法是被所有对象继承的(有点小毛病,应该指所有继承自NSObject的类),这两个方法就是为copy准备的。其中,mutableCopy是为了创建原始对象的可变类型的copy。这两个方法分别调用copyWithZone和mutableCopyWithZone两个方法来进行copy。一个类必须实现copyWithZone或者mutableCopyWithZone,才能进行copy或者mutableCopy。
那么,我们可以从以上获取到什么信息?

copy经常用于对象的修改、移动和保护。如果上述的几种应用都不需要,持有原始对象的引用就足够了,并不需要copy。

一个类必须实现copyWithZone或者mutableCopyWithZone,才能进行copy或者mutableCopy。

下一阶段,本文将展开讲述OC中的copy相关信息以及如何使用copy方法。

二、Objective-C中copy相关

1、OC中的copy相关内容

  • 在XCode 里Foundation.framework下的Headers里,也在系统里找到原文件:/System/Library/Frameworks/Foundation.framework/Versions/C/Headers/NSObject.h
@protocol NSCopying

- (id)copyWithZone:(nullable NSZone *)zone;

@end

@protocol NSMutableCopying

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

@end
  • 在/usr/include/objc 下面找到 runtime 的 NSObject.h

- (id)copy;

- (id)mutableCopy;

  • 修饰属性的关键字copy

2、这里需要注意的有以下几点

  • 若想使用copy和mutableCopy,需要分别实现NSCopying协议和NSMutableCopying协议,即实现copyWithZone:和mutableCopyWithZone:方法。

  • 继承自NSObject的大部分框架类均默认实现了NSCopying,并且一些具备可变类型的类如NSString、NSArray、NSDictionary,以及它们的可变类型类NSMutableString、NSMutableArray和NSMutableDictionary也实现了NSMutableCopying。(查了大部分常用类,均实现了NSCopying,所以暂时这么说吧,可能有人说NSNumber并没有实现NSCopying,那你可以看一下它的父类NSValue,其实现了NSCopying)

  • 对于一些自定义类,需要自己实现NSCopying。具体方式且看下部分。

三、非容器对象的深浅copy

首先,我们谈一下非容器对象的深浅copy,这些非容器对象,包含常用的NSString、NSNumber等,也包括我们自定义的一些非容器类的实例。下面分三个三面进行分析。

1、首先说说深浅copy

准则

浅copy:指针复制,不会创建一个新的对象。

深copy:内容复制,会创建一个新的对象。

2、框架类的深浅copy

准则

探究框架类深copy还是浅copy,需要清楚的是该类如何实现的NSCopying和NSMutableCopy的两个方法copyWithZone:和mutableCopyWithZone:。然而OC并不开源,并且本文这里也不会进行源码的推测。

那么,我们应该遵循怎样一个原则呢?如下:

对immutableObject,即不可变对象,执行copy,会得到不可变对象,并且是浅copy。

对immutableObject,即不可变对象,执行mutableCopy,会得到可变对象,并且是深copy。

对mutableObject,即可变对象,执行copy,会得到不可变对象,并且是深copy。

对mutableObject,即可变对象,执行mutableCopy,会得到可变对象,并且是深copy。
代码

// 此处以NSString为例探究框架类深浅copy

// 不可变对象

NSString *str = @"1";

NSString *str1 = [str copy];

NSString *str2 = [str mutableCopy];

// 可变对象

NSMutableString *mutableStr = [NSMutableString stringWithString:@"1"];

NSMutableString *mutableStr1 = [mutableStr copy];

NSMutableString *mutableStr2 = [mutableStr mutableCopy];

// 打印对象的指针来确认是否创建了一个新的对象

// 不可变对象原始指针

NSLog(@"%p", str);

// 不可变对象copy后指针

NSLog(@"%p", str1);

// 不可变对象mutalbeCopy后指针

NSLog(@"%p", str2);

// 可变对象原始指针

NSLog(@"%p", mutableStr);

// 可变对象copy后指针

NSLog(@"%p", mutableStr1);

// 可变对象mutalbeCopy后指针

NSLog(@"%p", mutableStr2);

结果分析

// 此处依次对应上述6个log,可见与前面所讲的原则吻合(此处不验证可变类型和不可变类型,默认上述原则正确即可)。

2016-10-21 10:50:52.879 Memory[67680:5623387] 0x10d85a1b0

2016-10-21 10:50:52.879 Memory[67680:5623387] 0x10d85a1b0

2016-10-21 10:50:52.879 Memory[67680:5623387] 0x60800007a080

2016-10-21 10:50:52.879 Memory[67680:5623387] 0x60800007a9c0

2016-10-21 10:50:52.880 Memory[67680:5623387] 0xa000000000000311

2016-10-21 10:50:52.880 Memory[67680:5623387] 0x60800007a900

3、自定义类的深浅copy

准则

对于一个我们自定义的类型,显然比框架类容易操纵的多。此处就拿NSCopying举例(因为从没有自定义过具有可变类型的类,当然,如果有需要的话,也可以实现NSMutableCopying)。自定义的类就和2中的原则没有半毛钱关系了,一切就看你怎么实现NSCopying协议中的copyWithZone:方法。
代码

// Model定义,copyWithZone第一种实现(浅copy)

@interface Model1 : NSObject
@property (nonatomic, assign) NSInteger a;
@end

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

// Model定义,copyWithZone第二种实现(深copy)

@interface Model1 : NSObject
@property (nonatomic, assign) NSInteger a;
@end

@implementation Model1

- (id)copyWithZone:(NSZone *)zone
{
    Model1 *model = [[Model1 allocWithZone:zone] init];
    model.a = self.a;
    return model;
}
@end

// 分别选择上述两种model进行指针打印。

Model1 *model = [[Model1 alloc] init];

Model1 *copyModel = [model copy];

NSLog(@"%p", model);

NSLog(@"%p", copyModel);

结果分析

// 对应上述一,可见实现了浅copy

2016-10-21 11:12:03.149 Memory[67723:5636292] 0x60000000c9d0

2016-10-21 11:12:03.149 Memory[67723:5636292] 0x60000000c9d0

// 对应上述二,可见实现了深copy

2016-10-21 11:16:46.803 Memory[67752:5640133] 0x60800001df00

2016-10-21 11:16:46.803 Memory[67752:5640133] 0x60800001def0

四、容器对象的深浅copy

前文已经知道了深浅copy的区别,你也大致猜到了为什么将容器对象拿出来作为一块。对,因为容器中可能包含很多对象,而这些对象也需要区分深浅copy。往深里说,容器中可能包含容器对象,那更是麻烦了。不要急,看下面,以NSArray的深浅copy为例,将容器的深浅copy分为四种。

1、浅copy

准则

容器的浅copy,符合三.2中的原则。
代码

// 和NSString浅copy的验证步骤一样

NSArray *arr = [NSArray arrayWithObjects:@"1", nil];

NSArray *copyArr = [arr copy];

NSLog(@"%p", arr);

NSLog(@"%p", copyArr);

结果分析

// 无疑是浅copy(你可能会问,为什么不看一下arr和copyArr内部元素的指针对比?这里并没有必要,最外层对象都没有创建新的,里面不用验证)

2016-10-21 11:27:57.554 Memory[67778:5646253] 0x600000010690

2016-10-21 11:27:57.554 Memory[67778:5646253] 0x600000010690

2、单层深copy

准则

容器的单层深copy,符合三.2中的原则(只是深copy变成了单层深copy)。这里的单层指的是完成了NSArray对象的深copy,而未对其容器内对象进行处理。
代码

NSArray *arr = [NSArray arrayWithObjects:@"1", nil];

NSArray *copyArr = [arr mutableCopy];

NSLog(@"%p", arr);

NSLog(@"%p", copyArr);

// 打印arr、copyArr内部元素进行对比

NSLog(@"%p", arr[0]);

NSLog(@"%p", copyArr[0]);

结果分析

// 可发现前两项地址不同,即完成深copy,但是后两项相同,这代表容器内部的元素并没有完成深copy,所有称之为单层深copy

2016-10-21 11:32:27.157 Memory[67801:5649757] 0x6000000030d0

2016-10-21 11:32:27.157 Memory[67801:5649757] 0x600000242e50

2016-10-21 11:32:27.157 Memory[67801:5649757] 0x10dd811b0

2016-10-21 11:32:27.157 Memory[67801:5649757] 0x10dd811b0

3、双层深copy

准则

容器的双层深copy已经脱离了三.2中的原则。这里的双层指的是完成了NSArray对象和NSArray容器内对象的深copy(为什么不说完全,是因为无法处理NSArray中还有一个NSArray这种情况)。
代码

// 随意创建一个NSMutableString对象

NSMutableString *mutableString = [NSMutableString stringWithString:@"1"];

// 随意创建一个包涵NSMutableString的NSMutableArray对象

NSMutableString *mutalbeString1 = [NSMutableString stringWithString:@"1"];

NSMutableArray *mutableArr = [NSMutableArray arrayWithObjects:mutalbeString1, nil];

// 将mutableString和mutableArr放入一个新的NSArray中

NSArray *testArr = [NSArray arrayWithObjects:mutableString, mutableArr, nil];

// 通过官方文档提供的方式创建copy

NSArray *testArrCopy = [[NSArray alloc] initWithArray:testArr copyItems:YES];

// testArr和testArrCopy指针对比

NSLog(@"%p", testArr);

NSLog(@"%p", testArrCopy);

// testArr和testArrCopy中元素指针对比

// mutableString对比

NSLog(@"%p", testArr[0]);

NSLog(@"%p", testArrCopy[0]);

// mutableArr对比

NSLog(@"%p", testArr[1]);

NSLog(@"%p", testArrCopy[1]);

// mutableArr中的元素对比,即mutalbeString1对比

NSLog(@"%p", testArr[1][0]);

NSLog(@"%p", testArrCopy[1][0]);

结果分析

// 这里可以发现,copy后,只有mutableArr中的mutalbeString1指针地址没有变化。而testArr的指针和testArr中的mutableArr、mutableString的指针地址均发生变化。所以称之为双层深复制。

2016-10-21 12:03:15.549 Memory[67855:5668888] 0x60800003c7a0

2016-10-21 12:03:15.549 Memory[67855:5668888] 0x60800003c880

2016-10-21 12:03:15.549 Memory[67855:5668888] 0x608000260540

2016-10-21 12:03:15.550 Memory[67855:5668888] 0xa000000000000311

2016-10-21 12:03:15.550 Memory[67855:5668888] 0x60800005d610

2016-10-21 12:03:15.550 Memory[67855:5668888] 0x60800000d2e0

2016-10-21 12:03:15.550 Memory[67855:5668888] 0x608000260980

2016-10-21 12:03:15.550 Memory[67855:5668888] 0x608000260980

限制

initWithArray: copyItems:会使NSArray中元素均执行copy方法。这也是我在testArr中放入NSMutableArray和NSMutableString的原因。如果我放入的是NSArray或者NSString,执行copy后,只会发生指针复制;如果我放入的是未实现NSCopying协议的对象,调用这个方法甚至会crash。这里,官方文档的描述有误。

4、完全深copy

准则

如果想完美的解决NSArray嵌套NSArray这种情形,可以使用归档、解档的方式。
代码

// 随意创建一个NSMutableString对象

NSMutableString *mutableString = [NSMutableString stringWithString:@"1"];

// 随意创建一个包涵NSMutableString的NSMutableArray对象

NSMutableString *mutalbeString1 = [NSMutableString stringWithString:@"1"];
NSMutableArray *mutableArr = [NSMutableArray arrayWithObjects:mutalbeString1, nil];

// 将mutableString和mutableArr放入一个新的NSArray中

NSArray *testArr = [NSArray arrayWithObjects:mutableString, mutableArr, nil];

// 通过归档、解档方式创建copy

NSArray *testArrCopy = [NSKeyedUnarchiver unarchiveObjectWithData:
[NSKeyedArchiver archivedDataWithRootObject:testArr]];;

// testArr和testArrCopy指针对比

NSLog(@"%p", testArr);
NSLog(@"%p", testArrCopy);

// testArr和testArrCopy中元素指针对比

// mutableString对比

NSLog(@"%p", testArr[0]);
NSLog(@"%p", testArrCopy[0]);

// mutableArr对比

NSLog(@"%p", testArr[1]);
NSLog(@"%p", testArrCopy[1]);

// mutableArr中的元素对比,即mutalbeString1对比

NSLog(@"%p", testArr[1][0]);
NSLog(@"%p", testArrCopy[1][0]);

结果分析

// 可见完成了完全深复制,testArr和testArrCopy中的元素,以及容器中容器的指针地址完全不同,所以完成了完全深复制。

2016-10-21 12:19:34.022 Memory[67887:5677318] 0x60800002db00

2016-10-21 12:19:34.022 Memory[67887:5677318] 0x60800002dc20

2016-10-21 12:19:34.022 Memory[67887:5677318] 0x608000260400

2016-10-21 12:19:34.023 Memory[67887:5677318] 0x6080002603c0

2016-10-21 12:19:34.023 Memory[67887:5677318] 0x608000051d90

2016-10-21 12:19:34.023 Memory[67887:5677318] 0x6080000521e0

2016-10-21 12:19:34.023 Memory[67887:5677318] 0x608000260600

2016-10-21 12:19:34.023 Memory[67887:5677318] 0x6080002606c0

限制

归档和解档的前提是NSArray中所有的对象都实现了NSCoding协议。

五、拾遗

1、关键字copy

代码与结果

// 首先分别给出copy和strong修饰的属性,以NSString举例

// 1、strong

@property (nonatomic, strong) NSString *str;

// 2、copy

@property (nonatomic, copy) NSString *str;

// 分别对1和2执行下述代码

NSMutableString *mutableStr = [NSMutableString stringWithFormat:@"123"];

self.str = mutableStr;

[mutableStr appendString:@"456"];

NSLog(@"%@", self.str);

NSLog(@"%p", self.str);

NSLog(@"%@", mutableStr);

NSLog(@"%p", mutableStr);

// 结果1

2016-10-21 14:08:46.657 Memory[68242:5714288] 123456

2016-10-21 14:08:46.657 Memory[68242:5714288] 0x608000071040

2016-10-21 14:08:46.657 Memory[68242:5714288] 123456

2016-10-21 14:08:46.657 Memory[68242:5714288] 0x608000071040

// 结果2

2016-10-21 14:11:16.879 Memory[68264:5716282] 123

2016-10-21 14:11:16.880 Memory[68264:5716282] 0xa000000003332313

2016-10-21 14:11:16.880 Memory[68264:5716282] 123456

2016-10-21 14:11:16.880 Memory[68264:5716282] 0x60000007bbc0

分析

结果1为strong修饰的结果,可见 [mutableStr appendString:@"456"]修改mutableStr造成了self.str的改变,显然不安全;结果2为copy修饰的结果,可见 [mutableStr appendString:@"456"]修改mutableStr未造成self.str的改变,显然安全。(从内存地址的变化也可以看出来)

这里可以推测出,copy关键字是在str属性的set方法里面返回了mutableStr的copy,而strong关键字仅仅是返回了mutableStr。

2、深浅copy对引用计数的影响

浅copy,类似strong,持有原始对象的指针,会使retainCount加一。

深copy,会创建一个新的对象,不会对原始对象的retainCount变化。

// 也许你会疑问arc下如何访问retainCount属性,这里提供了两种方式(下面代码中a代表一个任意对象,这个对象最好不要是NSString和NSNumber,因为用它们进行测试会出问题)

// kvc方式

NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)a));

// 桥接字方式

NSLog(@"Retain count %@", [a valueForKey:@"retainCount"]);

3、可变和不可变

可变和不可变上文谈的不是很多,因为本文认为这完全与NSCopying和NSMutableCopying的实现息息相关。当然,对于框架类,我们可以简单的认为,copy方法返回的就是不可变对象,mutableCopy返回的就是可变对象。如果是自定义的类,就看你怎么实现NSCopying和NSMutableCopying协议了。

4、copy和block

首先,MRR时代用retain修饰block会产生崩溃,因为作为属性的block在初始化时是被存放在栈区或静态区的,如果栈区的block内调用外部变量,那么block无法保留其内存,在初始化的作用域内使用并不会有什么影响,但一旦出了block的初始化作用域,就会引起崩溃。所有MRC中使用copy修饰,将block拷贝到堆上。

其次,在ARC时代,因为ARC自动完成了对block的copy,所以修饰block用copy和strong都无所谓。

5、strong和shallowCopy

这个问题困惑了很久,最后只能得出一个结论,浅copy和strong引用的区别仅仅是浅copy多执行一步copyWithZone:方法。

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

推荐阅读更多精彩内容