Objective-C copy那些事儿

写在前面

Objective-C中的copy相关内容比我想象中要丰富多了。

NSCopying和NSMutableCopying协议

使用对象时经常需要拷贝它。在Objective-C中,此操作是通过copy和mutableCopy方法完成的,基类NSObject中与copy相关的API如下:

- (id)copy;

- (id)mutableCopy;

+ (id)copyWithZone:(struct_NSZone *)zone OBJC_ARC_UNAVAILABLE;

+ (id)mutableCopyWithZone:(struct_NSZone *)zone OBJC_ARC_UNAVAILABLE;

显然,NSObject已经实现了copy和mutableCopy方法。

如果想让自己的类(继承自NSObject,假设叫CustomClass)支持拷贝操作,该怎么弄呢?第一个想到的处理方式恐怕是重写copy方法(暂时不谈mutableCopy,稍后再叙)。

但是,这种做法是错误的。

正确的做法是让自定义类遵循NSCopying协议(NSObject并未遵循该协议),该协议只有一个方法:

@protocolNSCopying

- (id)copyWithZone:(NSZone*)zone;

@end

简单来说,当对某个对象发送copy消息时,NSObject#copy的实现逻辑会去自动调用copyWithZone:方法,有点回调的感觉;因此,若想支持拷贝操作,需要在自定义类中让其支持NSCopying,即实现copyWithZone:方法,而不是重写copy方法。

参数zone是什么鬼?这是因为在以前开发程序时,会把内存分为不同的(zone),而对象会创建在某个区里面。现在不用了,每个程序只有一个区:默认区(default zone)。所以说,尽管必须实现copyWithZone:方法,但是不必担心其中的zone参数。

举个栗子,有个表示个人信息的类,可以在其接口定义中声明此类遵循NSCopying协议,如下:

@interfaceUserInfo:NSObject

@property(nonatomic,copy)NSString*firstName;

@property(nonatomic,copy)NSString*lastName;

- (instancetype)initWithFirstName:(NSString*)firstName

andLastName:(NSString*)lastName;

@end

@implementationUserInfo

- (instancetype)initWithFirstName:(NSString*)firstName

andLastName:(NSString*)lastName {

if(self= [superinit]) {

_firstName = [firstNamecopy];

_lastName = [lastNamecopy];

}

returnself;

}

#pragma mark - NSCopying

- (instancetype)copyWithZone:(NSZone*)zone {

UserInfo *copy= [[[selfclass] allocWithZone:zone] initWithFirstName:_firstName

andLastName:_lastName];

returncopy;

}

@end

再来讲一下mutableCopy方法和NSMutableCopying协议;它们俩与copy方法和NSCopying协议相对应。当你的类还有mutable版本时,你还应该遵循NSMutableCopying协议,并实现mutableCopyWithZone:方法,这样,当向该类对象发送mutableCopy消息时,NSObject的mutableCopy方法实现代码中会回调你的mutableCopyWithZone:方法。

Note:虽然在自定义copyWithZone:和mutableCopyWithZone:中可以弄各种花样,但是务必保证如下逻辑:

[CustomClass copy] -> CustomClass

// 向immutable对象发送copy消息,得到一个immutable对象

[MutableCustomClass copy] -> CustomClass

// 向mutable对象发送一个copy消息,得到一个immutable对象

[CustomClass mutableCopy] -> MutableCustomClass

// 向immutable对象发送mutableCopy消息,得到mutable对象

[MutableCustomClass mutableCopy] -> MutableCustomClass

// 向mutable对象发送mutableCopy消息,得到mutable对象

下一个问题:向immutable对象发送copy消息一定会得到一个新对象吗?

No!下面的测试栗子所做的事情是分别向不可变的NSString、NSArray、NSDictionary以及NSSet对象发送copy消息,得到几个新的对象,新对象显然是immutable的,问题是:这些新对象真的是对象吗?如下栗子分别把新老对象的地址给打印出来:

NSString*testString =@"1";

NSString*copyString = [testStringcopy];

NSLog(@"testString address = %x", testString);

NSLog(@"copyString address = %x", copyString);

// print:

// testString address = 79720cc0

// copyString address = 79720cc0

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

NSArray*copyArray = [testArraycopy];

NSLog(@"testArray address = %x", testArray);

NSLog(@"copyArray address = %x", copyArray);

// print:

// testArray address = 79722fb0

// copyArray address = 79722fb0

NSDictionary*testDictionary = @{@1:@2};

NSDictionary*copyDictionary = [testDictionarycopy];

NSLog(@"testDictionary address = %x", testDictionary);

NSLog(@"copyDictionary address = %x", copyDictionary);

// print:

// testDictionary address = 79722fd0

// copyDictionary address = 79722fd0

NSSet*testSet = [NSSetsetWithObject:@1];

NSSet*copySet = [testSetcopy];

NSLog(@"testSet address = %x", testSet);

NSLog(@"copySet address = %x", copySet);

// print:

// testSet address = 79722ff0

// copySet address = 79722ff0

答案很明了!NSString、NSArray、NSDictionary以及NSSet,这是我们最常用的四个含有mutable版本的类型;向这些类型的immutable对象发送copy消息,这些对象会直接返回本身,而不是返回一个新创建的对象。

关于这一点,我反复使用各种姿势测试了很多次,均得到这样的结果;但是目前还没能找到比较权威的说法对这个现象进行说明。不过想想也很容易理解,对于一个immutable对象,真的没必要再复制一个,毕竟其中的内容不会发生改变,如果复制了,那么内存中将会存在两个一模一样的资源,岂不浪费?

