OC中的copy

前言

不敢说覆盖OC中所有copy的知识点,但最起码是目前最全的最新的一篇关于 copy的技术文档了。后续发现有新的copy 知识点,我会第一时间更新到这篇博客内。

先提供一下完整的代码链接 下载完整代码 OC_Copy 后续博客中会用到。

本篇博客内容结构图:


NSObjct对象赋值操作在内存中的表现

将这部分拿到开篇来说,是因为如果我们不能准确的区分"对象"和"指针"这2个概念,那么后面关于copy的讨论和思考都是扯淡。

一行“千写万写而不明"的代码

我们来分析一下这行代码
Person *p = [Person alloc] init];

Person p
这句话,仅仅是声明了1个变量而已. 这个变量的类型是 Person

同时p是1个局部的变量, 所以p变量是存储在栈区的
p和* 放到一起 表示 p变量 是一个 指针变量,既然是指针变量,那么p只能存储一个内存地址
本质上来讲 p是1个指针变量 不是1个对象, p 指向的是 某一块内存区域的首地址

[Person alloc] init]
alloc 在堆区开辟一块内存空间,用来存放Person类的一个具体对象。这个对象 运行后面的指针(可以是多个) 来指向它。来一个指针,指向它,且那个指针是 strong 修饰的, 那么 这个对象的引用计数就+1,否者一直是 1 (alloc时候产生的)
init 进行一些初始化操作(如果Person类 是被第一次访问,会进行类加载,类加载详细过程我们后续再谈)

Person *p = [Person alloc] init]
中间这个 “=” 符号 将2段代码连在一起, 充当一个桥梁作用:
第一: 允许 p 指针 指向这个对象占用的内存区域的首地址(可以简洁的理解为 允许p指针指向对象)
第二: 将这个对象地址赋值给p指针
第三:后续所有对 “对象“的操作,都可以通过对p指针进行间接操作来完成。
这里先介绍一个基础,后续,我会拿出一篇博客 专门来说明 OC中的变量在内存中的使用分布。

一图以蔽之


copy 和 mutableCopy

copy 就是浅拷贝,mutableCopy就是深拷贝?

不知道从何时起,流行起这样一句话:copy 就是浅拷贝,mutableCopy就是深拷贝,这句话说的不准确,因为对可变对象调用copy 就是深拷贝,会产生新的对象。 不用代码来求证的结论,永远都是毫无意思的,下面我们会用代码来验证。

任何对象都可以执行 copy和mutableCopy? 看一段官方规定

In Objective-C, the methods copy and mutableCopy are inherited by all objects and intended for performing copies; the latter is for creating a mutable type of the original object. These methods in turn call the copyWithZone and mutableCopyWithZone methods, respectively, to perform the copying. An object must implement the corresponding copyWithZone method to be copyable.

在oc中 copy和mutableCopy两个方法是被所有对象(继承自NSObject的类)继承的,这两个方法就是为copy准备的。其中,mutableCopy是为了创建原始对象的可变类型的copy。这两个方法分别调用copyWithZone和mutableCopyWithZone两个方法来进行copy。一个类必须实现copyWithZone或者mutableCopyWithZone,才能进行copy或者mutableCopy。


shallow copy 和 deep copy

英文好的同学依然是直接点击查看 官方对于深浅copy的定义(传送门)

英文不好的同学,就只能听我唠叨了,我们将官方文档概述如下:

首先看张官方给的图

明白了对象和指针的区别,在看这个图就很容易理解了。

定义

对象拷贝有两种方式:浅复制和深复制。顾名思义,浅复制,并不拷贝对象本身,仅仅是拷贝指向对象的指针;深复制是除了拷贝指向对象的指针,而且直接拷贝整个对象内存到另一块内存中。再简单些说:浅复制就是指针拷贝;深复制就是内容拷贝。

准则

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

对引用计数的影响

浅copy,类似strong,持有原始对象的指针,会使retainCount加一。浅copy和strong引用的区别仅仅是浅copy多执行一步copyWithZone:方法。
深copy,会创建一个新的对象,不会对原始对象的retainCount变化。

关键点

找准源对象类型才能分清copy 是浅拷贝还是深拷贝

使用原则

针对不可变对象调用copy返回该对象本身,调用mutableCopy返回一个可变对象(新的);
针对可变对象调用copy返回一个不可变对象(新的),调用mutableCopy返回另外一个可变对象(新的)。

class copy mutableCopy
不可变(如,NSString) (浅拷贝) 返回本身 (深拷贝)创建新的可变对象(如,创建一个NSMutableString对象,地址跟原对象不同)
可变(如,NSMutableString) (深拷贝) 创建新的不可变对象(如,创建一个NSTaggedPointerString对象,地址跟原对象不同 ) (深拷贝) 创建新的可变对象(如,创建一个NSMutableString对象,地址跟原对象不同 )

