通过源码分析iOS中的深拷贝与浅拷贝

前言

关于iOS中对象的深拷贝和浅拷贝的文章有很多,但是大部分都是基于打印内存地址来推导结果,这篇文章是从源码的角度来分析深拷贝和浅拷贝。

深拷贝和浅拷贝的概念

拷贝的方式有两种:深拷贝和浅拷贝。

  • 浅拷贝又叫指针拷贝,比如说有一个指针,这个指针指向一个字符串,也就是说这个指针变量的值是这个字符串的地址,那么此时对这个字符串进行指针拷贝的意思就是又创建了一个指针变量,这个指针变量的值是这个字符串的地址,也就是这个字符串的引用计数+1。
  • 深拷贝又叫内容拷贝,比如有一个指针,这个指针指向一个字符串,也就是说这个指针变量的值是这个字符串的地址值,那么此时对这个字符串进行内容拷贝,就会创建一个新的指针,在一个新的地址区域创建一个字符串,这个字符串的值和原字符串的值相同,新的指针指向这个新创建的字符串。这时原字符串的引用计数没有+1。

浅拷贝就是拷贝后,并没有进行真正的复制,而是复制的对象和原对象都指向同一个地址

深拷贝是真正的复制了一份,复制的对象指向了新的地址

从上图可以看出,浅拷贝A指针改变了所指向的内容B指针也指向被修改后的内容。如果有些地方用到B指针,不希望在A指向的内容发生变化时也跟着变化,则需要用到深拷贝。

通俗理解为:浅拷贝好比你的影子,你死了,影子也没了;深拷贝好比克隆人,你死了,它还在。

对象的copy和mutableCopy方法

不管是集合对象还是非集合对象,接收到copy和mutableCopy消息时,都遵循以下准则:

  • copy返回immutable对象
  • mutableCopy返回mutable对象

下面对非集合对象和集合对象的copy和mutableCopy方法进行具体的阐述。

1.非集合类对象的copy和mutableCopy方法

非集合类对象指的是NSString,NSNumber...这些类。下面的例子以NSString类为例。

首先来看immutable对象拷贝的例子:

 NSString *string = @"test";
 NSString *copyString = [string copy];
 NSMutableString *mutableCopyString = [string mutableCopy];

 NSLog(@"%p \n %p \n %p \n", string, copyString, mutableCopyString);

打印结果:

0x101545068
0x101545068
0x60000024e940

通过打印结果我们可以看出来,copyString和string的地址值一样,而mutableCopyString和string的地址值不一样,这就说明imutable对象的copy方法进行了浅拷贝,mutableCopy方法进行了深拷贝。

再来看看mutable对象拷贝的例子:

 NSMutableString *string = [[NSMutableString alloc] initWithString:@"test"];
 NSString *copyString = [string copy];
 NSMutableString *mutableCopyString = [string mutableCopy];

 NSLog(@"%p \n%p \n%p \n", string, copyString, mutableCopyString);

打印结果:

0x600000240e40
0xa000000747365744
0x6000002411a0

通过打印结果可以看出来,copyString和string的内存地址不同,mutableCopyString和string的内存地址也不同。这说明mutable对象的copy方法和mutableCopy方法都进行了深拷贝。

总结起来就是:

immutable对象的copy方法进行了浅拷贝
immutable对象的mutableCopy方法进行了深拷贝
mutable对象的copy方法进行了深拷贝
mutable对象的mutableCopy方法进行了深拷贝。

用代码表示就是:

 [immutableObject copy];//浅拷贝
 [immutableObject mutableCopy];//深拷贝
 [mutableObject copy];//深拷贝
 [mutableObject mutableCopy];//深拷贝

以上是通过打印内存地址得出的结论,下面我们通过查看源码来证实一下我们的结论。

在opensource.apple.com的git仓库中的runtime源码中有NSObject.mm这个文件,在这个文件中有copy和mutableCopy方法的实现:

- (id)copy {
 return [(id)self copyWithZone:nil];
}