总结这一段内容的要点如下:

若想令自己的类具备拷贝功能,则需要遵循NSCopying协议,实现其定义的copyWithZone:方法;

若自定义的类分为immutable和mutable版本,则需要同时遵循NSCopying和NSMutableCopying协议;

向immutable对象发送copy消息,并不一定会得到一个新对象;

深拷贝和浅拷贝

在很长时间里,我都认为浅拷贝(shallow copy)指的是「指针拷贝」,而深拷贝(deep copy)才是真正copy一个对象;显然,这种说法是不正确的。

一般来说,「深拷贝」和「浅拷贝」这两个概念是分析集合类型才会谈及的。深拷贝的意思是:在拷贝对象时,将其底层数据也一并复制过去。Foundation框架中所有集合类型在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据。这样做的原因在于,容器内的对象未必能拷贝,而且调用者也未必想在拷贝容器时一并拷贝其中每一个对象。

深拷贝和浅拷贝对比图如下:

理解@property中的copy修饰符

经常看到@property中有些对象类型属性被strong修饰,有些被copy修饰。strong修饰符的作用不消多说,该如何理解copy修饰符呢?

关于@property中copy修饰符的使用,我曾经历了这么两个阶段:

使用copy修饰mutable类型,使用strong修饰immutable类型;

使用copy修饰immutable类型,使用strong修饰mutable类型;

关于第1个阶段,我忘记了当时是怎么想的,它显然是错的;

关于第2个阶段,我之所以有这样的认识是因为曾在stackoverflow中看到了如下这么一段说明:

For attributes whose type is an immutable value class that conforms to theNSCopyingprotocol, you almost always should specifycopyin your@propertydeclaration. Specifying retain is something you almost never want in such a situation.

这句话错了吗?当然没有,要不也不会得到这么多的votes。但为什么这么说呢?不晓得是当时没耐心还是咋地,反正没怎么思考这个问题。

接着以上文提到的UserInfo为栗子,对之进行简化,只是定义两个NSString属性:firstName和lastName,作为对比前者使用copy修饰,后者使用strong修饰。如下:

@interfaceUserInfo:NSObject

@property(nonatomic,copy)NSString*firstName;

@property(nonatomic,strong)NSString*lastName;

@end

基于UserInfo创建实例进行测试:

NSMutableString*mutableFirstName = [NSMutableStringstringWithFormat:@"李"];

NSMutableString*mutableLastName = [NSMutableStringstringWithFormat:@"四"];

UserInfo *u = [[UserInfo alloc] init];

u.firstName = mutableFirstName;

u.lastName  = mutableLastName;

NSLog(@"全名:%@%@", u.firstName, u.lastName);

// print: 全名:李四

// 改mutableFirstName「李」为「公孙」

[mutableFirstName deleteCharactersInRange:NSMakeRange(0,1)];

[mutableFirstName appendString:@"公孙"];

// 改mutableLastName「四」为「四哥」

[mutableLastName appendString:@"哥"];

NSLog(@"全名:%@%@", u.firstName, u.lastName);

// print: 全名:李四哥

简单来说,对于immutable对象类型属性,假设该类型存在mutable版本,若使用strong修饰该属性,则将会是不安全的。

在上述代码中,u.lastName被strong修饰,对之赋值一个mutable类型mutableLastName,之后改变mutableLastName的值(由「四」变为「四哥」),显然也影响到了u.lastName的值,这通常是我们所不希望发生的;作为对比,u.firstName被copy修饰,也为之赋值mutable类型mutableFirstName,之后也改变mutableFirstName的值(由「李」变为「公孙」),但是u.firstName不受影响。

再往深一点看:@property的copy的作用机制是什么?根据我的理解,copy修饰符的意义有两点:

在系统自动合成属性的setter提供「指示」,使用类似于_iVar = [var copy];的方式进行赋值;

告诉使用者,安心的使用吧!

因此,根据我的理解,系统合成UserInfo的firstName和lastName的setter代码如下:

- (void)setFirstName:(NSString*)firstName {

_firstName = [firstNamecopy];

}

- (void)setLastName:(NSString*)lastName {

_lastName = lastName;

}

写到这里,可以回答一个常见的问题了:如何重写带copy关键字的setter?

换句话说,即便firstName属性被copy修饰,但是如果重写setter时采用错误的方式,copy带来的好处会荡然无存。譬如这样重写setFirstName::

- (void)setFirstName:(NSString*)firstName {

_firstName = firstName;

}

得到的结果如下(显然,firstName也是不安全的):

全名:李四

全名:公孙四哥

继续深挖:

是不是所有遵循NSCopying类型属性都应该使用copy修饰呢?

mutable类型属性可以使用copy修饰吗?

对于第一个问题,答案是No!对于向NSString、NSDictionary等属性才需要使用copy修饰,因为它们存在mutable版本,在为属性赋值时,右值很可能是它们的mutable类型对象,若使用strong修饰则会带来不稳定因子;另外一个方面,如果属性类型不存在对应的mutable版本,则完全不用担心这点,反正你也无法在外部修改它,不稳定因子自然不存在了。

对于第二个问题,答案仍然是No!被修饰符copy修饰的属性,默认的setter赋值方式是_iVar = [var copy];而copy方法返回的是immutable类型,将immutable对象赋值给mutable类型指针显然是不对的。

P.S:如果存在mutableCopy修饰符,或许可以使用mutableCopy修饰mutable属性^_^

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

推荐阅读更多精彩内容