明白了上述内容,我们结合代码来看一下具体的shallow copy 和 deep copy


非集合类的对象 shallow copy 和 deep copy

如想查看以下完整的示例代码,请从 OC_Copy 文件夹中打开 OC_Copy1 工程, 下载完整代码 OC_Copy

准则

不管是集合类对象,还是非集合类对象,接收到copy和mutableCopy消息时,都遵循以下准则:
copy返回imutable对象;mutableCopy返回mutable对象;
对copy返回值使用mutable对象接口就会crash

系统非集合类对象指的是 NSString, NSNumber ... 之类的对象。下面先看个非集合类immutable对象拷贝的例子

NSString *string = @"origin";
NSString *stringCopy = [string copy];
NSMutableString *stringMCopy = [string mutableCopy];

通过查看内存,可以看到 stringCopy 和 string 的地址是一样,进行了指针拷贝;而 stringMCopy 的地址和 string 不一样,进行了内容拷贝;

一图以蔽之


再看mutable对象拷贝例子

NSMutableString *string = [NSMutableString stringWithString: @"origin"];
//copy
NSString *stringCopy = [string copy];
NSMutableString *mStringCopy = [string copy];
NSMutableString *stringMCopy = [string mutableCopy];
//change value
[mStringCopy appendString:@"mm"]; //这行代码会crash
[string appendString:@" origion!"];
[stringMCopy appendString:@"!!"];

运行以上代码,会在第7行crash,原因就是 copy 返回的对象是 immutable 对象,违反了我们上述中的原则。 注释第7行后再运行,查看内存,发现 string、stringCopy、stringMCopy 三个对象的内存地址都不一样,说明此时都是做内容拷贝。

一图以蔽之


综上两个例子,我们可以得出结论:

在非集合类对象中:
对immutable对象进行copy操作,是指针复制,mutableCopy操作时内容复制;
对mutable对象进行copy和mutableCopy都是内容复制。

用代码简单表示如下:
[immutableObject copy] // 浅复制 + 不可变对象
[immutableObject mutableCopy] //深复制 + 可变对象
[mutableObject copy] //深复制 + 不可变对象
[mutableObject mutableCopy] //深复制 + 可变对象


集合类的对象自身的 shallow copy 和 deep copy

我着重强调 “对象自身” 是因为这里对象 是集合类型的,它具有存储其它对象的能力, 对象自身 和 对象内的元素 执行的 copy操作是不同的。这里我们先看“对象自身”,后面 我们会讲 “对象内的元素” copy操作。

集合类对象是指NSArray、NSDictionary、NSSet ... 之类的对象。下面先看集合类immutable对象使用copy和mutableCopy的一个例子:

NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];

查看内容,可以看到copyArray和array的地址是一样的,而mCopyArray和array的地址是不同的。说明copy操作进行了指针拷贝,mutableCopy进行了内容拷贝。但需要强调的是:此处的内容拷贝,仅仅是拷贝array这个对象,array集合内部的元素仍然是指针拷贝。

一图以蔽之


这和上面的非集合immutable对象的拷贝还是挺相似的,那么mutable对象的拷贝会不会类似呢?我们继续往下,看mutable对象拷贝的例子:

NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];

查看内存,如我们所料,copyArray、mCopyArray和array的内存地址都不一样,说明copyArray、mCopyArray都对array进行了内容拷贝。同样,我们可以得出结论:

一图以蔽之


在集合类对象中,对immutable对象进行copy,是指针复制,mutableCopy是内容复制;对mutable对象进行copy和mutableCopy都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。用代码简单表示如下:
[immutableObject copy] // 浅复制 + 不可变对象
[immutableObject mutableCopy] //单层深复制 + 可变对象
[mutableObject copy] //单层深复制 + 不可变对象
[mutableObject mutableCopy] //单层深复制 + 可变对象

这个代码结论和非集合类的非常相似。


集合类的对象中元素的 one-level-deep copy 、two-level-deep copy 和 real-deep copy

集合中的元素,我们按照 下面的顺序进行论述:

集合类中对象为Foundation 架构下的

深复制 (one-level-deep copy、two-level-deep copy)
完全复制(real-deep copy)

集合类中对象为自定义对象的

深复制 (one-level-deep copy、two-level-deep copy)
完全复制(real-deep copy)

集合类的对象中的元素为 Foundation 架构下的

如想查看以下完整的示例代码,请从 OC_Copy 文件夹中打开 OC_Copy2 工程, 下载完整代码 OC_Copy

Foundation 架构下的对象 我们以NSString为例,下文中我们所说道浅复制 、深复制、完全复制 都是针对 集合类中的对象而言。

深复制 (单层深copy)

准则

对(集合类) 对象自身执行深copy,对 (集合类)对象内部的元素 执行浅copy,称为集合类的单层深复制。

