1.前言
閱讀本文前請(qǐng)先閱讀第一篇《iOS - 內(nèi)存管理(一)之MRR》突琳,因?yàn)椴糠謨?nèi)容有涉及之前的知識(shí)點(diǎn)。
本來copy這個(gè)東西不完全屬于是內(nèi)存管理的內(nèi)容碗殷,不過由于內(nèi)存管理的規(guī)則涉及到copy這個(gè)方法葱色,同時(shí)copy的實(shí)現(xiàn)也有著許多內(nèi)存管理的注意點(diǎn),所以有必要專門在這里將其介紹清楚歪泳。本文的內(nèi)容都基于MRR模式
2.copy的目的
copy的目的就是為了不同的數(shù)據(jù)使用方使用或修改數(shù)據(jù)的時(shí)候不會(huì)影響到另一方正在使用的數(shù)據(jù)。
copy方法就是為了復(fù)制出來一個(gè)對(duì)象的副本露筒,副本內(nèi)的數(shù)據(jù)跟原對(duì)象一樣呐伞,這樣不管之后修改原對(duì)象或者副本對(duì)象的內(nèi)容,都不會(huì)影響到另外一方的數(shù)據(jù)慎式。
就好比我有一本書伶氢,我要借給某人,我又不想這個(gè)人在我書上亂畫瘪吏,我就去復(fù)印了一本癣防,給他了復(fù)印本。這樣不管以后我怎么畫掌眠,或者他怎么畫蕾盯,都不會(huì)影響到我們彼此手中的書里的內(nèi)容。
這個(gè)目的非常簡(jiǎn)單蓝丙,也非常重要级遭,這個(gè)目的要是沒有想清楚,之后有些點(diǎn)的東西在理解上會(huì)有困擾渺尘。
3.不同的copy
copy一個(gè)對(duì)象正常來說結(jié)果都是創(chuàng)建了一個(gè)新的對(duì)象挫鸽,新對(duì)象和舊對(duì)象數(shù)據(jù)是一樣的。但是有的時(shí)候并沒有創(chuàng)建新對(duì)象鸥跟,而直接返回了就對(duì)象的引用丢郊。針對(duì)這兩種情況,就有了不同的名稱锌雀,分別是 深拷貝 和 淺拷貝蚂夕。
3.1淺拷貝
如果細(xì)心的人可能會(huì)問了,你上面不是剛說了copy的目的就是要?jiǎng)?chuàng)建一個(gè)新的副本腋逆,這樣兩邊使用自己的對(duì)象就不會(huì)影響到別人了么?為什么還會(huì)有淺拷貝的情況出現(xiàn)侈贷?如果舊的和新的都是一個(gè)對(duì)象那還怎么能達(dá)到copy的目的惩歉?
這個(gè)問題其實(shí)很好等脂,首先正常來說copy都是深拷貝,只有當(dāng)某個(gè)類是不可變類型的時(shí)候撑蚌,才會(huì)出現(xiàn)淺拷貝的情況上遥。
什么叫不可變的類型?即這個(gè)類型的對(duì)象里面放的數(shù)據(jù)争涌,一旦初始化了粉楚,之后就不能改變了。比如我們都熟悉的NSString亮垫,NSArray或者NSDictionary模软。
NSString *str = @"123";
//copyStr 和 str其實(shí)是指向內(nèi)存里同一個(gè)字符串對(duì)象,這個(gè)copy就是一個(gè)淺拷貝
NSString *copyStr = [str copy];
上面的代碼中饮潦,雖然這個(gè)copy一個(gè)淺拷貝燃异,但是它并沒有違背我們copy的目的。因?yàn)镹SString本身就是不可變的继蜡,也就是說不管是拿著舊的引用str還是副本引用copyStr回俐,你都沒法改變它內(nèi)部的數(shù)據(jù),也就是這個(gè)字符串永遠(yuǎn)都是123稀并。所以為了節(jié)省資源仅颇,這個(gè)copy也就沒有必要返回一個(gè)新的對(duì)象了。這便是淺拷貝的意義所在碘举。
有人可能想說灵莲,我就是想對(duì)一個(gè)不可變的類型對(duì)象,拷貝一個(gè)真正的新的對(duì)象出來殴俱,行不行政冻?行!還有另一個(gè)拷貝方法线欲,mutableCopy明场,
NSString *str = @"123";
//因?yàn)槭鞘褂昧薽utableCopy,明確說明我是要可變的拷貝李丰,所以拷貝出來的對(duì)象是一個(gè)新的對(duì)象
//并且這個(gè)對(duì)象的類型也變了苦锨,變成了它的可變類型NSMutableString
//這就是一個(gè)深拷貝
NSMutableString *copyStr = [str mutableCopy];
3.2深拷貝
深拷貝就很簡(jiǎn)單了,copy之后的對(duì)象是一個(gè)新的對(duì)象趴泌,比如上面的例子對(duì)一個(gè)不可變的字符串調(diào)用了mutableCopy這就是一個(gè)深拷貝舟舒。
假如對(duì)一個(gè)可變的類型進(jìn)行copy會(huì)怎樣?很簡(jiǎn)單嗜憔,對(duì)一個(gè)可變的類型進(jìn)行不管copy還是mutableCopy秃励,結(jié)果都是深拷貝。想想上面最開始說的copy的目的就能馬上明白吉捶,本身就是可變類型夺鲜,表示可以改變對(duì)象保存的數(shù)據(jù)內(nèi)容皆尔,如果還直接返回一個(gè)舊對(duì)象,那就沒法避免互相影響了币励。
3.3內(nèi)存管理的角度分析
下面是一個(gè)小總結(jié)慷蠕,如果理解了copy的目的和淺拷貝深拷貝,就不用背這種東西都能自己寫出來食呻。
不可變類型 -> copy -> 不可變類型(淺拷貝流炕,新舊指針指向同一個(gè)對(duì)象,該對(duì)象引用計(jì)數(shù)+1)
不可變類型 -> mutableCopy -> 可變類型 (深拷貝仅胞,舊對(duì)象引用計(jì)數(shù)不變每辟,新對(duì)象引用計(jì)數(shù)=1)
可變類型 -> copy -> 可變類型 (深拷貝,舊對(duì)象引用計(jì)數(shù)不變饼问,新對(duì)象引用計(jì)數(shù)=1)
可變類型 -> mutableCopy -> 可變類型 (深拷貝影兽,舊對(duì)象引用計(jì)數(shù)不變,新對(duì)象引用計(jì)數(shù)=1)
只有當(dāng)一個(gè)拷貝是淺拷貝的時(shí)候莱革,新舊指針指向同一個(gè)對(duì)象峻堰,該對(duì)象引用計(jì)數(shù)+1。如果一個(gè)拷貝是深拷貝的時(shí)候舊對(duì)象引用計(jì)數(shù)不變盅视,新對(duì)象引用計(jì)數(shù)=1捐名。
如果再細(xì)心的人在這里就能明白為什么我在上一篇《iOS - 內(nèi)存管理(一)之MRR》中說“用copy和mutableCopy獲得的新對(duì)象retainCount等于1”這一句話不嚴(yán)謹(jǐn)了。包括蘋果官方文檔的那個(gè)配圖闹击,也不嚴(yán)謹(jǐn)了镶蹋。
因?yàn)檫@種不嚴(yán)謹(jǐn)?shù)恼f法都是深拷貝的情況,淺拷貝的時(shí)候retainCount就不是如上所說了赏半。
但是贺归!但是!這個(gè)但是很重要断箫,雖然說不嚴(yán)謹(jǐn)拂酣,但是并不是不正確,并且在某種層面上來說是很正確的仲义。如果站在底層真實(shí)retainCount的角度來說不嚴(yán)謹(jǐn)婶熬。但是如果站在使用層面,或者說只要遵守了MRR的內(nèi)存管理原則埃撵,你怎么去理解這個(gè)copy都不會(huì)造成內(nèi)存泄漏問題赵颅,你把它全部都當(dāng)成深拷貝來管理都沒關(guān)系。下面用代碼舉例說明
//從真實(shí)retainCount角度來看
{
//新建的一個(gè)不可變的數(shù)組暂刘,數(shù)組的retainCount等于1
NSArray *arr = [[NSArray alloc] initWithObjects:obj, nil];
//這是一個(gè)淺拷貝饺谬,這個(gè)數(shù)組的retainCount = 2
NSArray * arr2 = [arr copy];
//因?yàn)閍rr和arr2都是指向同一個(gè)內(nèi)存對(duì)象,所以我調(diào)用arr release和arr2 release效果一樣的
//release兩次就對(duì)象銷毀了
[arr release];
[arr release];//或者[arr2 release]效果相同
}
可以看到這種純考慮retainCount的想法不是太可取鸳惯,而且也不應(yīng)該對(duì)一個(gè)變量調(diào)用兩次release商蕴,站在使用方的層面也不應(yīng)該去考慮這個(gè)考慮是淺的還是深的叠萍。所以我們換一個(gè)思維來管理芝发,就是上一篇說的只要是alloc/new/copy/mutableCopy返回的绪商,都當(dāng)成一個(gè)新的對(duì)象,注意看下面代碼的注釋:
//使用方角度來看
{
//新建的一個(gè)不可變的數(shù)組辅鲸,因?yàn)槭怯胊lloc創(chuàng)建的格郁,所以持有他,有責(zé)任釋放
NSArray *arr = [[NSArray alloc] initWithObjects:obj, nil];
//copy了一個(gè)新數(shù)組出來独悴,不要管是不是淺拷貝例书,因?yàn)槭莄opy的,所以持有他刻炒,有責(zé)任釋放
NSArray * arr2 = [arr1 copy];
//arr和arr2都是持有的决采,所以都有責(zé)任釋放
[arr release];
[arr2 release];
}
可以看到兩份代碼一樣的,所以內(nèi)存管理的結(jié)果也是一樣的坟奥,但是由于思想不同树瞭,在寫這兩個(gè)代碼時(shí)候管理內(nèi)存的思路是不同的,也可以看出就將copy看成是獲得了一個(gè)獨(dú)立的副本會(huì)更加有利于去內(nèi)存管理爱谁。
理解一個(gè)東西很重要的一點(diǎn)是要基于這個(gè)東西所在的層的原則去理解晒喷,有時(shí)候你要用下一層實(shí)現(xiàn)的思維去使用上一層的方法往往會(huì)給理解造成困擾。
4.NSCopying和NSMutableCopying
NSCopying是一個(gè)protocol協(xié)議访敌,我們平時(shí)自定義的類默認(rèn)是沒有實(shí)現(xiàn)這個(gè)協(xié)議的凉敲,也就是說我們自定義的類時(shí)沒有copy方法可以調(diào)用的,如果想要有copy方法寺旺,就要讓你的自定義類遵守NSCopying這個(gè)協(xié)議爷抓,并實(shí)現(xiàn)協(xié)議規(guī)定的方法。
4.1 copyWithZone
NSCopying協(xié)議只規(guī)定了一個(gè)方法要實(shí)現(xiàn)就是:
- (id)copyWithZone:(NSZone *)zone
在這個(gè)方法里阻塑,返回一個(gè)新的副本對(duì)象的引用蓝撇。
對(duì)于前面說的不可變類型淺拷貝的情況,就是在里面直接返回當(dāng)前對(duì)象的引用:
- (id)copyWithZone:(NSZone *)zone
{
return [self retain];
}
4.2 實(shí)例變量的copy方式
對(duì)于我們的自定義的類叮姑,通常都不會(huì)是不可變的類型唉地,也就是說我們自定義的類創(chuàng)建的對(duì)象里面保存的數(shù)據(jù)通常都是可以被不斷重設(shè)的,這種情況下copyWithZone方法的實(shí)現(xiàn)就不是簡(jiǎn)單的返回self retain了传透。
比如我有如下的一個(gè)自定義類耘沼,它想要實(shí)現(xiàn)copy方法:
@interface Book : NSObject <NSCopying>
{
NSString *_bookName;
Person *_author;
}
@end
這個(gè)類可能有很多個(gè)實(shí)例變量,當(dāng)我創(chuàng)建了一個(gè)新的副本對(duì)象朱盐,這些原有實(shí)例變量?jī)?nèi)容需要都被賦值過去群嗤,那么這些實(shí)例變量它具體是要被深拷貝還是淺拷貝到新的對(duì)象里,這就要看這個(gè)實(shí)例變量的setter方法是怎么寫的兵琳。
(1)如果setter方法里是copy了再賦值給實(shí)例變量,那么就要深拷貝它
意思就是统阿,比如_author的setter方法如下
- (void)setAuthor:(Person *)author
{
[_author autorelease];
// 這里對(duì)新設(shè)置進(jìn)來的author進(jìn)行了copy
// 說明這里本意就是對(duì)設(shè)置的author持有它的副本米间,從而外接改變了原有author的內(nèi)容也不影響當(dāng)前實(shí)例變量的_author值
// 更直接的說就是,是希望不同的Book對(duì)象持有不同的author對(duì)象
_author = [author copy];
}
所以根據(jù)注釋說的破衔,每設(shè)置一個(gè)新的值進(jìn)來的時(shí)候,setter都會(huì)copy一個(gè)副本出來再傳給實(shí)例變量钱烟。所以再copy整個(gè)Book類的時(shí)候晰筛,也要深拷貝這個(gè)實(shí)例變量。
(2)如果setter方法里是retain新值再賦值拴袭,那么就要淺拷貝它
加入author的setter方法如下读第,則在copy Book的時(shí)候只需要淺拷貝這個(gè)變量:
- (void)setAuthor:(Person *)author
{
[_author autorelease];
// 這里對(duì)新設(shè)置進(jìn)來的author進(jìn)行了retain
// 說明這里本意就是持有這個(gè)新值,而不是新值的副本
_author = [author retain];
}
也很容易理解拥刻,就是它只想持有原始對(duì)象怜瞒,沒有想持有一個(gè)不一樣的副本。
(3)如果setter方法里是直接賦值的般哼,那么也是淺拷貝它
- (void)setAuthor:(Person *)author
{
// 這里對(duì)新設(shè)置進(jìn)來的author直接賦值
// 說明這里本意就是使用這個(gè)新值吴汪,而不是持有,允許它雖然被銷毀
// 更不是要新值的副本
_author = author;
}
這種情況也是淺拷貝即可逝她,它只想要原始對(duì)象的使用權(quán)浇坐,沒有想持有一個(gè)不一樣的副本。
4.3 父類沒有實(shí)現(xiàn)copy方法
當(dāng)一個(gè)類的父類沒有實(shí)現(xiàn)copy方法的時(shí)候黔宛,在這個(gè)類的copyWithZone里近刘,最好是使用alloc,initXXX方法來創(chuàng)建新的副本對(duì)象。因?yàn)槟切├^承的實(shí)例變量的細(xì)節(jié)通常都被封裝在里面了臀晃。
- (void)copyWithZone:(NSZone *)zone
{
Book *copy = [[Book alloc] initWithName:[self bookName] author:[self author]];
return copy;
}
只要父類沒有實(shí)現(xiàn)copy觉渴,子類就有義務(wù)將父類中定義的變量和子類中定義的變量都拷貝給副本對(duì)象里。
4.3 父類實(shí)現(xiàn)了copy方法
當(dāng)父類實(shí)現(xiàn)了copy方法徽惋,我們可以直接super copyWithZone來獲得副本對(duì)象案淋,然后再將子類自己定義的實(shí)例變量復(fù)制給新的副本對(duì)象。
//假設(shè)父類實(shí)現(xiàn)了copy
- (void)copyWithZone:(NSZone *)zone
{
Book *copy = [super copyWithZone:zone];
[copy setAuthor:[self author]];
return copy;
}
假如父類的copyWithZone的實(shí)現(xiàn)里面可能或者用了NSCopyObject()這個(gè)方法來創(chuàng)建副本险绘,那么在子類里調(diào)用完了父類的copyWithZone之后踢京,還要將父類里定義的retain或者copy的實(shí)例變量,重新賦值一次宦棺。
注意:因?yàn)镹SCopyObject()只會(huì)淺拷貝所有實(shí)例變量瓣距,也就是說通過NSCopyObject()拷貝返回出來的對(duì)象是一個(gè)新的對(duì)象,但是對(duì)象里面的所有實(shí)例變量都是指向原本對(duì)象的實(shí)例變量代咸,并且這些實(shí)力變量指向內(nèi)容的retainCount是沒有變化的蹈丸。并且賦值的時(shí)候一定要注意先將實(shí)例變量設(shè)為nil,否則會(huì)實(shí)例變量被提前銷毀。上代碼解釋更清楚:
//假設(shè)我的TestObj類有兩個(gè)實(shí)例變量
//隨便取的類型的名字逻杖,不要太糾結(jié)
@interface TestObj : NSObject
{
NSMutableString *_name;
NSError *_error;
}
//_name的set方法里奋岁,舊值release,copy 了新值然后保存
- (void)setName:(NSMutableString *)name
{
[_name release];
_name = [name mutableCopy];
}
//error的set方法里荸百,舊值release闻伶,retain了新值然后保存
- (void)setError:(NSError *)error
{
[_error release];
_error = [error retain];
}
//TestObj類的copy方法
- (id)copyWithZone:(NSZone *)zone
{
/*
使用NSCopyObject()方法copy出來的新的副本對(duì)象copy
*/
TestObj *copy = NSCopyObject(self, 0, zone);
/*
走完上面這句以后self和copy是兩個(gè)TestObj對(duì)象,它們的實(shí)例變量里的值是一樣的
由于NSCopyObject()方法拷貝出來對(duì)象里的實(shí)例變量值都是淺拷貝
也就是說copy里面的_name和_error指向的對(duì)象值就是self的_name和_error里的管搪,并且retainCount都沒變
這就到之后最后銷毀的時(shí)候很可能會(huì)被多release一次虾攻,導(dǎo)致程序異痴÷颍或崩潰
*/
/*
所以我們必須在NSCopyObject之后更鲁,重新設(shè)值一次原對(duì)象“持有”的那些實(shí)例變量。
至于為什么要先設(shè)置為nil奇钞,可以這樣理解:
假設(shè)當(dāng)前self的_name的retainCount=1澡为,copy的_name因?yàn)槭菧\拷貝,一樣的值retainCount也是1
但是set方法里會(huì)先autorelease舊的值景埃,所以還沒等賦值媒至,_name的retainCount就歸零了
所以要先將這個(gè)淺拷貝的“弱指針”指向nil。
*/
copy->_name = nil;
[copy setName:self.name];
copy->_error = nil;
[copy setError:self.error];
return copy;
}
5.NSMutableCopying
以上講的都是NSCopying協(xié)議谷徙,NSMutableCopying協(xié)議同理拒啰,只有一個(gè)方法要實(shí)現(xiàn):
-(id)mutableCopyWithZone:(nullable NSZone *)zone;
但是通常來說,我們自己的自定義的類要實(shí)現(xiàn)copy方法完慧,只需要遵守NSCopying協(xié)議即可谋旦,因?yàn)橐话愕淖远x類都是可變的。除非有特殊情況屈尼,你要實(shí)現(xiàn)想NSString和NSMutableString這種配對(duì)的可變和不可變的類册着,那么你才需要遵守這個(gè)協(xié)議。然后針對(duì)可變不可變來實(shí)現(xiàn)不同的內(nèi)容脾歧。
6. 結(jié)語
至此copy相關(guān)的內(nèi)容就介紹完了甲捏,主要還是為了填補(bǔ)上一篇中遺留的一些內(nèi)容,可以繼續(xù)閱讀下一篇《iOS - 內(nèi)存管理(三)之ARC》