- (id)mutableCopy {
 return [(id)self mutableCopyWithZone:nil];
}

我们发现copy和mutableCopy方法只是简单的调用了copyWithZone:和mutableCopyWithZone:两个方法。然后我在searchcode.com中找到了NSString和NSMutableString的Source Code。

NSString.m中,找到了关于copy的方法:

- (id)copyWithZone:(NSZone *)zone {
 if (NSStringClass == Nil)
 NSStringClass = [NSString class];
 return RETAIN(self);
}

- (id)mutableCopyWithZone:(NSZone*)zone {
 return [[NSMutableString allocWithZone:zone] initWithString:self];
}

通过这个源码我们知道了,对于NSString对象,调用copy方法就是调用了copyWithZone:方法。而copyWithZone:方法并没有创建新的对象,而是使指针持有了原来的NSString对象,所以NSString的copy方法是浅拷贝。

而调用mutableCopy方法就是调用了mutableCopyWithZone:方法。从mutableCopyWithZone:的实现我们可以看到,这个方法是创建了一个新的可变的字符串对象。因此NSString的mutableCopy方法是深拷贝。

NSMutableString.m中,只找到了copy和copyWithZone:方法,并没有找到mutableCopyWithZone:方法:

-(id)copy {
 return [[NSString alloc] initWithString:self];
}

-(id)copyWithZone:(NSZone*)zone {
 return [[NSString allocWithZone:zone] initWithString:self];
}

对NSMutableString对象调用copy方法会调用这里的copyWithZone:方法的实现,我们可以看到这里创建了一个新的不可变的字符串。所以对NSMutableString对象执行copy方法是深拷贝。

由于在NSMutableString中没有实现mutableCopyWithZone:方法,所以会调用父类的mutableCopyWithZone:方法,也就是NSString类的mutableCopyWithZone:方法,而我们知道,NSString类的mutableCopyWithZone:方法会创建一个新的可变字符串。所以对NSMutableString对象执行mutableCopy方法是深拷贝。

2.集合对象的copy和mutableCopy

集合对象指的是NSArray,NSDictionary,NSSet等之类的对象。下面以NSArray为例看看immutable对象使用copy和mutableCopy的例子:

 NSArray *array = @[@"1", @"2", @"3"];
 NSArray *copyArray = [array copy];
 NSMutableArray *mutableCopyArray = [array mutableCopy];

 NSLog(@"%p\n%p\n%p", array, copyArray, mutableCopyArray);

打印结果:

0x60400025bed0
0x60400025bed0
0x60400025c2f0

通过打印结果可以看出来,copyArray的地址和array的地址是一样的,说明对array进行copy是进行浅拷贝。而

mutableCopyArray的地址和array的地址是不一样的,说明对array进行mutableCopy是进行了深拷贝。

再来看mutable对象执行copy和mutableCopy的例子:

 NSMutableArray *array = [[NSMutableArray alloc] initWithArray:@[@"1", @"2", @"3"]];
 NSArray *copyArray = [array copy];
 NSMutableArray *mutableCopyArray = [array mutableCopy];

 NSLog(@"%p\n%p\n%p", array, copyArray, mutableCopyArray);

打印结果:

0x604000447440
0x604000447050
0x604000447080

通过打印结果可以看出,copyArray和mutableCopyArray的地址都和array的地址不同,这说明对可变数组进行copy和mutableCopy操作都进行了深拷贝。

因此得出结论:

在集合类对象中,对immutable对象进行copy操作是浅拷贝,进行mutableCopy操作是深拷贝。对mutable对象进行copy操作是深拷贝,进行mutableCopy操作是深拷贝。

用代码表示就是:

 [immutableObject copy];//浅拷贝
 [immutableObject mutableCopy];//深拷贝
 [mutableObject copy];//深拷贝
 [mutableObject mutableCopy];//深拷贝

以上是通过打印内存地址得到的结论,下面我们通过源码来验证一下我们的结论。