代码

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *mutableString1 = [NSMutableString stringWithString:@"1"];
    NSMutableString *mutalbeString2 = [NSMutableString stringWithString:@"1"];
    NSMutableArray  *mutableArr     = [NSMutableArray arrayWithObjects:mutalbeString2, nil];
    // mutableString1和mutableArr放入NSArray
    NSMutableArray *testArr = [NSMutableArray arrayWithObjects:mutableString1, mutableArr, nil];
    
    NSArray *testArrCopy = [testArr copy];
    
    NSLog(@"%p", testArr);
    NSLog(@"%p", testArrCopy);
    
    NSLog(@"查看数组内部元素的地址---------------");
    NSLog(@"%p", testArr[0]);
    NSLog(@"%p", testArrCopy[0]);

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

结果分析

bulid程序,查看内存,发现testArr通过[testArr copy]执行深拷贝,数组自身进行了深拷贝,数组内部的元素执行浅拷贝。
2017-03-20 16:38:20.416 ZYYCopyTest2[6668:282496] 数组自身地址0x600000058750
2017-03-20 16:38:20.416 ZYYCopyTest2[6668:282496] 数组自身地址0x600000022a80
2017-03-20 16:38:20.417 ZYYCopyTest2[6668:282496] 查看数组内部元素的地址---------------
2017-03-20 16:38:20.417 ZYYCopyTest2[6668:282496] 0x600000069800
2017-03-20 16:38:20.417 ZYYCopyTest2[6668:282496] 0x600000069800
2017-03-20 16:38:20.417 ZYYCopyTest2[6668:282496] 0x600000058d50
2017-03-20 16:38:20.417 ZYYCopyTest2[6668:282496] 0x600000058d50

一图以蔽之


深复制 (双层深copy)

准则

这里的双层指的NSArray对象一层 和 NSArray容器内对象 的一层
但不能说完全,因为无法处理NSArray中还有一个NSArray这种情况)

代码

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *mutableString1 = [NSMutableString stringWithString:@"1"];
    NSMutableString *mutalbeString2 = [NSMutableString stringWithString:@"1"];
    NSMutableArray  *mutableArr     = [NSMutableArray arrayWithObjects:mutalbeString2, nil];
    // mutableString1和mutableArr放入NSArray
    NSMutableArray *testArr = [NSMutableArray arrayWithObjects:mutableString1, mutableArr, nil];
    
    NSArray *testArrCopy = [[NSArray alloc] initWithArray:testArr copyItems:YES];;
    
    NSLog(@"数组自身地址%p %p", testArr,&testArr);
    NSLog(@"数组自身地址%p", testArrCopy);
    
    NSLog(@"查看数组内部元素的地址---------------");
    NSLog(@"%p", testArr[0]);
    NSLog(@"%p", testArrCopy[0]);

    NSLog(@"%p", testArr[1]);
    NSLog(@"%p", testArrCopy[1]);
    
    NSLog(@"查看数组中包含的数组中的元素的地址---------------");
    // mutableArr中的元素对比,即mutalbeString2对比
    NSLog(@"%p", testArr[1][0]);
    NSLog(@"%p", testArrCopy[1][0]);
}

结果分析

修改第7行代码,bulid程序,查看内存,数组和数组中的元素(非数组的)都进行了深拷贝,但是mutalbeString2的指针地址均没有变化,仍然是浅拷贝。所以这里的 深拷贝不是完全深拷贝 可以理解为 双层深复制
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 数组自身地址0x60800004f750 0x7fff5e938980
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 数组自身地址0x60800003c5e0
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 查看数组内部元素的地址---------------
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 0x60800007d400
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 0xa000000000000311
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 0x60800004f690
2017-03-20 16:48:42.916 ZYYCopyTest2[6886:293351] 0x60800001d6d0
2017-03-20 16:48:42.917 ZYYCopyTest2[6886:293351] 查看数组中包含的数组中的元素的地址---------------
2017-03-20 16:48:42.917 ZYYCopyTest2[6886:293351] 0x60800007d440
2017-03-20 16:48:42.917 ZYYCopyTest2[6886:293351] 0x60800007d440

限制

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

一图以蔽之



完全深复制 (完美copy)

准则

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

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *mutableString1 = [NSMutableString stringWithString:@"1"];
    NSMutableString *mutalbeString2 = [NSMutableString stringWithString:@"1"];
    NSMutableArray  *mutableArr     = [NSMutableArray arrayWithObjects:mutalbeString2, nil];
    // mutableString1和mutableArr放入NSArray
    NSMutableArray *testArr = [NSMutableArray arrayWithObjects:mutableString1, mutableArr, nil];
    
    NSArray *testArrCopy = [NSKeyedUnarchiver unarchiveObjectWithData:
                            [NSKeyedArchiver archivedDataWithRootObject:testArr]];
                            
    
    NSLog(@"数组自身地址%p %p", testArr,&testArr);
    NSLog(@"数组自身地址%p", testArrCopy);
    
    NSLog(@"查看数组内部元素的地址---------------");
    NSLog(@"%p", testArr[0]);
    NSLog(@"%p", testArrCopy[0]);

    NSLog(@"%p", testArr[1]);
    NSLog(@"%p", testArrCopy[1]);
    
    NSLog(@"查看数组中包含的数组中的元素的地址---------------");
    // mutableArr中的元素对比,即mutalbeString2对比
    NSLog(@"%p", testArr[1][0]);
    NSLog(@"%p", testArrCopy[1][0]);
    
    NSLog(@"修改元素---------------");
    testArrCopy[1][0] = @"111";
    NSLog(@"%@", testArr[1][0]);
    NSLog(@"%@", testArrCopy[1][0]);
}

