iOS-内存管理4-Copy

一. copy(不可变拷贝)、mutableCopy(可变拷贝)

copy就是拷贝, 拷贝的目的:产生一个副本对象,跟源对象互不影响
修改了源对象,不会影响副本对象,修改了副本对象,不会影响源对象。

iOS提供了两个拷贝方法:

  1. copy,不可变拷贝。不管原来是可变还是不可变,copy之后产生的都是不可变副本。
  2. mutableCopy,可变拷贝。不管原来是可变还是不可变,mutableCopy之后产生的都是可变副本。

我们都知道:当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它。
现在你应该明白了,拷贝会产生一个新的副本,就是一个新的对象,所以在不需要这个对象时,就要调用release或者autorelease来释放它。

二. 深拷贝和浅拷贝

  1. 深拷贝:内容拷贝,产生新的对象
  2. 浅拷贝:指针拷贝,没有产生新的对象
  3. 调用copy、mutableCopy后到底是深拷贝还是浅拷贝,系统说了算,只要达到产生一个副本对象,并且副本对象和源对象互不影响的目的就可以。

在MRC环境下,str1是不可变字符串,运行如下代码:

void test2()
{
    NSString *str1 = [[NSString alloc] initWithFormat:@"test"];
    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];
}

打印:

test test test
0xaaf541fbfb3f44e9  0xaaf541fbfb3f44e9  0x100667a20

可以发现,str1和str2内存地址一模一样,str3内存地址不一样,我们可以画出内存图:

内存图.png

现在想一下,拷贝的目的是产生一个副本对象,并且副本对象和源对象互不影响。

  1. 当不可变的str1调用copy,本来str1就是不可变的,要变成不可变的str2,既然都是不可变的,那就谈不上影响这回事,如果重新创建一个一模一样不可变的对象岂不是浪费,所以就干脆变成指针拷贝了(也就是浅拷贝),这样也达到了拷贝的目的。
  2. 当不可变的str1调用mutableCopy,需要从不可变的str1变成可变的str3,一个不可变,一个可变,为了达到副本对象和源对象互不影响的目的,这里只能使用深拷贝了。
  3. 上面的str1和str2指向同一个对象,所以[str1 release]和[str2 release]效果都是一样的,他们都是让这个对象的引用计数器减一,两次release之后,引用计数器为0,对象被释放,这样也合情合理。

同理,如果str1是可变字符串呢?

void test3()
{
    NSMutableString *str1 = [[NSMutableString alloc] initWithFormat:@"test"];
    NSString *str2 = [str1 copy]; // 深拷贝
    NSMutableString *str3 = [str1 mutableCopy]; // 深拷贝
    
    NSLog(@"%@ %@ %@", str1, str2, str3);
    NSLog(@"%p %p %p", str1, str2, str3);

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

打印:

test test test
0x1005182b0  0x7656f0fdae5b70cb  0x100518390

可以看出,上面都是深拷贝。这个也很容易理解,刚开始str1已经是可变字符串了,为了达到拷贝后副本对象和源对象互不影响的目的,就不能指针拷贝了,所以这里都是深拷贝,内存图如下:

内存图.png

同理,我们可以用代码验证NSArray、NSDictionary拷贝之后的情况,总结如下图,验证代码可见文末Demo。

深拷贝、浅拷贝.png

总结:

拷贝的目的就是产生一个新的副本,并且副本对象和源对象互不影响。
为了达到这个目的并且尽量不占用没必要的内存,当调用copy、mutableCopy方法时,系统会自动决定是深拷贝还是浅拷贝(当从不可变到不可变,既然大家都是不可变,那么就直接指针拷贝得了,还省内存,其他情况的拷贝只要有可变的,为了拷贝之后互不影响只能深拷贝了)。

三. copy修饰属性

如果使用copy修饰属性:

@property (copy, nonatomic) NSArray *data;

那这个属性的setter方法就是这样的:

#import "MJPerson.h"

@implementation MJPerson

- (void)setData:(NSArray *)data
{
    if (_data != data) {
        [_data release];
        _data = [data copy];
    }
}

- (void)dealloc
{
    self.data = nil;
    [super dealloc];
}
@end

运行如下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *p = [[MJPerson alloc] init];
        
        p.data = @[@"jack",@"rose"];
        
        [p release];
    }
    return 0;
}

当调用p.data = @[@"jack",@"rose"],就是调用setData:方法,这时候就把传进来的数组进行copy操作,从不可变数组到不可变数组,所以这里是浅拷贝,外面传进来的对象和里面指向的对象都是同一个对象。

如果将上面的不可变数组换成可变数组:

@property (copy, nonatomic) NSMutableArray *data;

执行如下代码:


#import <Foundation/Foundation.h>
#import "MJPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *p = [[MJPerson alloc] init];
        
        p.data = [NSMutableArray array];
        [p.data addObject:@"jack"]; //报错 
        [p.data addObject:@"rose"];
        
        [p release];
    }
    return 0;
}

发现会报错:

-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x100505750

原因很简单,因为我们使用的是copy修饰,把一个可变数组传进去,copy之后就变成了不可变数组,给不可变数组添加元素当然会报如上错误啦!

总结:

所以,为了防止上面的错误,一般使用copy修饰,右边都放的是不可变对象,以防出现不可预料的错误,如下:

@property (copy, nonatomic) NSString *str;
@property (copy, nonatomic) NSArray *data;

注意:属性的修饰只有copy,不存在什么mutableCopy。

四. 为什么NSString使用copy

观察系统的属性,你会发现,系统的字符串属性都是使用copy:

比如UITextField的text属性
@property(nullable, nonatomic,copy) NSString *text; // default is nil

那这个属性的setter方法就是这样的:

- (void)setText:(NSString *)text
{
    if (_text != text) {
        [_text release];
        _text = [text copy];
    }
}

- (void)dealloc
{
    self.text = nil;
    [super dealloc];
}

思考一下为什么这么设计?

使用copy之后,不管你外面传进来的是可变还是不可变的,我都能保证我里面的属性是不可变的。如果你想修改text,那么你直接给text属性赋一个新的字符串就好了,如下:

UITextField *textField;
textField.text = @"标题";

我不希望你下面这样改,不给你提供这样的方式。

[textField.text appendString:@"标题"];

比如如下代码:

NSMutableString *mutableStr = [NSMutableString stringWithFormat:@"123"];

UITextField *textField;
textField.text = mutableStr;

//修改mutableStr不会影响到textField.text
[mutableStr appendString:@"456"];

textField.text是不可变的,给他传进去一个可变的mutableStr,修改mutableStr不会影响到textField.text,因为它们是拷贝之后两个独立的对象。

如果要是调用[mutableStr appendString:@"456"]之后,显示到UI界面上的文字也改变了,那么这就很诡异了,所以一般对于字符串这种和UI界面相关的,我们都使用copy,对于NSArray、NSDictionary一般还是使用strong

五. 自定义copy

以前我们讲copy都是拿Foundation框架自带的类进行操作,那么我们自定义类可以使用copy吗?iOS中确实提供了这样的机制来做这种事情。

对于Foundation框架自带的这些类,有copy和mutableCopy操作,如下:

NSArray, NSMutableArray;
NSDictionary, NSMutableDictionary;
NSString, NSMutableString;
NSData, NSMutableData;
NSSet, NSMutableSet;

但是我们自定义的类没有mutableCopy,因为mutableCopy只是Foundation框架自带的这些类才有。而且我们给自定义对象添加可变或不可变也没有意义啊(因为自定义对象里面的属性都可以改嘛,比如:person.name = @"test"),所以对于自定义对象我们不区分什么可变或不可变,我们只要管理好它的copy就好了。

下面就讲一下,如何自定义copy

自定义类如果想实现copy方法,必须遵守NSCopying协议,并且实现copyWithZone方法,copy方法底层就是调用copyWithZone方法

代码如下:

MJPerson.h

#import <Foundation/Foundation.h>

@interface MJPerson : NSObject <NSCopying>
@property (assign, nonatomic) int age;
@property (assign, nonatomic) double weight;
@end

MJPerson.m

#import "MJPerson.h"

@implementation MJPerson

- (id)copyWithZone:(NSZone *)zone
{
    MJPerson *person = [[MJPerson allocWithZone:zone] init];
    person.age = self.age;
    person.weight = self.weight;
    return person;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"age = %d, weight = %f", self.age, self.weight];
}

@end

运行代码:

#import <Foundation/Foundation.h>
#import "MJPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MJPerson *p1 = [[MJPerson alloc] init];
        p1.age = 20;
        p1.weight = 50;

        MJPerson *p2 = [p1 copy]; //将p1拷贝一份
        p2.age = 30;

        NSLog(@"p1对象:%@", p1);
        NSLog(@"p2对象:%@", p2);

        NSLog(@"p1对象指针:%p", p1);
        NSLog(@"p2对象指针:%p", p2);

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

打印:

p1对象:age = 20, weight = 50
p2对象:age = 30, weight = 50

p1对象指针:0x1004b6fc0
p2对象指针:0x1004b7020

通过打印可知,成功copy了两个对象。

注意:

  1. 上面的属性都是基本数据类型,所以可以直接赋值。
  2. 属性如果是copy修饰的字符串,也可以直接赋值,因为set方法内部也是调用copy。
  3. 属性如果是strong修饰的对象,要使用它的copy方法,前提这个对象也要遵守NSCopying协议,并实现copyWithZone方法。

Demo地址:copy

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

推荐阅读更多精彩内容

  • 一、抛出一个问题:使用CADisplayLink、NSTimer 有什么注意点?1.1-1.6的demo1.1、分...
    IIronMan阅读 1,361评论 1 3
  • 平时的积累记录下,记性差,方便平时查找 一 ARC和MRC的 小入门 1MRC在ARC之前,MRC就是传统的手工内...
    howhyone阅读 373评论 0 0
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 1. MRC ObjC中的内存管理机制跟C语言中指针的内容是同样重要的,要开发一个程序并不难,但是优秀的程序则更测...
    周灬阅读 1,099评论 0 0
  • copy的目的 顾名思义拷贝就是要产生一个副本对象,和我们平时使用的Ctr+C、Ctr+V是一样的,目的是保证副本...
    季末灬离殇阅读 655评论 1 1