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方法后,源对象和拷贝对象分别指向不同的内存地址

1529889-d2875eb72cfd9eeb.png

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

1529889-fa959d16fe63d07c.png

可变字符串调用 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调用时遵循的规则一致。

类型copymutableCopy
NSStringNSString(浅拷贝)NSMutableString(深拷贝)
NSMutableStringNSString(深拷贝)NSMutableString(深拷贝)
NSArrayNSArray(浅拷贝)NSMutableArray(深拷贝)
NSMutableArrayNSArray(深拷贝)NSMutableArray(深拷贝)
NSDictionaryNSDictionary(浅拷贝)NSMutableDictionary(深拷贝)
NSMutableDictionaryNSDictionary(深拷贝)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: 方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值