结果分析

修改第7行代码,bulid程序,查看内存,可见完成了完全深复制,testArr和testArrCopy中的元素,以及容器中容器的指针地址完全不同,所以完成了完全深复制。testArr和testArrCopy数组内的元素值修改互不影响。

2017-03-20 17:23:45.953 ZYYCopyTest2[7303:321658] 数组自身地址0x60000005f5c0 0x7fff5cb5f980
2017-03-20 17:23:45.954 ZYYCopyTest2[7303:321658] 数组自身地址0x61000005d940
2017-03-20 17:23:45.954 ZYYCopyTest2[7303:321658] 查看数组内部元素的地址---------------
2017-03-20 17:23:45.954 ZYYCopyTest2[7303:321658] 0x600000261cc0
2017-03-20 17:23:45.954 ZYYCopyTest2[7303:321658] 0x61000007fec0
2017-03-20 17:23:45.954 ZYYCopyTest2[7303:321658] 0x60000005f2f0
2017-03-20 17:23:45.955 ZYYCopyTest2[7303:321658] 0x61000005d910
2017-03-20 17:23:45.955 ZYYCopyTest2[7303:321658] 查看数组中包含的数组中的元素的地址---------------
2017-03-20 17:23:45.955 ZYYCopyTest2[7303:321658] 0x600000261d00
2017-03-20 17:23:45.955 ZYYCopyTest2[7303:321658] 0x61000007ffc0
2017-03-20 17:23:45.955 ZYYCopyTest2[7303:321658] 修改元素---------------
2017-03-20 17:23:45.955 ZYYCopyTest2[7303:321658] 1
2017-03-20 17:23:45.956 ZYYCopyTest2[7303:321658] 111

限制

这里使用归档和解档的前提是NSArray中所有的对象(NSString)都实现了NSCoding协议。(如果是你自定义的对象,你需要单独实现NSCoding协议 )

一图以蔽之


集合类的对象中的元素为 Custom对象

如想查看以下完整的示例代码,请从 OC_Copy 文件夹中打开 OC_Copy3_1 工程,下载完整代码 OC_Copy

深复制 (单层深copy)

准则

1、对比之前的 NSString,我们需要遵守NSCopying, NSMutableCopying,才可以进行copy和mutableCopy 操作
2、集合类对象自身执行了深copy,集合类对象中的元素执行浅copy

首先我们需要 让自定义的类实现NSCopying, NSMutableCopying
代码

@interface Persion : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy) NSString *name;
@end

@implementation Persion

- (id)copyWithZone:(nullable NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    return p;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    return p;
}

@end

- (void)viewDidLoad {
    Persion *p1 = [[Persion alloc] init];
    p1.name = @"persion1";
    NSLog(@"p1:%p,%@",p1, p1);
    Persion *p2 = [p1 copy];
    p2.name = @"persion2";
    NSLog(@"p2:%p,%@",p2, p2);
}

结果分析

由于我们自定义实现的copy 和 mutableCopy 代码完全是一致的,所以这里换成 mutablecopy 也一样,都是执行深拷贝。
2017-03-20 17:33:26.836 ZYYCopyTest2[7462:330502] p1:0x61000001c370,<Persion: 0x61000001c370>
2017-03-20 17:33:26.836 ZYYCopyTest2[7462:330502] p2:0x61800001c650,<Persion: 0x61800001c650>

我们将 自定义的 persion 类 放到数组中,来观察 数组自身和数组内的元素 都执行了哪些copy。

- (void)viewDidLoad {
    NSMutableArray * arr1 = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        Persion *p = [[Persion alloc] init];
        p.name = [NSString stringWithFormat:@"persion%d",i];
        [arr1 addObject:p];
    }
    NSLog(@"arr1: %p, %@", arr1, arr1);
    NSMutableArray *arr2 = [arr1 mutableCopy];
    NSLog(@"arr2: %p, %@", arr2, arr2);
}

结果分析

