Object-C 中拷贝到底是怎么一回事

什么是拷贝,拷贝的目的

谈到OC中的拷贝,一般是指copy && mutableCopy,或者有人说浅拷贝 && 深拷贝

谈拷贝之前,先谈一下OC中拷贝的目的

  • OC拷贝的目的:
    拷贝是为了使源对象产生一个副本,跟源对象互不影响:
    1、修改了源对象之后不会影响到副本对象;
    2、修改了副本对象,不会影响到源对象。
    也就是说,克隆出一个“独立的”对象。

什么样的对象可以拷贝

那么什么样的对象可以拷贝呢?NSObject类提供了两个拷贝的方法:

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

- (id)copy;
- (id)mutableCopy;

同时NSObject类也提供了两个协议,遵守协议可以调用协议内的方法:

@protocol NSCopying

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

@end

@protocol NSMutableCopying

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

@end

分两类对象来讨论:

1、一类是系统提供的对象,比如:NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等
2、另一类是自定义的对象

不论是系统提供的对象还是自定义的对象,只要是继承自NSObject类的对象就可以进行拷贝操作。
自定义对象需要遵守NSCopying或NSMutableCopying协议。


copy && mutableCopy

  • 下面以字符串为例来展开说明:
- (void)copyFunction
{
    NSString *originalString = [[NSString alloc] initWithFormat:@"originalString"];
    NSString *copyString = [originalString copy];
    NSString *mutableString_A = [originalString mutableCopy];
    NSMutableString *mutableString_B = [originalString copy];
    
    NSLog(@"地址 originalString:%p, copyString:%p, mutableString_A:%p, mutableString_B:%p",originalString, copyString, mutableString_A, mutableString_B);
}

/*
*  打印结果:
*  地址 originalString:0x102849610, 
*      copyString:0x102849610, 
*      mutableString_A:0x102849df0, 
*      mutableString_B:0x102849610
*/  
- (void)copyFunction
{
    NSString *originalString = [[NSString alloc] initWithFormat:@"originalString"];
    NSString *copyString = [originalString copy];
    NSString *mutableString_A = [originalString mutableCopy];
    NSMutableString *mutableString_B = [originalString copy];
    
    [mutableString_B appendString:@"append"];    
}

/*
运行结果报错:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendString:'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff4d67de65 __exceptionPreprocess + 256
    1   libobjc.A.dylib                     0x00007fff796d9720 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff4d67dc97 +[NSException raise:format:] + 201
    3   CoreFoundation                      0x00007fff4d6bc203 mutateError + 121
    4   Object-C                            0x00000001000014b2 -[Person copyFunction] + 194
    5   Object-C                            0x00000001000015e1 main + 97
    6   libdyld.dylib                       0x00007fff7a7a808d start + 1
    7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
*/

以上两个代码片段有反应出以下几个现象:
1、一个不可变字符串调用copy方法得到的字符串的地址和源字符串地址一致,并且和所接收字符串是否是可变字符串无关。
2、一个不可变字符串调用mutableCopy方法得到的字符串的地址和源字符串不一致,并且和所接收字符串是否是可变字符串无关。
3、第二个代码片段运行报错:Attempt to mutate immutable object with appendString: (通过appendString: 方法来改变不可变对象)

得到结论:

  1. 不可变对象调用copy方法后,源对象和拷贝对象指向同一块内存地址

  2. 不可变对象调用mutableCopy方法后,源对象和拷贝对象分别指向不同的内存地址

下面再看一个代码片段

- (void)copyFunction
{
    NSMutableString *originalString = [[NSMutableString alloc] initWithFormat:@"originalString"];
    NSString *copyString = [originalString copy];
    NSMutableString *mutableString = [originalString mutableCopy];
    
    NSLog(@"地址 originalString:%p, copyString:%p, mutableString:%p",originalString, copyString, mutableString);
}
/*
*  地址 originalString:0x1005352b0, 
       copyString:0x100534b20, 
       mutableString:0x100500700
*/

以上代码片段有反应出以下几个现象:
1、一个可变字符串调用copy方法得到的字符串的地址和源字符串地址不一致
2、一个可变字符串调用mutableCopy方法得到的字符串的地址和源字符串不一致

得到结论:

  1. 可变对象调用copy方法后,源对象和拷贝对象分别指向不同的内存地址

  2. 可变对象调用mutableCopy方法后,源对象和拷贝对象分别指向不同的内存地址

不可变字符串调用 copy && mutableCopy 方法
可变字符串调用 copy && mutableCopy 方法

总结如下:

  1. 不论源对象是可变的还是不可变的,调用copy方法返回的就是一个不可变的副本,调用mutableCopy方法返回的就是一个可变的副本。

  2. 不可变对象调用copy方法后,源对象和拷贝对象指向同一块内存地址

  3. 不可变对象调用mutableCopy方法后,源对象和拷贝对象分别指向不同的内存地址

  4. 可变对象调用copy方法后,源对象和拷贝对象分别指向不同的内存地址

  5. 可变对象调用mutableCopy方法后,源对象和拷贝对象分别指向不同的内存地址

