iOS底层原理 - 内存管理 之 copy

面试题引发的思考:

Q: 谈一谈对copymutableCopy、浅拷贝、深拷贝的理解?

  • copy:不可变拷贝,产生不可变副本;
  • mutableCopy:可变拷贝,产生可变副本;
  • 浅拷贝:指针拷贝,没有产生新的对象;
  • 深拷贝:内容拷贝,产生新的对象。

Q: iOS项目中copy修饰词的使用?

  • copy修饰词 只能声明 不可变类型。
  • 字符串一般用copy修饰,用于UI控件显示的时候不会有问题。
  • 属性不存在mutableCopy修饰词,只有部分Foundation框架自带的一些类可以用mutableCopy修饰。

Q: 自定义的类实现 copy 功能?

  • 遵守 NSCopying 协议;
  • 实现 copyWithZone: 方法。
// TODO: -----------------  Person类  -----------------
// 自定义的类实现 copy 功能需要:1. 遵守 NSCopying 协议
@interface Person : NSObject 
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
// 自定义的类实现 copy 功能需要:2. 实现 copyWithZone: 方法
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    Person *person = [[Person allocWithZone:zone] init];
    person.age = self.age;
    person.name = self.name;
    return person;
}
- (NSString *)description {
    return [NSString stringWithFormat:@"age = %d, weight = %@", self.age, self.name];
}
@end

1. 原理介绍

拷贝的目的:产生一个副本对象,跟源对象互不影响

  • 修改了源对象,不会影响副本对象;
  • 修改了副本对象,不会影响源对象。

iOS提供了2个拷贝方法:

  • copy:不可变拷贝,产生不可变副本;
  • mutableCopy:可变拷贝,产生可变副本。

浅拷贝和深拷贝:

  • 浅拷贝:指针拷贝,没有产生新的对象;
    不可变对象NSStringNSArrayNSDictionary调用copy
  • 深拷贝:内容拷贝,产生新的对象;
    深拷贝:其他情况。

2. 案例分析

(1) copymutableCopy分析

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSString *str1 = [NSString stringWithFormat:@"test"];
        NSString *str2 = [str1 copy]; // 返回的是NSString
        NSMutableString *str3 = [str1 mutableCopy]; // 返回的是NSMutableString

        [str3 appendString:@"ceshi"];

        NSLog(@"%@ %@ %@", str1, str2, str3);
    }
    return 0;
}

// 打印结果
Demo[1234:567890] test test testceshi
// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSMutableString *str1 = [NSMutableString stringWithFormat:@"test"];
        NSString *str2 = [str1 copy]; // 返回的是NSString
        NSMutableString *str3 = [str1 mutableCopy]; // 返回的是NSMutableString

        [str3 appendString:@"ceshi"];

        NSLog(@"%@ %@ %@", str1, str2, str3);
    }
    return 0;
}

// 打印结果
Demo[1234:567890] test test testceshi

由以上代码可知:

  • copy:不可变拷贝,产生不可变副本;
  • mutableCopy:可变拷贝,产生可变副本。

(2) 浅拷贝和深拷贝分析

将环境改为手动内存管理,我们知道:

当调用allocnewcopymutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        // 避免使用Tagged Pointer 技术存储数据,所以赋值长一些
        NSString *str1 = [[NSString alloc] initWithFormat:@"test123456789"];
        NSString *str2 = [str1 copy]; // 浅拷贝:指针拷贝,没有产生新的对象
        NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝:内容拷贝,产生新的对象

        NSLog(@"%@ %@ %@", str1, str2, str3);
        NSLog(@"%p %p %p", str1, str2, str3);

        [str3 release];
        [str2 release];
        [str1 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] test123456789 test123456789 test123456789
Demo[1234:567890] 0x1005b9b60 0x1005b9b60 0x100604a40

由打印结果可知:
str1str2的地址是一样的,说明str1str2是指向的是同一个对象。其指针分析如下:

指针分析
// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test123456789"];
        NSString *str2 = [str1 copy]; // 深拷贝:内容拷贝,产生新的对象
        NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝:内容拷贝,产生新的对象

        [str1 appendFormat:@"111"];
        [str3 appendFormat:@"333"];

        NSLog(@"%@ %@ %@", str1, str2, str3);
        NSLog(@"%p %p %p", str1, str2, str3);

        [str3 release];
        [str2 release];
        [str1 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] test123456789111 test123456789 test123456789333
