iOS copy和mutableCopy

1,572 阅读11分钟

导语:copy从字面意思上来讲就是复制的意思,好比我们平时通过一个文件来复制生成另外一个文件一样,而两个文件互不干扰。其实在oc中,copy的意思也是复制一个对象

copy的特点

  • 修改源对象的属性和行为,不会影响副本对象
  • 修改副本对象的属性和行为,不会影响源对象

oc中NSString、NSArray、NSDictionary的copy和mutableCopy

  • 通过copy创建不可变的副本(NSString,NSArray等)
  • 通过mutableCopy创建可变的副本(NSMutableString,NSMutableArray等)
  • 对象能使用copy的前提是遵循了NSCopying协议,并实现了copyWithZone:方法

NSString中的copy和mutableCopy

  • 不可变的字符串调用copy方法
NSString *str = @"123";
NSString *newStr = [str copy];
NSLog(@"str = %p, newStr = %p", str, newStr);
**2021-11-10 19:14:45.469933+0800 copy的基本使用[15171:1881395] str = 0x100004018, newStr = 0x100004018**
  • 不可变的字符串调用mutableCopy方法
NSString *str = @"123";
NSMutableString *newStr = [str mutableCopy];
NSLog(@"str = %p, newStr = %p", str, newStr);
**2021-11-10 19:18:28.447722+0800 copy的基本使用[15315:1883896] str = 0x100004018, newStr = 0x104804b30**
  • 可变的字符串调用copy方法
NSMutableString *mutableStr = [[NSMutableString alloc]initWithString:@"123"];
NSString *newMutableStr = [mutableStr copy];
NSLog(@"mutableStr = %p, newMutableStr = %p", mutableStr, newMutableStr);
**2021-11-10 19:22:27.857990+0800 copy的基本使用[15480:1887745] mutableStr = 0x1005480a0, newMutableStr = 0xd509abef3c7c50c5**
  • 可变的字符串调用mutableCopy方法
NSMutableString *mutableStr = [[NSMutableString alloc]initWithString:@"123"];
NSMutableString *newMutableStr = [mutableStr mutableCopy];
NSLog(@"mutableStr = %p, newMutableStr = %p", mutableStr, newMutableStr);
**2021-11-10 19:23:50.067708+0800 copy的基本使用[15534:1888793] mutableStr = 0x100598980, newMutableStr = 0x1005989d0**

可以看出只有不可变的字符串进行copy操作的时候才会不生成新的对象,而其他的三种情况都会生成新的对象。这也正好跟copy的特点相吻合,不可变的字符串进行copy操作的时候产品的副本也是不可变的,两者都不可变,所以不生成新的对象也不会造成源对象和副本对象互相影响,其他的三种情况都有可变的对象,所以必须生成一个新的对象,这样才能互不干扰

NSArray中的copy和mutableCopy

NSArray的copy其实跟NSString的copy操作是一样的,只有不可变数组进行copy操作不会生成新的对象,其他的三种情况都会生成新的操作

NSArray *arr = @[@1,@2,@3];

NSArray *newArr = [arr copy];
NSLog(@"arr = %p, newArr = %p", arr, newArr);

NSMutableArray *newMutableArr = [arr mutableCopy];
NSLog(@"arr = %p, newMutableArr = %p", arr, newMutableArr);

NSMutableArray *mutableArr = [NSMutableArray arrayWithObject:@1];

NSArray *newArr1 = [mutableArr copy];
NSLog(@"mutableArr = %p, newArr1 = %p", mutableArr, newArr1);

NSArray *newMutableArr1 = [mutableArr mutableCopy];
NSLog(@"mutableArr = %p, newMutableArr1 = %p", mutableArr, newMutableArr1);

**2021-11-10 19:34:06.700294+0800 copy的基本使用[15884:1893935] arr = 0x1000080a8, newArr = 0x1000080a8**

**2021-11-10 19:34:06.700918+0800 copy的基本使用[15884:1893935] arr = 0x1000080a8, newMutableArr = 0x100744d80**

**2021-11-10 19:34:06.701005+0800 copy的基本使用[15884:1893935] mutableArr = 0x100744c90, newArr1 = 0x100746390**

**2021-11-10 19:34:06.701051+0800 copy的基本使用[15884:1893935] mutableArr = 0x100744c90, newMutableArr1 = 0x100746d00**

NSDictionary中的copy和mutableCopy

NSDictionary中的copy操作也是跟NSString一样的,在这里就不过多举例了

通过上面的例子我们可以引出两个概念:

深拷贝和浅拷贝