首先应该知道一点 : “数组中只是存储了对象的地址,而非存储了对象的本体。”
bulid程序,观察内存,发现arr1 和 arr2 执行了深拷贝,数组内部的元素执行的是浅拷贝。 
2017-03-20 17:42:12.121 ZYYCopyTest2[7623:339260] arr1: 0x60800004b0d0, (
    "<Persion: 0x60800000d960>",
    "<Persion: 0x60800000da00>",
    "<Persion: 0x60800000da10>"
)
2017-03-20 17:42:12.122 ZYYCopyTest2[7623:339260] arr2: 0x600000049030, (
    "<Persion: 0x60800000d960>",
    "<Persion: 0x60800000da00>",
    "<Persion: 0x60800000da10>"
)

深复制 (双层深copy)

如想查看以下完整的示例代码,请从 OC_Copy 文件夹中打开 OC_Copy3_2 工程,下载完整代码 OC_Copy

这里我们介绍2种方法

1、既然数组中的每个元素是浅copy,那么我们对数组中的每个元素再执行一次copy操作。
2、使用 系统提供的 initWithArray:copyItems: 方法

方法一 代码

- (void)viewDidLoad {
    NSMutableArray * arr1 = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        Persion *p = [[Persion alloc] init];
        p.name = [NSString stringWithFormat:@"persion%d",i];
        [arr1 addObject:p];
    }
    NSLog(@"arr1: %p, %@", arr1, arr1);
   
    NSMutableArray *arr2 = [NSMutableArray new];
    for (int i = 0; i < arr1.count; i++) {
        Persion *newP = [arr1[i] copy];
        [arr2 addObject:newP];
    }
    NSLog(@"arr2: %p, %@", arr2, arr2);
}

结果分析

arr1和arr2以及各自内部的元素都进行了深拷贝。
2017-03-20 17:47:51.247 ZYYCopyTest2[7718:344280] arr1: 0x608000053cb0, (
    "<Persion: 0x608000018fc0>",
    "<Persion: 0x608000019000>",
    "<Persion: 0x608000019010>"
)
2017-03-20 17:47:51.247 ZYYCopyTest2[7718:344280] arr2: 0x608000053b90, (
    "<Persion: 0x608000018fd0>",
    "<Persion: 0x6080000190c0>",
    "<Persion: 0x6080000190d0>"
)

方法二

- (void)viewDidLoad {
    NSMutableArray * arr1 = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        Persion *p = [[Persion alloc] init];
        p.name = [NSString stringWithFormat:@"persion%d",i];
        [arr1 addObject:p];
    }
    NSLog(@"arr1: %p, %@", arr1, arr1);
   
    NSMutableArray *arr2 = [[NSMutableArray alloc] initWithArray:arr1 copyItems:YES];;
    NSLog(@"arr2: %p, %@", arr2, arr2);
}

结果分析

同第一种方法一样 ,arr1和arr2 以及各自内部的元素都进行了深拷贝。
2017-03-20 17:51:55.997 ZYYCopyTest2[7824:348246] arr1: 0x618000241440, (
    "<Persion: 0x618000010b50>",
    "<Persion: 0x618000010ba0>",
    "<Persion: 0x618000010c40>"
)
2017-03-20 17:51:55.998 ZYYCopyTest2[7824:348246] arr2: 0x61000005faa0, (
    "<Persion: 0x610000010a40>",
    "<Persion: 0x610000010a20>",
    "<Persion: 0x610000010a50>"
)

限制

在使用 initWithArray:copyItems: 方法时候,应该注意传入的参数 必须是遵守 NSCopying, NSMutableCopying协议的对象,否者程序crash

如果我们 自定义的类型A中嵌套了其他自定义的类B的对象,那么 A对象(包含A对象内 非B的元素)都是深copy,但B仍是浅copy。

如想查看以下完整的示例代码,请从 OC_Copy 文件夹中打开 OC_Copy3_3工程, 下载完整代码 OC_Copy

代码如下:

@class Son;

@interface Persion : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) Son *son;
@end

@interface Son : NSObject 
@property (nonatomic, copy) NSString *name;
@end


@implementation Persion

- (id)copyWithZone:(nullable NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    p.son  = self.son;
    return p;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    p.son  = self.son;
    return p;
}

@end

@implementation Son

@end

- (void)viewDidLoad {
    NSMutableArray * arr1 = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        Persion *p = [[Persion alloc] init];
        p.name = [NSString stringWithFormat:@"persion%d",i];
        Son *son = [[Son alloc] init]; // 这里一定要记得初始化
        p.son = son;
        p.son.name = [NSString stringWithFormat:@"son%d",i];
        [arr1 addObject:p];
    }
    NSLog(@"arr1: %p, %@", arr1, arr1);
   
//    NSMutableArray *arr2 = [NSMutableArray new];
//    for (int i = 0; i < arr1.count; i++) {
//        Persion *newP = [arr1[i] copy];
//        [arr2 addObject:newP];
//    }
//    NSLog(@"arr2: %p, %@", arr2, arr2);
    
    NSMutableArray *arr2 = [[NSMutableArray alloc] initWithArray:arr1 copyItems:YES];;
    NSLog(@"arr2: %p, %@", arr2, arr2);
    
    Persion *p2 = arr2[0];
    p2.name = @"new persion";
    p2.son.name = @"new son";

    Persion *p1 = arr1[0];
    NSLog(@"arr1--> p1.name ==%@",p1.name);
    NSLog(@"arr1--> p1.son.name ==%@",p1.son.name);
}

