对象拷贝操作也比较常见,在ObjC中有两种方式的拷贝:copy和mutablecopy,这两中方式都将产生一个新的对象,只是后者产生的是一个可变对象。在ObjC中如果要想实现copy或者mutablecopy操作需要实现NSCopy或者NSMutableCopy协议,拷贝操作产生的新的对象默认引用计数器是1,在非ARC模式下我们应该对这个对象进行内存管理。在熟悉这两种操作之前我们首先需要弄清两个概念:深复制(或深拷贝)和浅复制(或浅拷贝)。
- 浅复制:在执行复制操作时,对于对象中每一层(对象中包含的对象,例如说属性是某个对象类型)复制都是指针复制(如果从引用计数器角度出发,那么每层对象的引用计数器都会加1)。
- 深复制:在执行复制操作时,至少有一个对象的复制是对象内容复制(如果从引用计数器角度出发,那么除了对象内容复制的那个对象的引用计数器不变,其他指针复制的对象其引用计数器都会加1)。
注:
指针拷贝:拷贝的是指针本身(也就是具体对象的地址)而不是指向的对象内容本身。
对象复制:对象复制指的是复制内容是对象本身而不是对象的地址。
完全复制:上面说了深复制和浅复制,既然深复制是至少一个对象复制是对象内容复制,那么如果所有复制都是对象内容复制那么这个复制就叫完全复制。
对比copy和mutablecopy其实前面我们一直还用到一个操作是retain,它们之间的关系如下:
retain:始终采取浅复制,引用计数器会加1,返回的对象和被复制对象是同一个对象1(也就是说这个对象的引用多了一个,或者说是指向这个对象的指针多了一个);
copy:对于不可变对象copy采用的是浅复制,引用计数器加1(其实这是编译器进行了优化,既然原来的对象不可变,复制之后的对象也不可变那么就没有必要再重新创建一个对象了);对于可变对象copy采用的是深复制,引用计数器不变(原来的对象是可变,现在要产生一个不可变的当然得重新产生一个对象);
mutablecopy:无论是可变对象还是不可变对象采取的都是深复制,引用计数器不变(如果从一个不可变对象产生一个可变对象自然不用说两个对象绝对不一样肯定是深复制;如果从一个可变对象产生出另一个可变对象,那么当其中一个对象改变自然不希望另一个对象改变,当然也是深复制)。
注:
可变对象:当值发生了改变,那么地址也随之发生改变;
不可变对象:当值发生了改变,内容首地址不发生变化;
引用计数器:用于计算一个对象有几个指针在引用(有几个指针变量指向同一个内存地址);
// // main.m // FoundationFramework // // Created by Kenshin Cui on 14-2-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import <Foundation/Foundation.h> void test1(){ NSString *name=@"Kenshin"; NSString *str1=[NSString stringWithFormat:@"I'm %@.",name];//注意此时str1的计数器是1 NSLog(@"%lu",[str1 retainCount]); //结果:1 NSMutableString *str2=[str1 mutableCopy];//注意此时str2的计数器为1,str1的计数器还是1 //NSMutableString *str5 =CFRetain((__bridge CFTypeRef)str2); NSLog(@"retainCount(str1)=%lu,retainCount(str2)=%lu",[str1 retainCount],[str2 retainCount]); //结果:retainCount(str1)=1,retainCount(str2)=1 [str2 appendString:@"def"];//改变str2,str1不变 NSLog(@"%zi",str1==str2);//二者不是向同一个对象,结果:0 NSLog(@"str1=%@",str1); //结果:str1=I'm Kenshin. NSLog(@"str2=%@",str2); //结果:str2=I'm Kenshin.def NSLog(@"str1's %lu",[str1 retainCount]); NSString *str3=[str1 copy];//str3不是产生的新对象而是复制了对象指针,但是str1的计数器+1(当然既然str3同样指向同一个对象,那么如果计算str3指向的对象引用计数器肯定等于str1的对象引用计数器) NSLog(@"%zi",str1==str3);//二者相等指向同一个对象,结果:1 NSLog(@"retainCount(str1)=%lu,retainCount(str3)=%lu",str1.retainCount,str3.retainCount); //结果:retainCount(str1)=2,retainCount(str3)=2 //需要注意的是使用copy和mutableCopy是深复制还是浅复制不是绝对,关键看由什么对象产生什么样的对象 NSString *str4=[str2 copy];//由NSMutableString产生了NSString,二者类型都不同肯定是深拷贝,此时str2的计数器还是1,str4的计数器也是1 [str2 appendString:@"g"];//改变原对象不影响str4 NSLog(@"%zi",str2==str4); //结果:0 NSLog(@"str2=%@",str2); //结果:str2=I'm Kenshin.defg NSLog(@"str4=%@",str4); //结果:str4=I'm Kenshin.def [str1 release]; str1=nil; [str3 release];//其实这里也可以调用str1再次release,因为他们两个指向的是同一个对象(但是一般不建议那么做,不容易理解) str3=nil; [str2 release]; str2=nil; [str4 release]; str4=nil; //上面只有一种情况是浅拷贝:不可变对象调用copy方法 } int main(int argc,char *argv[]){ test1(); return 0; }
为了方便大家理解上面的代码,这里以图形画出str1、str2、str3、str4在内存中的存储情况:
从上面可以清楚的看到str1和str3同时指向同一个对象,因此这个对象的引用计数器是2(可以看到两箭头指向那个对象),str2和str4都是两个新的对象;另外ObjC引入对象拷贝是为了改变一个对象不影响另一个对象,但是我们知道NSString本身就不能改变那么即使我重新复制一个对象也没有任何意义,因此为了性能着想如果通过copy方法产生一个NSString时ObjC不会再复制一个对象而是将新变量指向同一个对象。
注意网上很多人支招在ARC模式下可以利用_objc_rootRetainCount()或者CFGetRetainCount()取得retainCount都是不准确的,特别是在对象拷贝操作之后你会发现二者取值也是不同的,因此如果大家要查看retainCount最好还是暂时关闭ARC。
要想支持copy或者mutablecopy操作那么对象必须实现NSCoping协议并实现-(id)copyWithZone:(NSZone*)zone方法,在Foundation中常用的可复制对象有:NSNumber、NSString、NSMutableString、NSArray、NSMutableArray、NSDictionary、NSMutableDictionary。下面看一下如何让自定义的类支持copy操作:
Person.h
// // Person.h // FoundationFramework // // Created by Kenshin Cui on 14-2-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import <Foundation/Foundation.h> @class Account; @interface Person : NSObject @property NSMutableString *name; @property (nonatomic,assign) int age; @end
Person.m
// // Person.m // FoundationFramework // // Created by Kenshin Cui on 14-2-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "Person.h" @implementation Person -(NSString *)description{ return [NSString stringWithFormat:@"name=%@,age=%i",_name,_age]; } //实现copy方法 -(id)copyWithZone:(NSZone *)zone{ //注意zone是系统已经分配好的用于存储当前对象的内存 //注意下面创建对象最好不要用[[Person allocWithZone:zone]init],因为子类如果没有实现该方法copy时会调用父类的copy方法,此时需要使用子类对象初始化如果此时用self就可以表示子类对象,还有就是如果子类调用了父类的这个方法进行重写copy也需要调用子类对象而不是父类Person Person *person1=[[[self class] allocWithZone:zone]init]; person1.name=_name; person1.age=_age; return person1; } @end
main.m
// // main.m // FoundationFramework // // Created by Kenshin Cui on 14-2-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import <Foundation/Foundation.h> #import "Account.h" #import "Person.h" void test1(){ Person *person1=[[Person alloc]init]; NSMutableString *str1=[NSMutableString stringWithString:@"Kenshin"]; person1.name=str1; //由于name定义的时候使用属性参数采用的是copy策略,而根据前面的知识我们知道NSMutableString的copy策略采用的是对象内容复制,因此如果修改str1不会改变person1.name [str1 appendString:@" Cui"]; NSLog(@"%@",str1);//结果:Kenshin Cui NSLog(@"%@",person1.name); //结果:Kenshin } void test2(){ Person *person1=[[Person alloc]init]; person1.name=[NSMutableString stringWithString:@"Kenshin"]; person1.age=28; Person *person2=[person1 copy]; NSLog(@"%@",person1); //结果:name=Kenshin,age=0 NSLog(@"%@",person2); //结果:name=Kenshin,age=0 [person2.name appendString:@" Cui"]; NSLog(@"%@",person1);//结果:name=Kenshin Cui,age=28 NSLog(@"%@",person2);//结果:name=Kenshin Cui,age=28 } int main(int argc,char *argv[]){ test1(); test2(); return 0; }
在上面的代码中重点说一下test2这个方法,在test2方法中我们发现当修改了person2.name属性之后person1.name也改变了,这是为什么呢?我们可以看到在Person.m中自定义实现了copy方法,同时实现了一个浅拷贝。之所以说是浅拷贝主要是因为我们的name属性参数是直接赋值完成的,同时由于name属性定义时采用的是assign参数(默认为assign),所以当通过copy创建了person2之后其实person2对象的name属性和person1指向同一个NSMutableString对象。通过图形表示如下:
上面test2的写法纯属为了让大家了解复制的原理和本质,实际开发中我们很少会遇到这种情况,首先我们一般定义name的话可能用的是NSString类型,根本也不能修改;其次我们定义字符串类型的话一般使用(copy)参数,同样可以避免这个问题(因为NSMutableString的copy是深复制)。那么如果我们非要使用NSMutabeString同时不使用属性的copy参数如何解决这个问题呢?答案就是使用深复制,将-(id)copyWithZone:(NSZone *)zone方法中person1.name=_name改为,person1.name=[_name copy];或person1.name=[_name mutablecopy]即可,这样做也正好满足我们上面对于深复制的定义。
补充-NSString的引用计数器
在好多语言中字符串都是一个特殊的对象,在ObjC中也不例外。NSString作为一个对象类型存储在堆中,多数情况下它跟一般的对象类型没有区别,但是这里我们需求强调一点那就是字符串的引用计数器。
// // main.m // FoundationFramework // // Created by Kenshin Cui on 14-2-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import <Foundation/Foundation.h> int main(int argc,char *argv[]){ NSString *str1=@"Kenshin"; NSLog(@"retainCount(str1)=%i",(unsigned long)str1.retainCount); //结果:-1 [str1 retain]; NSLog(@"retainCount(str1)=%i",(unsigned long)str1.retainCount); //结果:-1 NSString *str2=[NSString stringWithString:@"Kaoru"]; NSLog(@"retainCount(str2)=%i",str2.retainCount); //结果:-1 [str1 retain]; NSLog(@"retainCount(str2)=%i",str2.retainCount); //结果:-1 NSString *str2_1=[NSString stringWithString:[NSString stringWithFormat:@"Kaoru %@",@"sun"]]; NSLog(@"retainCount(str2_1)=%i",str2_1.retainCount); [str2_1 release]; [str2_1 release]; NSString *str3=[NSString stringWithFormat:@"Rosa %@",@"Sun"]; NSLog(@"retainCount(str3)=%i",str3.retainCount); //结果:1 [str3 retain]; NSLog(@"retainCount(str3)=%i",str3.retainCount); //结果:2 [str3 release]; [str3 release]; NSString *str4=[NSString stringWithUTF8String:"Jack"]; NSLog(@"retainCount(str4)=%i",str4.retainCount); //结果:1 [str4 retain]; NSLog(@"retainCount(str4)=%i",str4.retainCount); //结果:2 [str4 release]; [str4 release]; NSString *str5=[NSString stringWithCString:"Tom" encoding:NSUTF8StringEncoding]; NSLog(@"retainCount(str5)=%i",str5.retainCount); //结果:1 [str5 retain]; NSLog(@"retainCount(str5)=%i",str5.retainCount); //结果:2 [str5 release]; [str5 release]; NSMutableString *str6=@"Jerry"; NSLog(@"retainCount(str6)=%i",str6.retainCount); //结果:-1 [str6 retain]; NSLog(@"retainCount(str6)=%i",str6.retainCount); //结果:-1 [str6 release]; [str6 release]; NSMutableArray *str7=[NSMutableString stringWithString:@"Lily"]; NSLog(@"retainCount(str7)=%i",str7.retainCount); //结果:1 [str7 retain]; NSLog(@"retainCount(str7)=%i",str7.retainCount); //结果:2 [str7 release]; [str7 release]; return 0; }
看完上面的例子如果不了解NSString的处理你也许会有点奇怪?请看下面的解释
- str1是一个字符串常量,它存储在常量区,系统不会对它进行引用计数,因此无论是初始化还是做retain操作其引用计数器均为-1;
- str3、str4、str5创建的对象同一般对象类似,存储在堆中,系统会对其进行引用计数;
- 采用stringWithString定义的变量有些特殊,当后面的字符串是字符串常量,则它本身就作为字符串常用量存储(str2),类似于str1;如果后面的参数是通过类似于str3、str4、str5的定义,那么它本身就是一个普通对象,只是后面的对象引用计数器默认为1,当给它赋值时会做一次拷贝操作(浅拷贝),引用计数器加1,所有str2_1引用计数器为2;
- str6其实和str1类似,虽然定义的是可变数组,但是它的本质还是字符串常量,事实上对于可变字符串只有为字符串常量时引用计数器才为-1,其他情况它的引用计数器跟一般对象完全一致;