NSArray.m中,我找到了copyWithZone:和mutableCopyWithZone:方法。

- (id)copyWithZone:(NSZone *)zone
{
 return RETAIN(self);
}

- (id)mutableCopyWithZone:(NSZone*)zone
{
 if (NSMutableArrayClass == Nil)
 NSMutableArrayClass = [NSMutableArray class];
 return [[NSMutableArrayClass alloc] initWithArray:self];
}

当调用copy方法时,实际上是执行了这里的copyWithZone:方法,在这个方法里面并没有创建新的对象,而只是持有了旧的对象,因此,对于不可变的数组对象,执行copy操作是浅拷贝。

当调用mutableCopy方法时,实际上是执行了这里的mutableCopyWithZone:方法,在这个方法里面,利用原来的数组对象,创建了一个新的可变数组对象,因此对于不可变的数组对象,执行mutableCopy操作是深拷贝。

NSArray.m这个文件的第825行是NSMutableArray的实现。在第875行找到了copyWithZone:的实现,没有找到mutableCopyWithZone:的实现:

- (id)copyWithZone:(NSZone*)zone
{
 if (NSArrayClass == Nil)
 NSArrayClass = [NSArray class];
 return [[NSArrayClass alloc] initWithArray:self copyItems:YES];
}

当调用copy方法时,实际是调用了这里的copyWithZone:方法,在这个方法的实现里,是利用原来的可变数组创建了一个新的不可变数组,因此对可变数组执行copy操作是深拷贝。

当调用mutableCopy时,由于NSMutableArray本身没有实现mutableCopyWithZone:方法,所以会调用父类也就是NSArray类的实现,而通过上面我们也能看到NSArray的实现:利用原数组创建了一个新的可变数组,因此,对可变数组进行mutableCopy操作是深拷贝。

回答经典面试题

面试题:为什么NSString类型的成员变量的修饰属性用copy而不是strong呢?

首先要搞清楚的就是对NSString类型的成员变量用copy修饰和用strong修饰的区别。如果使用了copy修饰符,那么在给成员变量赋值的时候就会对被赋值的对象进行copy操作,然后再赋值给成员变量。如果使用的是strong修饰符,则不会执行copy操作,直接将被赋值的变量赋值给成员变量。

假设有一个NSString类型的成员变量string,对其进行赋值:

 NSString *testString = @"test";
 self.string = testString;

如果该成员变量是用copy修饰的,则等价于:

self.string = [testString copy];

如果是用strong修饰的,则没有copy操作:

self.string = testString;

知道了使用copy和strong的区别后,我们再来分析为什么要使用copy修饰符。先看一段代码:

 NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"test"];
 self.string = mutableString;
 NSLog(@"%@", self.string);
 [mutableString appendString:@"addstring"];
 NSLog(@"%@", self.string);

如果这里成员变量string是用strong修饰的话,打印结果就是:

2018-09-04 10:50:16.909998+0800 copytest[2856:78171] test
2018-09-04 10:50:16.910128+0800 copytest[2856:78171] testaddstring

很显然,当mutableString的值发生了改变后,string的值也随之发生改变,因为self.string = mutableString;这行代码实际上是执行了一次指针拷贝。string的值随mutableString的值的发生改变这显然不是我们想要的结果。

如果成员变量string是用copy修饰,打印结果就是:

2018-09-04 10:58:07.705373+0800 copytest[3024:84066] test
2018-09-04 10:58:07.705496+0800 copytest[3024:84066] test

这是因为使用copy修饰符后,self.string = mutableString;就等价于self.string = [mutableString copy];,也就是进行了一次深拷贝,所以mutableString的值再发生变化就不会影响到string的值。

回答面试题:

NSString类型的成员变量使用copy修饰而不是strong修饰是因为有时候赋给该成员变量的值是NSMutableString类型的,这时候如果修饰符是strong,那成员变量的值就会随着被赋值对象的值的变化而变化。若是用copy修饰,则对NSMutableString类型的值进行了一次深拷贝,成员变量的值就不会随着被赋值对象的值的改变而改变。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • IOS ObjectiveC中的赋值与对象拷贝

    IOS ObjectiveC中的赋值与对象拷贝 在开发过程中我们经常会遇到对象拷贝的问题,下面我们分别讨论赋值操作.对象拷贝.以及浅拷贝(Shallow copy)与深拷贝(Deep copy)的区别与各自的实现方式. 一.不同对象的赋值操作 Objective-C中有两类对象,一类是结构体(或者基本数据类型也算),另一类是NSObject对象. 对于结构体,代码直接会操作其实体,因此赋值操作会创建一个源对象的副本(一个新的对象):而对于NSObject对象,必须使用指针来操作对象,所以其赋值操

  • 浅析iOS中的浅拷贝和深拷贝(copy和mutableCopy)

    ios提供了copy和mutablecopy方法,顾名思义,copy就是复制了一个imutable的对象,而mutablecopy就是复制了一个mutable的对象. copy与retain的区别: copy是创建一个新对象,retain是创建一个指针,引用对象计数加1.Copy属性表示两个对象内容相同,新的对象retain为1 ,与旧有对象的引用计数无关,旧有对象没有变化.copy减少对象对上下文的依赖. retain属性表示两个对象地址相同(建立一个指针,指针拷贝),内容当然相同,这个对象的

  • 详解iOS的深浅拷贝

    前言 OC对象的三种拷贝方式 OC的对象拷贝有如下三种方式,很多时候我们把深复制和完全复制混为一谈,其他他们是有区别的,具体如下 浅复制(shallow copy):在浅复制操作时,对于被复制对象的每一层都是指针复制. 深复制(one-level-deep copy):在深复制操作时,对于被复制对象,至少有一层是深复制. 完全复制(real-deep copy):在完全复制操作时,对于被复制对象的每一层都是对象复制. 两图以避之 理解深复制(mutableCopy) 浅复制很简单,就不演示了,看

  • 通过源码分析iOS中的深拷贝与浅拷贝

    前言 关于iOS中对象的深拷贝和浅拷贝的文章有很多,但是大部分都是基于打印内存地址来推导结果,这篇文章是从源码的角度来分析深拷贝和浅拷贝. 深拷贝和浅拷贝的概念 拷贝的方式有两种:深拷贝和浅拷贝. 浅拷贝又叫指针拷贝,比如说有一个指针,这个指针指向一个字符串,也就是说这个指针变量的值是这个字符串的地址,那么此时对这个字符串进行指针拷贝的意思就是又创建了一个指针变量,这个指针变量的值是这个字符串的地址,也就是这个字符串的引用计数+1. 深拷贝又叫内容拷贝,比如有一个指针,这个指针指向一个字符串,也

  • 通过源码分析Python中的切片赋值

    本文主要介绍的关于Python切片赋值的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: 昨天有同学问了我这么个问题: t = [1, 2, 3] t[1:1] = [7] # 感谢@一往直前 的疑问,之前写为 t[1:1] = 7了 print t # 输出 [1, 7, 2, 3] 这个问题之前还真没遇到过,有谁会对列表这么进行赋值吗?不过对于这个输出结果的原因确实值得去再了解下,毕竟之前也看过<Python源码分析>.(题外话:据说最近有大牛在写新的版本) 想着今天有空看看P

  • 通过源码分析Golang cron的实现原理

    目录 前言 Demo示例 源码实现 结构体 Cron 和 Entry New()实现 AddFunc()实现 Start()实现 Run()实现 Stop()实现 Remove()实现 小结 前言 golang实现定时任务很简单,只须要简单几步代码即可以完成,最近在做了几个定时任务,想研究一下它内部是怎么实现的,所以将源码过了一遍,记录和分享在此.需要的朋友可以参考以下内容,希望对大家有帮助. 关于go cron是如何使用的可以参考之前的文章:一文带你入门Go语言中定时任务库Cron的使用 De

  • 通过源码分析Vue的双向数据绑定详解

    前言 虽然工作中一直使用Vue作为基础库,但是对于其实现机理仅限于道听途说,这样对长期的技术发展很不利.所以最近攻读了其源码的一部分,先把双向数据绑定这一块的内容给整理一下,也算是一种学习的反刍. 本篇文章的Vue源码版本为v2.2.0开发版. Vue源码的整体架构无非是初始化Vue对象,挂载数据data/props等,在不同的时期触发不同的事件钩子,如created() / mounted() / update()等,后面专门整理各个模块的文章.这里先讲双向数据绑定的部分,也是最主要的部分.

  • SpringBoot通过源码探究静态资源的映射规则实现

    我们开发一个Spring Boot项目,肯定要导入许多的静态资源,比如css,js等文件 如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的! 1.静态资源映射规则 1.1.第一种映射规则 SpringBoot中,SpringMVC的web配置都在 WebMvcA

  • 如何通过源码了解Java的自动装箱拆箱详解

    目录 什么叫装箱 & 拆箱? 首先看一段代码 装箱(valueOf()) 为什么要有[-128,127]的缓存? 为什么是[-128,127]? 自动装箱带来的性能问题 小总结 拆箱(intValue) 补充:自动装箱.拆箱总是会发生吗? 总结 什么叫装箱 & 拆箱? 将int基本类型转换为Integer包装类型的过程叫做装箱,反之叫拆箱. 首先看一段代码 public static void main(String[] args) { Integer a = 127, b = 127;

  • Ubuntu18.04通过源码安装Odoo14的教程

    本系列背景介绍 Odoo 是一个基于Python语言构建的开源软件,面向企业应用的CRM,ERP等领域,其目标是对标SAP,Oracle等大型软件提供商,但却通过仅仅一个平台满足企业所有管理的业务需求. 本系列文章针对Odoo 14版,从系统安装,开发环境配置,代码结构,主要功能升级,源码赏析,Anodoo对Odoo的关键扩展等角度,预先给大家介绍即将在2020年发布的这一最新版本. 本篇概述 Odoo14的安装和历史版本差不多,同样也包括安装文件,源码,Docker等多种形式,本文则通过源码方

  • Jwt通过源码揭秘隐藏大坑

    目录 前言 集成JWT 坑在哪里 查看源码探索问题原因 总结 前言 JWT是目前最为流行的接口认证方案之一,有关JWT协议的详细内容,请参考:https://jwt.io/introduction 今天分享一下在使用JWT在项目中遇到的一个问题,主要是一个协议的细节,非常容易被忽略,如果不是自己遇到,或者去看源码的实现,我估计至少80%的人都会栽在这里,下面来还原一下这个问题的过程,由于这个问题出现有一定的概率,不是每次都会出现,所以才容易掉坑里. 集成JWT 在Asp.Net Core中集成J

  • 详解java中的深拷贝和浅拷贝(clone()方法的重写、使用序列化实现真正的深拷贝)

    1.序列化实现 public class CloneUtils { @SuppressWarnings("unchecked") public static <T extends Serializable> T clone(T object){ T cloneObj = null; try { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream obs = new Objec

  • js中区分深拷贝与浅拷贝的实战过程

    目录 一.自我理解 二.数据存储形式 (1)基本数据类型存储于栈中 (2)引用数据类型存储与堆中 三.怎样实现深拷贝? (1)借助JSON对象的parse和stringify (2)手写递归 (3)JQuery中extend方法 总结/注意 总结 一.自我理解 简单来讲就是:深拷贝层层拷贝,浅拷贝只拷贝第一层. 在深拷贝中,新对象中的更改不会影响原对象,而在浅拷贝中,新对象中的更改,原对象中也会跟着改. 在深拷贝中,原对象与新对象不共享相同的属性,而在浅拷贝中,它们具有相同的属性. 举个栗子:存

随机推荐