结果分析

上面被注解的创建arr2的代码和下面2行创建arr2的代码 运行结果都是一样的。
bulid程序,查看内存,发现和我们料想的一样,p1.son.name 执行了浅copy
由于 p1.son.name 执行了浅copy 所以 p1.son.name 的值 受到了  p2.son.name 的 影响。 

2017-03-20 18:11:56.977 ZYYCopyTest2[8345:367979] arr1: 0x6180000443e0, (
    "<Persion: 0x618000027300>",
    "<Persion: 0x618000027560>",
    "<Persion: 0x618000027580>"
)
2017-03-20 18:11:56.977 ZYYCopyTest2[8345:367979] arr2: 0x6180000440b0, (
    "<Persion: 0x618000027600>",
    "<Persion: 0x6180000275e0>",
    "<Persion: 0x6180000276c0>"
)
2017-03-20 18:11:56.977 ZYYCopyTest2[8345:367979] arr1--> p1.name ==persion0
2017-03-20 18:11:56.978 ZYYCopyTest2[8345:367979] arr1--> p1.son.name ==new son

完全深复制 (完美copy)

如想查看以下完整的示例代码,请从 OC_Copy 文件夹中打开 OC_Copy3_4工程, 下载完整代码 OC_Copy

代码如下:

@class Son;

@interface Persion : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) Son *son;
@end

@interface Son : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy) NSString *name;
@end

@implementation Persion

- (id)copyWithZone:(nullable NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    p.son  = self.son;
    return p;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    p.son  = self.son;
    return p;
}

@end


@implementation Son

- (id)copyWithZone:(nullable NSZone *)zone {
    Son *s = [[self class] allocWithZone:zone];
    s.name = self.name;
    return s;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Son *s = [[self class] allocWithZone:zone];
    s.name = self.name;
    return s;
}

@end

- (void)viewDidLoad {
    NSMutableArray * arr1 = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        Persion *p = [[Persion alloc] init];
        p.name = [NSString stringWithFormat:@"persion%d",i];
        Son *son = [[Son alloc] init]; // 这里一定要记得初始化
        p.son = son;
        p.son.name = [NSString stringWithFormat:@"son%d",i];
        [arr1 addObject:p];
    }
    NSLog(@"arr1: %p, %@", arr1, arr1);
   
    NSMutableArray *arr2 = [[NSMutableArray alloc] initWithArray:arr1 copyItems:YES];;
    NSLog(@"arr2: %p, %@", arr2, arr2);
    
    Persion *p2 = arr2[0];
    p2.name = @"new persion";
    p2.son.name = @"new son";

    Persion *p1 = arr1[0];
    NSLog(@"arr1--> p1.name ==%@",p1.name);
    NSLog(@"arr1--> p1.son.name ==%@",p1.son.name);
}

结果分析:

bulid程序,观察内存,发现这里 数组自身 和 数组内的元素 都是深拷贝, arr1的 元素值 不受 arr2 元素值的影响。这里就是完美复制。 
2017-03-20 17:59:07.546 ZYYCopyTest2[7987:354996] arr1: 0x608000045490, (
    "<Persion: 0x608000034d40>",
    "<Persion: 0x608000034d60>",
    "<Persion: 0x608000034f00>"
)
2017-03-20 17:59:07.546 ZYYCopyTest2[7987:354996] arr2: 0x610000044770, (
    "<Persion: 0x610000034500>",
    "<Persion: 0x610000034600>",
    "<Persion: 0x610000034700>"
)
2017-03-20 17:59:07.546 ZYYCopyTest2[7987:354996] arr1--> p1.name ==persion0
2017-03-20 17:59:07.546 ZYYCopyTest2[7987:354996] arr1--> p1.son.name ==son0

另外利用归档接档,同样可以达到上述效果。

准则

对于自定义的类,需要遵循NSCoding协议。

代码

@class Son;

@interface Persion : NSObject <NSCopying, NSMutableCopying,NSCoding>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) Son *son;
@end

@interface Son : NSObject <NSCopying, NSMutableCopying,NSCoding>
@property (nonatomic, copy) NSString *name;
@end


static NSString *NameKey = @"NameKey";
static NSString *SonKey  = @"SonKey";

@implementation Persion

// 解码
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        _name = [aDecoder decodeObjectForKey:NameKey];
        _son = [aDecoder decodeObjectForKey:SonKey];
    }
    return self;
}

// 编码
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.name forKey:NameKey];
    [aCoder encodeObject:self.son forKey:SonKey];
}