深拷贝:拷贝对象本身,在内存中生存另外一个对象 浅拷贝:不拷贝对象本身,仅仅是拷贝对象的指针。在MRC中相单于将对象进行了一次retain操作

图片来源网络

复制方式.png

从这个图中可以看出只有[NSArray copy]、[NSDictionary copy]、[NSString copy]是浅拷贝,其他的操作都是深拷贝。

copy在property(属性中的使用)

在这,我们先思考一个问题,在property(属性)中NSString的用strong还是用copy好?

我们先看下面一个例子:

@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end

NSMutableString *str = [[NSMutableString alloc]initWithString:@"MK"];
Person *person = [[Person alloc]init];
person.name = str;
[str appendString:@" cool"];
NSLog(@"person.name = %@", person.name);
2021-11-10 19:54:11.636787+0800 copy的基本使用[16675:1905943] person.name = MK cool

在上面我们定义了一个Person类,并且里面有一个strong修饰的name属性。在main函数中我们用一个可变的字符串赋值给person实例里面的name变量,当可变的str字符串追加了字符串的时候,我们发现person中的name属性也发生了改变。因为strong修饰符相单于MRC里面的retain修饰符,在MRC只是把引用计数加1,而在strong里面其实就是多了一个指针指向了MK的字符串。如果把strong修饰符改成copy就不会出现这个问题,因为可变的字符串进行copy操作也会生成一个新的对象,相单于世深拷贝。

如下:

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

NSMutableString *str = [[NSMutableString alloc]initWithString:@"MK"];
Person *person = [[Person alloc]init];
person.name = str;
[str appendString:@" cool"];
NSLog(@"person.name = %@", person.name);
2021-11-10 19:54:11.636787+0800 copy的基本使用[16675:1905943] person.name = MK cool

在思考一个问题,可变的字符串(NSMutableString)用strong还是copy呢? 我们先用copy修饰

@interface Person : NSObject

@property (nonatomic, copy) NSMutableString *mutableStr;

@end

NSMutableString *str = [[NSMutableString alloc]initWithString:@"MK"];
Person *person = [[Person alloc]init];
person.mutableStr = str;

[person.mutableStr appendString:@" 123"];

**2021-11-10 21:28:01.543669+0800 copy的基本使用[17899:1930293] -[NSTaggedPointerString appendString:]: unrecognized selector sent to instance 0x21a639271ae3d24f**

当我们用实例中的可变的字符串追加字符的时候,程序会产生崩溃,这是因为,将可变的字符串进行copy的时候产生的是不可变的字符串,实际上person中的mutableStr是不可变的,追加字符的时候会产生崩溃,所以这里用strong比较好点,不会产生崩溃,这个时候用strong也会跟NSString有同样的问题,这里我们只能自己避免。

通过这种方式也可以,直接mutableCopy一个字符串出来赋值给person的mutableStr。

person.mutableStr = [str mutableCopy];

NSArray和NSDictonary同样也有这样的问题

总结:

  • 不可变的NSString、NSArray、NSDictonary用copy好点
  • 可变的NSMutableString、NSMutableArray、NSMutableDictonary用strong比较好点

copy在block中的使用

MRC中block属性修饰符

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, retain) MyBlock myBlock;
@end

Person *person = [[Person alloc]init];
Dog *dog = [[Dog alloc]init];
NSLog(@"retainCount = %ld",[dog retainCount]);
person.myBlock = ^{
    NSLog(@"retainCount = %ld",[dog retainCount]);
    NSLog(@"dog = %@",dog);
};
NSLog(@"retainCount = %ld",[dog retainCount]);
[dog release];
person.myBlock();
[person release];

**2021-11-11 08:57:57.991547+0800 copy的基本使用[26038:2068864] retainCount = 1**
**2021-11-11 08:57:57.992204+0800 copy的基本使用[26038:2068864] retainCount = 1**
**2021-11-11 08:58:37.926729+0800 copy的基本使用[26038:2068864] -[Dog dealloc]**
**2021-11-11 08:58:38.567806+0800 copy的基本使用[26038:2068864] *** -[Dog retainCount]: message sent to deallocated instance 0x100712db0**

当dog对象在使用block之前释放的时候,再次调用block的时候程序会发生崩溃,使用了已经释放的对象。

image.png 可以看出这时候的block存放在栈中。我们把修饰符改成copy

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) MyBlock myBlock;
@end

image.png

这时候的block存放在堆中,再次调用block程序不会发生崩溃