于是引出两个概念:深拷贝 && 浅拷贝

  • 深拷贝 :内容拷贝,产生新的对象
NSString *originalString = [[NSString alloc] initWithFormat:@"originalString"];
NSMutableString *mutableString = [originalString mutableCopy];

mutableString 拷贝了originalString的内容
originalString 和 mutableString 分别指向两个不同的内存地址
originalString 的内容为不可变的
mutableString 的内容为可变的

NSMutableString *originalString = [[NSMutableString alloc] initWithFormat:@"originalString"];
NSString *copyString = [originalString copy];
NSMutableString *mutableString = [originalString mutableCopy];

copyString 和 mutableString 拷贝了 originalString 的内容
originalString 、copyString 和 mutableString 分别指向三个不同的内存地址
originalString 的内容为可变的
copyString 的内容为不可变的
mutableString 的内容为可变的

  • 浅拷贝 :指针拷贝,不产生新的对象
NSString *originalString = [[NSString alloc] initWithFormat:@"originalString"];
NSString *copyString = [originalString copy];

originalString 和 copyString指向同一块内存地址,保存的内容为不可变的

下面再来查看一下 copy 和 mutableCopy 的内存调用情况

- (void)copyFunction
{
    NSString *originalString = [[NSString alloc] initWithFormat:@"originalString"];
    NSLog(@"originalString retain count = %zd",originalString.retainCount); // retainCount = 1
    
    NSString *copyString = [originalString copy];
    NSLog(@"originalString retain count = %zd",originalString.retainCount); // retainCount = 2
    NSLog(@"copyString retain count = %zd",copyString.retainCount); // retainCount = 2

    NSMutableString *mutableCopyString = [originalString mutableCopy];
    NSLog(@"originalString retain count = %zd",originalString.retainCount); // retainCount = 2
    NSLog(@"copyString retain count = %zd",copyString.retainCount); // retainCount = 2
    NSLog(@"mutableCopyString retain count = %zd",mutableCopyString.retainCount); // retainCount = 1

    [mutableCopyString release];
    [copyString release];
    [originalString release];
}
  1. 创建originalString字符串,originalString的引用计数为1
  2. originalString 调用copy方法,拷贝字符串给copyString ,
    originalString 和 copyString 指向同一块内存地址,这块内存有两个指针指向,引用计数为2。originalString和copyString的引用计数都为2。此时 copy 相当于 retain,只是引用计数+ 1
  3. originalString 调用mutableCopy方法,深拷贝字符串给mutableCopyString
    mutableCopyString指向新的一块内存地址,引用计数为1
    originalString 指向的内存地址没有新的指针指向,引用计数仍然为 2

但是可能有人打印retain count = -1,例如下面代码块:

- (void)copyFunction
{
    NSString *originalString = [[NSString alloc] initWithFormat:@"abc"];
    NSLog(@"originalString retain count = %zd",originalString.retainCount); // retainCount = -1
    
    NSString *copyString = [originalString copy];
    NSLog(@"originalString retain count = %zd",originalString.retainCount); // retainCount = -1
    NSLog(@"copyString retain count = %zd",copyString.retainCount); // retainCount = -1

    NSMutableString *mutableCopyString = [originalString mutableCopy];
    NSLog(@"originalString retain count = %zd",originalString.retainCount); // retainCount = -1
    NSLog(@"copyString retain count = %zd",copyString.retainCount); // retainCount = -1
    NSLog(@"mutableCopyString retain count = %zd",mutableCopyString.retainCount); // retainCount = 1

    
    
    [mutableCopyString release];
    [copyString release];
    [originalString release];
    
    NSLog(@"地址 originalString:%p, copyString:%p, mutableString:%p",originalString, copyString, mutableCopyString);
    // 地址 originalString:0x8f08b0bf23c20133, copyString:0x8f08b0bf23c20133, mutableString:0x100508a50
    NSLog(@"类型 originalString:%@, copyString:%@, mutableString:%@",[originalString className], [copyString className], [mutableCopyString className]);
    // 类型 originalString:NSTaggedPointerString, copyString:NSTaggedPointerString, mutableString:__NSCFString
}

对比上面两个代码块,发现除了originalString赋值的内容不一样,其他没有任何改变。但是打印retain count 获得到的结果不同。

originalString 和 copyString 的 retainCount 均为 -1 ,mutableCopyString的retainCount扔为 1。

打印地址和类型发现

  1. originalString 和 copyString 的地址为 :0x8f08b0bf23c20133 mutableCopyString 的地址为:0x100508a50 很明显这两个地址相差很大,应该不是同一类型。

  2. originalString 和 copyString 类型为NSTaggedPointerString,mutableCopyString的类型为__NSCFString