- (id)copyWithZone:(nullable NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    p.son  = self.son;
    return p;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Persion *p = [[self class] allocWithZone:zone];
    p.name = self.name;
    p.son  = self.son;
    return p;
}

@end


@implementation Son

// 解码
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super init];
    if (self) {
        _name = [aDecoder decodeObjectForKey:@"name"];
    }
    return self;
}

// 编码
- (void)encodeWithCoder:(NSCoder *)aCoder {
    [aCoder encodeObject:self.name forKey:@"name"];
}

- (id)copyWithZone:(nullable NSZone *)zone {
    Son *s = [[self class] allocWithZone:zone];
    s.name = self.name;
    return s;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    Son *s = [[self class] allocWithZone:zone];
    s.name = self.name;
    return s;
}

@end

- (void)viewDidLoad {
    NSMutableArray * arr1 = [NSMutableArray new];
    for (int i = 0; i < 3; i++) {
        Persion *p = [[Persion alloc] init];
        p.name = [NSString stringWithFormat:@"persion%d",i];
        Son *son = [[Son alloc] init]; // 这里一定要记得初始化
        p.son = son;
        p.son.name = [NSString stringWithFormat:@"son%d",i];
        [arr1 addObject:p];
    }
    NSLog(@"arr1: %p, %@", arr1, arr1);
   
    NSMutableArray *arr2 = [NSKeyedUnarchiver unarchiveObjectWithData:
                        [NSKeyedArchiver archivedDataWithRootObject:arr1]];
                            
    NSLog(@"arr2: %p, %@", arr2, arr2);
    
    Persion *p2 = arr2[0];
    p2.name = @"new persion";
    p2.son.name = @"new son";

    Persion *p1 = arr1[0];
    NSLog(@"arr1--> p1.name ==%@",p1.name);
    NSLog(@"arr1--> p1.son.name ==%@",p1.son.name);
}


结果分析

bulid程序,观察内存,也同样实现了完美copy
2017-03-20 18:23:59.312 ZYYCopyTest2[8503:377321] arr1: 0x61800004a650, (
    "<Persion: 0x61800002c6c0>",
    "<Persion: 0x61800002c420>",
    "<Persion: 0x61800002c2a0>"
)
2017-03-20 18:23:59.312 ZYYCopyTest2[8503:377321] arr2: 0x61800004ae30, (
    "<Persion: 0x61800002c660>",
    "<Persion: 0x61800002c560>",
    "<Persion: 0x61800002b100>"
)
2017-03-20 18:23:59.312 ZYYCopyTest2[8503:377321] arr1--> p1.name ==persion0
2017-03-20 18:23:59.312 ZYYCopyTest2[8503:377321] arr1--> p1.son.name ==son0

注意

如果想做到完美copy,我们需要一层一层的实现 “NSCopying, NSMutableCopying, NSCoding”3个协议的方法 ,这样代码量真的好多,后续我们介绍如果利用runtime 来快速进行 归档 接档 自定义类对象。


copy做为 property 修饰关键字

UIView及其父类 并没有像 数组 字典 字符串这些类一样 遵守 NSCopying, NSMutableCopying 协议,下面代码程序会crash。

@interface ViewController : UIViewController
@property (nonatomic, copy) UIView *view; 
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.copyView = [[UIView alloc] init]; 
}

输出结果
ZYYCopyTest[1153:32849] -[UIView copyWithZone:]: unrecognized selector sent to instance 0x7fa9cf8096f0
ZYYCopyTest[1153:32849] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIView copyWithZone:]: unrecognized selector sent to instance 0x7fa9cf8096f0'

非集合类对象 使用copy声明

系统非集合类对象指的是 NSString, NSNumber ... 之类的对象。下面先看个非集合类immutable对象拷贝的例子

@interface ViewController ()
@property(nonatomic, copy) NSString *aCopyStr;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = @"zyy";
    self.aCopyStr = str;
    NSLog(@"str输出:%p,%@\n", str,str);
    NSLog(@"_aCopyStr输出:%p,%@\n", _aCopyStr,_aCopyStr);
}

bulid程序,观察内存,发现_aCopyStr 和 str 的内存地址一样,说明进行了 shallow copy 指针拷贝(浅拷贝)。

再看mutable对象拷贝例子

@interface ViewController ()
@property(nonatomic, copy) NSMutableString *aCopyMStr;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableString *mstrOrigin = [[NSMutableString alloc] initWithString:@"123"];
    self.aCopyMStr = mstrOrigin;
    //[self.aCopyMStr appendString:@"6"];//crash
    NSLog(@"引用计数%@",[mstrOrigin valueForKey:@"retainCount"]);
    [mstrOrigin appendString:@"321"];
    NSLog(@"mstrOrigin输出:%p,%@\n", mstrOrigin,mstrOrigin);
    NSLog(@"aCopyMStr输出:%p,%@\n",  _aCopyMStr,_aCopyMStr);
}