**2021-11-11 09:08:38.115245+0800 copy的基本使用[26435:2076149] retainCount = 1**
**2021-11-11 09:08:38.116298+0800 copy的基本使用[26435:2076149] retainCount = 2**
**2021-11-11 09:09:19.682681+0800 copy的基本使用[26435:2076149] retainCount = 1**
**2021-11-11 09:09:22.305131+0800 copy的基本使用[26435:2076149] dog = <Dog: 0x10050f0a0>**
**2021-11-11 09:09:24.768995+0800 copy的基本使用[26435:2076149] -[Person dealloc]**

不过这时候dog对象好像没有释放,是因为block对其引用计数加了1,导致dog对象没有释放,我们只需要在person对象释放的时候,对block进行释放,block在释放的时候,会给被block引用的对象都发送一个release消息。

@implementation Person
- (void)dealloc {
    NSLog(@"%s", __func__ );
    Block_release(_myBlock);
    [super dealloc];
}
@end

可以得出结论:block在栈中:不会对使用到的对象引用计数加1。而block在堆中会对使用到的对象的引用计算加1,这样防止在调用block之前对象被提前释放导致产生崩溃,所以在MRC中block最好用copy修饰,如果用retain修饰,编译器也会给出警告

image.png

ARC中block属性修饰符

那么在ARC中,我们的block用strong还是copy呢。

先用strong修饰:

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, strong) MyBlock myBlock;
@end

image.png

这时候的block存放在堆中,那么用copy修饰存放在哪里呢?

typedef void(^MyBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) MyBlock myBlock;
@end

image.png

copy修饰的时候block也是存放在堆中。

之前我们需要手动的用copy属性来修饰block,让block从stack拷贝到heap,保证block在出了作用域之后也能够让block继续存在,并且以ARC的方式来决定什么时候释放在heap上的block。在2014年9月后的一次编译器优化之后,如果用strong修饰block,编译器会自动将blcok从stack拷贝到heap上。

所以只要你使用ARC,现在的Clang编译器,你可以像对待其他对象一样对待block。因为block是可变的。这意味着你不需要拷贝他们。

编译器优化了之后,不应该要求使用者将属性改为copy,使用者只要知道被strong过的block也会想普通对象一样被ARC管理就行了。

copy和block的循环引用问题

因为copy修饰的block会被拷贝到堆中,导致对象的引用计数加1,当block中又引用到本身的时候,这时候会造成循环引用的问题,这时候又该怎么解决呢

Person *person = [[Person alloc]init];
person.myBlock = ^{

    NSLog(@"person = %@",person);
};
[person release];
**2021-11-11 09:32:10.484107+0800 copy的基本使用[27410:2097007] person = <Person: 0x100790ab0>**
**Program ended with exit code: 0**

这时候person对象并没有被释放,那么该如何解决呢? 这时候我们只需要在变量的前面加上__block来修饰变量,告诉编译器引用计数不要加1,只时候就可以释放

__block Person *person = [[Person alloc]init];
person.myBlock = ^{
    NSLog(@"person = %@",person);
};
person.myBlock();
[person release];
**2021-11-11 09:34:03.820602+0800 copy的基本使用[27500:2099466] person = <Person: 0x100605460>**
**2021-11-11 09:34:03.825047+0800 copy的基本使用[27500:2099466] -[Person dealloc]**
        

在ARC中,我们通过__weak关键字修饰

Person *person = [[Person alloc]init];

__weak Person *weakPerson = person;
person.myBlock = ^{
    NSLog(@"person = %@",weakPerson);
};
person.myBlock();

自定义类实现copy

自定义的类实现copy需要满足下面几个条件

  • 遵循NSCopying协议
  • 实现NSCopying里面的copyWithZone:方法
NS_ASSUME_NONNULL_BEGIN
typedef void(^MyBlock)(void);

@interface Person : NSObject <NSCopying>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *arr;
@property (nonatomic, strong) MyBlock myBlock;

@end
NS_ASSUME_NONNULL_END
- (id)copyWithZone:(NSZone *)zone {
    //创建对象
    id obj = [[[**self** class] allocWithZone:zone]init];
    //对象赋值
    [obj setName:_name];
    [obj setArr:_arr];
    [obj setMyBlock:_myBlock];
    //返回对象
    return obj;
}

子类进行自定义copy

当父类遵循了NSCopying协议,并且实现了copyWithZone:方法,子类同时也可以调用父类的copy方法,也可以重写方法,来初始化自己独有的属性

- (id)copyWithZone:(NSZone *)zone

{
    //调用父类的copy方法,先初始化父类的属性
    id obj =  [**super** copyWithZone:zone];
    //初始化自己独有的属性
    [obj setAge:_age];
    //返回对象
    return obj;
}

总结:copy方式就是生成一个跟源对象一摸一样的新对象。而且两者不会互相影响