Demo[1234:567890] 0x10049d9b0 0x10049ded0 0x10049dfd0

由打印结果可知:
str1str2str3的地址都是不一样的,说明str1str2str3是分别指向的是不同的对象。其指针分析如下:

指针分析

(3) 扩展分析

1> 对于数组
// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSArray *array1 = [[NSArray alloc] initWithObjects:@"1", @"2", nil];
        NSArray *array2 = [array1 copy]; // 浅拷贝:指针拷贝,没有产生新的对象
        NSMutableArray *array3 = [array1 mutableCopy]; // 深拷贝:内容拷贝,产生新的对象

        NSLog(@"%p %p %p", array1, array2, array3);

        [array1 release];
        [array2 release];
        [array3 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] 0x10049eca0 0x10049eca0 0x10049ef50

由打印结果可知:
array1array2的地址是一样的,说明array1array2是指向的是同一个对象。

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSMutableArray *array1 = [[NSMutableArray alloc] initWithObjects:@"1", @"2", nil];
        NSArray *array2 = [array1 copy]; // 深拷贝:内容拷贝,产生新的对象
        NSMutableArray *array3 = [array1 mutableCopy]; // 深拷贝:内容拷贝,产生新的对象

        NSLog(@"%p %p %p", array1, array2, array3);

        [array1 release];
        [array2 release];
        [array3 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] 0x10313c020 0x10313c700 0x10313c720

由打印结果可知:
array1array2array3的地址都是不一样的,说明array1array2array3是分别指向的是不同的对象。

2> 对于字典
// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSDictionary *dict1 = [[NSDictionary alloc] initWithObjectsAndKeys:@"jack", @"name", nil];
        NSDictionary *dict2 = [dict1 copy]; // 浅拷贝:指针拷贝,没有产生新的对象
        NSMutableDictionary *dict3 = [dict1 mutableCopy]; // 深拷贝:内容拷贝,产生新的对象

        NSLog(@"%p %p %p", dict1, dict2, dict3);

        [dict1 release];
        [dict2 release];
        [dict3 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] 0x100511810 0x100511810 0x1005118f0

由打印结果可知:
dict1dict2的地址是一样的,说明dict1dict2是指向的是同一个对象。

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSMutableDictionary *dict1 = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"jack", @"name", nil];
        NSDictionary *dict2 = [dict1 copy]; // 深拷贝:内容拷贝,产生新的对象
        NSMutableDictionary *dict3 = [dict1 mutableCopy]; // 深拷贝:内容拷贝,产生新的对象

        NSLog(@"%p %p %p", dict1, dict2, dict3);

        [dict1 release];
        [dict2 release];
        [dict3 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] 0x103132250 0x103132850 0x103132870

由打印结果可知:
dict1dict2dict3的地址都是不一样的,说明dict1dict2dict3是分别指向的是不同的对象。


(4) 总结

根据以上代码及分析可总结如下:

  • copy:不可变拷贝,产生不可变副本;
  • mutableCopy:可变拷贝,产生可变副本;
  • 浅拷贝:指针拷贝,没有产生新的对象;
  • 深拷贝:内容拷贝,产生新的对象;
  • 不可变对象NSStringNSArrayNSDictionary调用copy时是浅拷贝;
  • 其他情况是深拷贝。

总结图例如下:

总结图例

3. 修饰词copy的使用分析

(1) 总结修饰词copyretain的区别

对于retain修饰词修饰:

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
@property(nonatomic, retain) NSArray *dataArray;
@end

@implementation Person
// retain 相当于一下代码
- (void)setDataArray:(NSArray *)dataArray {
    if (_dataArray != dataArray) {
        [_dataArray release];
        _dataArray = [dataArray retain]; // 仅此处有差别
    }
}
- (void)dealloc {
    self.dataArray = nil;
    [super dealloc];
}
@end

对于copy修饰词修饰:

// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
@property(nonatomic, copy) NSArray *dataArray;
@end