@end

bulid 程序,观察内存,发现mstrOrigin 和 _aCopyMStr 2个对象的的内存地址不一样,说明此刻做的是 (deep copy 深复制 (内容拷贝) 。
mstrOrigin值的变化 不影响 _aCopyMStr
将注解的代码放开,程序会crash。报错说 self.aCopyMStr是 不可变对象, 这就奇怪了,我声明时候明明是 NSMutableString *aCopyMStr , 其实这样因为,在 self.aCopyMStr = mstrOrigin 这一步, aCopyMStr事先用的copy 进行的修饰,程序在执行深拷贝的同时,也返回了一个 不可变对象。

集合类对象 使用copy声明

针对 NSArray、NSDictionary、NSSet等类进行试验出现跟NSString类似的现象,不一一列举,有兴趣可以自己去试验。


copy 和 block 的关系

首先说block的三种类型。

1._NSConcreteGlobalBlock
全局的静态block,不会访问外部的变量。就是说block没有调用其他的外部变量。

2._NSConcreteStackBlock
保存在栈中的 block,当函数返回时会被销毁。这个block就是没有被赋值,并且block访问了外部变量。

3._NSConcreteMallocBlock
保存在堆中的 block,当引用计数为 0 时会被销毁,对_NSConcreteStackBlock 执行copy得来的。

代码举例

- (void)textBlock {
    // __NSGlobalBlock__
    ^{NSLog(@"1111");}
    ();
    
    // __NSStackBlock__
    ^{NSLog(@"%@",self);}
    ();
    
    // __NSMallocBlock__
    // 等号是赋值运算, 相当于对block进行copy, 即相当于自动调用了block的copy方法
    void(^blkStack)(void) = ^{NSLog(@"%d",i);};
    blkStack();
 }

block 使用copy 修饰的原因:

我们知道,函数的声明周期是随着函数调用的结束就终止了。我们的block是写在函数中的。

如果是全局静态block的话,他直到程序结束的时候,才会被被释放。但是我们实际操作中基本上不会使用到不访问外部变量的block。

如果是保存在栈中的block,他会随着函数调用结束被销毁。从而导致我们在执行一个包含block的函数之后,就无法再访问这个block。因为(函数结束,函数栈就销毁了,存在函数里面的block也就没有了),我们再使用block时,就会产生空指针异常。

如果是堆中的block,也就是copy修饰的block。他的生命周期就是随着对象的销毁而结束的。只要对象不销毁,我们就可以调用的到在堆中的block。

这就是为什么我们要用copy来修饰block。因为不用copy修饰的访问外部变量的block,只在他所在的函数被调用的那一瞬间可以使用。之后就消失了。

block 什么时候被自动copy

经过代码验证,得出下面的结论,大家有兴趣的可以自己再验证一下。有错误的地方,欢迎大伙积极留言指出。

作为变量:
一个 block 刚声明的时候是在栈上
赋值给一个普通变量之后就会被 copy 到堆上
赋值给一个 weak 变量不会被 copy

作为属性:

用 strong 和 copy 修饰的属性会被 copy
用 weak 和 assign 修饰的属性不会被 copy

函数传参:
作为参数传入函数 block不会被 copy
作为函数的返回值 block会被 copy

针对 block 做为参数 传入函数 block 不会被copy,这些知名的开源库是这么做的

这里写图片描述
这里写图片描述

关于 block 和copy的关系暂时先到这。


copy英文文献

1、https://developer.apple.com/library/prerelease/content/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html#//apple_ref/doc/uid/TP40010162-SW1
2、https://en.wikipedia.org/wiki/Object_copying


copy装逼小测试

阅读本篇博客之后,建议看一下 iOS中关于Copy的疑问汇总(传送门),如果你都OK,说明你以后可以拿着copy来装逼了。


感谢

最后,感谢感谢大家的阅读,希望对您有所帮助。如果有错误的地方或者不理解的地方,希望大家在评论区积极指出。如果对您有所帮助,希望给作者点个赞,顶一下,您的支持是我最核心的动力。


更多iOS技术交流
请加群: 553633494

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

推荐阅读更多精彩内容

  • 本文为转载: 作者:zyydeveloper 链接:http://www.jianshu.com/p/5f776a...
    Buddha_like阅读 824评论 0 2
  • 什么是copy? copy从字面意思来看就是“复制”、“拷贝”,是一个产生副本的过程。而在OC中,copy是用来复...
    Ashscar阅读 675评论 0 2
  • 眼看就要中秋了 八月啊 水上的木筏 悠然自得的漂流 载满的不是乘客 而是乘客心里的乡愁
    夕木阳阅读 119评论 0 0
  • 李清照 ——致敬女神 寻觅过无数你的样子 可记忆只给我一道风景 你被刻在雪的国度 纵使石头是坚硬的 命运却...
    三只yu阅读 200评论 0 5