那么 NSTaggedPointerString 是什么类型,为什么会影响到retain count ?

  • Tagged Pointer
  1. 从arm64开始,iOS引入了TaggedPointer技术,用于优化NSNumber、NSData、NSString等小对象的存储

  2. 在没有使用TaggedPointer之前,NSNumber等对象需要动态分配内存,维护引用计数。NSNumber指针存储的是堆中NSNumber对象的地址值

  3. 使用TaggedPointer之后,NSNumber指针里面存储的数据变成了Tag+Data,也就是将数据直接存储在了指针中,这样就不用动态分配内存地址,也不用维护引用计数,节省了之前的调用开销。

  4. 系统会根据NSString的内容长短自动决定是NSTaggedPointerString类型还是__NSCFString类型。由于@"abc"长度可以存储在指针中,所以originalString 和 copyString 类型为NSTaggedPointerString。

详细讲解Tagged Pointer会另起一篇介绍。


NSArray、NSMutableArray、NSDictionary、NSMutableDictionary 调用 copy、mutableCopy的情况是怎样?

NSArray、NSMutableArray、NSDictionary、NSMutableDictionary 调用 copy、mutableCopy 和 NSString 、NSMutableString调用时遵循的规则一致。

类型 copy mutableCopy
NSString NSString(浅拷贝) NSMutableString(深拷贝)
NSMutableString NSString(深拷贝) NSMutableString(深拷贝)
NSArray NSArray(浅拷贝) NSMutableArray(深拷贝)
NSMutableArray NSArray(深拷贝) NSMutableArray(深拷贝)
NSDictionary NSDictionary(浅拷贝) NSMutableDictionary(深拷贝)
NSMutableDictionary NSDictionary(深拷贝) NSMutableDictionary(深拷贝)
  • 自定义对象实现拷贝
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        Person *copyPerson = [person copy];        
    }
    return 0;
}

/*
*  -[Person copyWithZone:]: unrecognized selector sent to instance 0x10051c890
*  *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person copyWithZone:]: unrecognized selector sent to instance 0x10051c890'
*/

如果直接将自定义的对象调用copy方法,会报错,提示Persong类中没有对象方法copyWithZone:

解决方法是,People类遵守NSCopying 或 NSMutableCopying协议,并实现copyWithZone: 或mutableCopyWithZone:方法

@interface Person ()<NSCopying,NSMutableCopying>

@end

@implementation Person

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

- (id)mutableCopyWithZone:(nullable NSZone *)zone
{
    Person *person = [[Person alloc] init];
    person.age = 50;
    return person;
}

@end
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.number = 1;
        
        Person *copyPerson = [person copy];
        copyPerson.age = 20;
        copyPerson.number = 2;
        
        Person *mutableCopyPeople = [person mutableCopy];
        mutableCopyPeople.age = 30;
        mutableCopyPeople.number = 3;
        
        NSLog(@"person age = %d, number = %d",person.age, person.number);
        // person age = 20, number = 2
        NSLog(@"copyPerson age = %d, number = %d",copyPerson.age, copyPerson.number);
        // copyPerson age = 20, number = 2
        NSLog(@"mutableCopyPeople age = %d, number = %d",mutableCopyPeople.age, mutableCopyPeople.number);
        // mutableCopyPeople age = 30, number = 3
        NSLog(@"地址 person : %p, copyPerson : %p, mutableCopyPeople : %p",person, copyPerson, mutableCopyPeople);
        // 地址 person : 0x100631a90, copyPerson : 0x100631a90, mutableCopyPeople : 0x100632020
    }
    return 0;
}
  • 手动实现copy修饰的setter方法
@interface Person ()
@property (nonatomic, copy) NSArray *array;
@end

@implementation Person

- (void)setArray:(NSArray *)array
{
    if (_array != array) {
        [_array release];
        _array = [array copy];
    }
}

@end

但是如果用copy修饰一个可变类型的对象时会出现什么问题?

@interface Person ()
@property (nonatomic, copy) NSMutableArray *mutableArray;
@end

@implementation Person

- (void)addObject {
    self.mutableArray = [NSMutableArray array];
    [self.mutableArray addObject:@"A"];
}

@end
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        [person addObject];        
    }
    return 0;
}
/*
*   -[__NSArray0 addObject:]: unrecognized selector sent to instance 0x100506f60
*  *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x100506f60'
*/

发现报错:NSArray无法调用addObject:方法,为什么会出现这种情况?

因为 mutableArray 是用copy来修饰的,那么
self.mutableArray = [NSMutableArray array]; 相当于:
_mutableArray = [可变数组 copy];
于是 _mutableArray 就变成了一个不可变数组,自然就没有addObject: 方法。

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

推荐阅读更多精彩内容