@implementation Person
// copy 相当于一下代码
- (void)setDataArray:(NSArray *)dataArray {
    if (_dataArray != dataArray) {
        [_dataArray release];
        _dataArray = [dataArray copy]; // 仅此处有差别
    }
}
- (void)dealloc {
    self.dataArray = nil;
    [super dealloc];
}
@end

因为用copy修饰词修饰的dataArray,会先把旧的_dataArrayrelease,然后调用copy赋值给_dataArray,那么_dataArray就是不可变类型。

得出结论:

copy修饰词只能声明不可变类型。


(2) 案例分析

1> 对于copy修饰词修饰不可变数组NSArray
// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
@property(nonatomic, copy) NSArray *dataArray;
@end

@implementation Person
@end

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];

        person.dataArray = @[@"jack", @"rose"];
        // 相当于 不可变的数组  调用 copy 返回一个 不可变的数组 给 person.dataArray
        // person.dataArray = [@[@"jack", @"rose"] copy];

        NSLog(@"%@", person.dataArray);

        [person release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] (
    jack,
    rose
)

运行正常,没有问题。

2> 对于copy修饰词修饰可变数组NSMutableArray
// TODO: -----------------  Person类  -----------------
@interface Person : NSObject
@property(nonatomic, copy) NSMutableArray *dataArray;
@end

@implementation Person
@end

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Person *person = [[Person alloc] init];

        person.dataArray = [NSMutableArray array];
        // 相当于 可变的数组 赋值给 person.dataArray,然后调用 copy 返回一个 不可变数组

        [person.dataArray addObject:@"jack"];
        [person.dataArray addObject:@"rose"];

        NSLog(@"%@", person.dataArray);

        [person release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] '-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x7fff898d1060'

运行崩溃,提示“方法找不到错误”。

因为可变数组dataArraycopy修饰词修饰,语句person.dataArray = [NSMutableArray array];相当于可变的数组赋值给person.dataArray,然后调用copy返回一个不可变数组,所以不可变数组person.dataArray没有addObject:方法,运行崩溃。


(3) iOS项目中copy修饰词的使用

1> UI控件显示使用copy修饰

对于UITextFieldtext属性内部代码如下:

UITextField属性

我们发现iOS底层代码对于字符串相关的基本上都是使用copy修饰。

具体事例分析如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSMutableString *text = [NSMutableString stringWithFormat:@"123"];

    UITextField *textField;
    // copy 保证 textField.text 赋值的是 text 的副本,不会因为 text 的改变而改变
    textField.text = text;

    // 此处修改 text 不会影响 textField.text 的值
    [text appendString:@"abc"];
}

由以上分析可知:

字符串一般用copy修饰,用于UI控件显示的时候不会有问题。

属性不存在mutableCopy修饰词,因为只有部分Foundation框架自带的一些类可以用mutableCopy修饰,比如以下类:

NSString, NSMutableString;
NSArray, NSMutableArray;
NSDictionary, NSMutableDictionary;
NSData, NSMutableData;
NSSet, NSMutableSet;
等等...
2> 自定义的类实现copy功能
// TODO: -----------------  Person类  -----------------
// 自定义的类实现 copy 功能需要:1. 遵守 NSCopying 协议
@interface Person : NSObject 
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
// 自定义的类实现 copy 功能需要:2. 实现 copyWithZone: 方法
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
    Person *person = [[Person allocWithZone:zone] init];
    person.age = self.age;
    person.name = self.name;
    return person;
}
- (NSString *)description {
    return [NSString stringWithFormat:@"age = %d, weight = %@", self.age, self.name];
}
@end

// TODO: -----------------  main  -----------------
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p1 = [[Person alloc] init];
        p1.age = 20;
        p1.name = @"Jack";

        // p2是一个全新的副本对象,与p1互不干涉
        Person *p2 = [p1 copy];
        p2.age = 30;

        NSLog(@"%@", p1);
        NSLog(@"%@", p2);

        [p2 release];
        [p1 release];
    }
    return 0;
}

// 打印结果
Demo[1234:567890] age = 20, weight = Jack
Demo[1234:567890] age = 30, weight = Jack

自定义的类实现 copy 功能需要:

  • 遵守 NSCopying 协议;
  • 实现 copyWithZone: 方法。

你可能感兴趣的:(iOS底层原理 - 内存管理 之 copy)