iOS中assign,copy,retain之間的區別以及weak和strong的區別

本文邏輯圖如下


文章邏輯圖

在知道他們區別之前,我們首先要知道NSObject對象的賦值操作做了哪些操作。

A=C其實是在內存中創建了一個A,然後又開闢了一個內存B,B裏面存放的着值C。


NSObject賦值示意圖1

如下:

NSMutableString*tempMStr = [[NSMutableString alloc]initWithString:@"strValue"];
NSLog(@"tempMStr值地址:%p,tempMStr值%@,tempMStr值引用計數%@\n", tempMStr,tempMStr,[tempMStr valueForKey:@"retainCount"]);

//輸出tempMStr值地址:0x7a05f650,tempMStr值strValue,tempMStr值引用計數1

此處tempMStr就是A,值地址就是C,“strValue”就是B,而引用計數這個概念是針對C的,賦值給其他變量或者指針設置爲nil,如tempStr = nil,都會使得引用計數有所增減。當內存區域引用計數爲0時就會將數據抹除。而我們使用copy,strong,retain,weak,assign區別就在:

1.是否開闢新的內存
2.是否對地址C有引用計數增加

需要注意的是property修飾符是在被賦值時起作用。

1.以典型的NSMutableString爲例

@property(copy,nonatomic)NSMutableString*aCopyMStr;
@property(strong,nonatomic)NSMutableString*strongMStr;
@property(weak,nonatomic)NSMutableString*weakMStr;
@property(assign,nonatomic)NSMutableString*assignMStr;

NSMutableString*mstrOrigin = [[NSMutableStringalloc]initWithString:@"mstrOriginValue"];

self.aCopyMStr= mstrOrigin;
self.strongMStr= mstrOrigin;
self.strongMStr= mstrOrigin;
self.weakMStr= mstrOrigin;

NSLog(@"mstrOrigin輸出:%p,%@\n", mstrOrigin,mstrOrigin);
NSLog(@"aCopyMStr輸出:%p,%@\n",_aCopyMStr,_aCopyMStr);
NSLog(@"strongMStr輸出:%p,%@\n",_strongMStr,_strongMStr);
NSLog(@"weakMStr輸出:%p,%@\n",_weakMStr,_weakMStr);
NSLog(@"引用計數%@",[mstrOriginvalueForKey:@"retainCount"]);

//輸出結果
//2016-09-01 15:19:13.134 lbCopy[1205:87583] mstrOrigin輸出:0x7892a5e0,mstrOriginValue
//2016-09-01 15:19:13.135 lbCopy[1205:87583] aCopyMStr輸出:0x7893deb0,mstrOriginValue
//2016-09-01 15:19:13.135 lbCopy[1205:87583] strongMStr輸出:0x7892a5e0,mstrOriginValue
//2016-09-01 15:19:13.135 lbCopy[1205:87583] weakMStr輸出:0x7892a5e0,mstrOriginValue
//2016-09-01 15:19:13.135 lbCopy[1205:87583] 引用計數2

strongMStr和weakMStr指針指向的內存地址都和mstrOrigin相同,但mstrOrigin內存引用計數爲2,不爲3,因爲weakMStr雖然指向了數據內存地址(之後用C簡稱,見示意圖1),但不會增加C計數。copy修飾的的aCopyMStr,賦值後則是自己單獨開闢了一塊內存,內存上保存“mstrOrigin”字符串,並指向。

拷貝示意圖如下


NSMutableString拷貝示意圖2

可見當我修改mstrOrigin的值的時候,必然不會影響aCopyMStr,只會影響strongMStr和weakMStr。我們來驗證下

NSLog(@"------------------修改原值後------------------------");

[mstrOriginappendString:@"1"];
NSLog(@"mstrOrigin輸出:%p,%@\n", mstrOrigin,mstrOrigin);
NSLog(@"aCopyMStr輸出:%p,%@\n",_aCopyMStr,_aCopyMStr);
NSLog(@"strongMStr輸出:%p,%@\n",_strongMStr,_strongMStr);
NSLog(@"weakMStr輸出:%p,%@\n",_weakMStr,_weakMStr);

//輸出結果
//2016-09-01 15:33:02.839 lbCopy[1205:87583] mstrOrigin輸出:0x7892a5e0,mstrOrigin1
//2016-09-01 15:33:02.839 lbCopy[1205:87583] aCopyMStr輸出:0x7893deb0,mstrOrigin
//2016-09-01 15:33:02.839 lbCopy[1205:87583] strongMStr輸出:0x7892a5e0,mstrOrigin1
//2016-09-01 15:33:02.839 lbCopy[1205:87583] weakMStr輸出:0x7892a5e0,mstrOrigin1

copy會重新開闢新的內存來保存一份相同的數據。被賦值對象和原值修改互不影響。strong和weak賦值都指向原來數據地址,區別是前者會對數據地址進行引用計數+1,後者不會

引用計數是否+1有什麼實質區別呢?

如果知道“值地址的引用計數爲0時,地址上保存的值就會被釋放”。那麼區別就不難理解,weak修飾的指針A指向的值地址C,那麼地址上當其他指向他的指針被釋放的時候,這個值地址引用計數也就變爲0了,這個A的值也就爲nil了。換句話說當值地址C上沒有其他強引用指針修飾的時候C就會被立即釋放,A的值就變爲nil了。

這裏我們來初始化mstrOrigin和並將strongMStr設置爲nil讓C的引用計數爲0,然後輸出weakMStr,看是否爲nil.
注:初始化和設爲nil都可以將指針所指向的數據地址引用計數減少1

mstrOrigin = [[NSMutableStringalloc]initWithString:@"mstrOriginChange2"];
self.strongMStr=nil;
NSLog(@"mstrOrigin輸出:%p,%@\n", mstrOrigin,mstrOrigin);
NSLog(@"strongMStr輸出:%p,%@\n",_strongMStr,_strongMStr);
NSLog(@"weakMStr輸出:%p,%@\n",_weakMStr,_weakMStr);

輸出結果
//2016-09-01 15:41:33.793 lbCopy[1247:100742] mstrOrigin輸出:0x7874d140,mstrOriginChange2
//2016-09-01 15:41:33.793 lbCopy[1247:100742] strongMStr輸出:0x0,(null)
//2016-09-01 15:41:33.794 lbCopy[1247:100742] weakMStr輸出:0x0,(null)

可見之前引用計數2是mstrOrigin和strongMStr添加的。
結論:copy會重新開闢新的內存來保存一份相同的數據。被賦值對象和原值修改互不影響。strong和weak雖然都指向原來數據地址,原值修改的時候storng和weak會隨之變化。區別是前者會對數據地址進行引用計數+1防止原地址值被釋放,但後者不會,當其他值都不在指向值地址時,值地址被釋放,weak的值也就是爲nil了。我們稱會對數據地址增加引用計數的爲強引用,不改變引用計數的爲弱引用

1.2 assign和weak的區別

對assign和weak修飾的值進行賦值,並輸出指針結構地址和值

self.assignMStr= mstrOrigin;
self.weakMStr= mstrOrigin;
mstrOrigin = [[NSMutableStringalloc]initWithString:@"mstrOriginChange3"];
NSLog(@"weakMStr輸出:%p,%@\n",_weakMStr,_weakMStr);
NSLog(@"assignMStr輸出:%p,%@\n",self.assignMStr,self.assignMStr);

可以發現在輸出assignMStr時會偶爾出現奔潰的情況。原因是發送了野指針的情況。assign同weak,指向C並且計數不+1,但當C地址引用計數爲0時,assign不會對C地址進行B數據的抹除操作,只是進行值釋放。這就導致野指針存在,即當這塊地址還沒寫上其他值前,能輸出正常值,但一旦重新寫上數據,該指針隨時可能沒有值,造成奔潰。

1.3那retain是什麼

ARC之前屬性構造器的關鍵字是retain,copy,assign,strong和weak是ARC帶出來的關鍵字。
retain現在同strong,就是指針指向值地址,同時進行引用計數加1。

2.非NSMutableString的情況



上面我們討論了典型的例子NSMutableString,即非容器可變變量。也就是說還存在其他三種類型需要討論...

1.非容器不可變變量NSSting
2.容器可變變量NSMutableArray
3.容器不可變變量NSArray

更重要的是不同類型會有不同結果...,好吧,不要奔潰,上面一大段我們討論了1/4,接下來我們要討論其他的3/4情況。但好消息是,其他幾種情況基本與上面非容器可變變量情況基本類似。

2.1容器可變變量

容器可變變量的典型例子就是NSMutableArray
下面代碼可以忽略,只做參考用

@property(copy,nonatomic)NSMutableArray*aCopyMArr;
@property(strong,nonatomic)NSMutableArray*strongMArr;
@property(weak,nonatomic)NSMutableArray*weakMArr;

NSMutableArray*mArrOrigin = [[NSMutableArrayalloc]init];
NSMutableString*mstr1 = [[NSMutableStringalloc]initWithString:@"value1"];
NSMutableString*mstr2 = [[NSMutableStringalloc]initWithString:@"value2"];
NSMutableString*mstr3 = [[NSMutableStringalloc]initWithString:@"value3"];

[mArrOriginaddObject:mstr1];
[mArrOriginaddObject:mstr2];

//將mArrOrigin拷貝給aCopyMArr,strongMArr,weakMArr
self.aCopyMArr= mArrOrigin;
self.strongMArr= mArrOrigin;
self.weakMArr= mArrOrigin;

NSLog(@"mArrOrigin輸出:%p,%@\n", mArrOrigin,mArrOrigin);
NSLog(@"aCopyMArr輸出:%p,%@\n",_aCopyMArr,_aCopyMArr);
NSLog(@"strongMArr輸出:%p,%@\n",_strongMArr,_strongMArr);
NSLog(@"weakMArr輸出:%p,%@\n",_weakMArr,_weakMArr);
NSLog(@"weakMArr輸出:%p,%@\n",_weakMArr[0],_weakMArr[0]);
NSLog(@"mArrOrigin中的數據引用計數%@", [mArrOriginvalueForKey:@"retainCount"]);
NSLog(@"%p %p %p %p",&mArrOrigin,mArrOrigin,mArrOrigin[0],mArrOrigin[1]);

//以下是輸出
2016-09-02 20:42:30.777 lbCopy[4207:475091] mArrOrigin輸出:0x78f81680,(
value1,
value2
)
2016-09-02 20:42:30.777 lbCopy[4207:475091] aCopyMArr輸出:0x7a041340,(
value1,
value2
)
2016-09-02 20:42:30.777 lbCopy[4207:475091] strongMArr輸出:0x78f81680,(
value1,
value2
)
2016-09-02 20:42:30.777 lbCopy[4207:475091] weakMArr輸出:0x78f81680,(
value1,
value2
)
2016-09-02 20:42:30.777 lbCopy[4207:475091] weakMArr輸出:0x78f816a0,value1
2016-09-02 20:42:30.778 lbCopy[4207:475091] mArrOrigin中的數據引用計數(
3,
3
)
2016-09-02 20:42:30.778 lbCopy[4207:475091] 0xbffb4098 0x78f81680 0x78f816a0 0x78f81710
//以上是輸出

//給原數組添加一個元素
[mArrOriginaddObject:mstr3];

NSLog(@"mArrOrigin輸出:%p,%@\n", mArrOrigin,mArrOrigin);
NSLog(@"aCopyMArr輸出:%p,%@\n",_aCopyMArr,_aCopyMArr);
NSLog(@"strongMArr輸出:%p,%@\n",_strongMArr,_strongMArr);
NSLog(@"weakMArr輸出:%p,%@\n",_weakMArr,_weakMArr);
NSLog(@"mArrOrigin中的數據引用計數%@", [mArrOriginvalueForKey:@"retainCount"]);

//修改原數組中的元素,看是否有隨之變化
[mstr1appendFormat:@"aaa"];

NSLog(@"mArrOrigin輸出:%p,%@\n", mArrOrigin,mArrOrigin);
NSLog(@"aCopyMArr輸出:%p,%@\n",_aCopyMArr,_aCopyMArr);
NSLog(@"strongMArr輸出:%p,%@\n",_strongMArr,_strongMArr);
NSLog(@"weakMArr輸出:%p,%@\n",_weakMArr,_weakMArr);

//以下是輸出
2016-09-02 20:42:30.778 lbCopy[4207:475091] mArrOrigin輸出:0x78f81680,(
value1,
value2,
value3
)
2016-09-02 20:42:30.778 lbCopy[4207:475091] aCopyMArr輸出:0x7a041340,(
value1,
value2
)
2016-09-02 20:42:30.778 lbCopy[4207:475091] strongMArr輸出:0x78f81680,(
value1,
value2,
value3
)

2016-09-02 20:42:30.778 lbCopy[4207:475091] weakMArr輸出:0x78f81680,(
value1,
value2,
value3
)
2016-09-02 20:42:30.779 lbCopy[4207:475091] mArrOrigin中的數據引用計數(
3,
3,
2
)
2016-09-02 20:42:30.779 lbCopy[4207:475091] mArrOrigin輸出:0x78f81680,(
value1aaa,
value2,
value3
)
2016-09-02 20:42:30.779 lbCopy[4207:475091] aCopyMArr輸出:0x7a041340,(
value1aaa,
value2
)
2016-09-02 20:42:30.779 lbCopy[4207:475091] strongMArr輸出:0x78f81680,(
value1aaa,
value2,
value3
)
2016-09-02 20:42:30.779 lbCopy[4207:475091] weakMArr輸/出:0x78f81680,(
value1aaa,
value2,
value3
)
//以上是輸出

上面代碼有點多,所做的操作是mArrOrigin(value1,value2)賦值給copy,strong,weak修飾的aCopyMArr,strongMArr,weakMArr。通過給原數組增加元素,修改原數組元素值,然後輸出mArrOrigin的引用計數,和數組地址,查看變化。
發現其中數組本身指向的內存地址除了aCopyMArr重新開闢了一塊地址,strongMArr,weakMArr和mArrOrigin指針指向的地址是一樣的。也就是說

容器可變變量中容器本身和非容器可變變量是一樣的,copy深拷貝,strongMArr,weakMArr和assign都是淺拷貝

另外我們發現被拷貝對象mArrOrigin中的數據引用計數居然不是1而是3。也就是說容器內的數據拷貝都是進行了淺拷貝。同時當我們修改數組中的一個數據時strongMArr,weakMArr,aCopyMArr中的數據都改變了,說明

容器可變變量中的數據在拷貝的時候都是淺拷貝

容器可變變量的拷貝結構如下圖

   

NSMutableArray拷貝示意圖3

2.2非容器不變變量

典型例子是NSString

我們還是以代碼引出結果

@property(copy,nonatomic)NSString*aCopyStr;
@property(strong,nonatomic)NSString*strongStr;
@property(weak,nonatomic)NSString*weakStr;
@property(assign,nonatomic)NSString*assignStr;

NSLog(@"\n\n\n\n------------------不可變量實驗------------------------");

NSString*strOrigin = [[NSStringalloc]initWithUTF8String:"string 1"];

self.aCopyStr= strOrigin;

self.strongStr= strOrigin;

self.weakStr= strOrigin;

NSLog(@"strOrigin輸出:%p,%@\n", strOrigin,strOrigin);

NSLog(@"aCopyStr輸出:%p,%@\n",_aCopyStr,_aCopyStr);

NSLog(@"strongStr輸出:%p,%@\n",_strongStr,_strongStr);

NSLog(@"weakStr輸出:%p,%@\n",_weakStr,_weakStr);

NSLog(@"------------------修改原值後------------------------");

strOrigin =@"aaa";

NSLog(@"strOrigin輸出:%p,%@\n", strOrigin,strOrigin);

NSLog(@"aCopyStr輸出:%p,%@\n",_aCopyStr,_aCopyStr);

NSLog(@"strongStr輸出:%p,%@\n",_strongStr,_strongStr);

NSLog(@"weakStr輸出:%p,%@\n",_weakStr,_weakStr);

NSLog(@"------------------結論------------------------");

NSLog(@"strOrigin值值爲改變,但strOrigin和aCopyStr指針地址和指向都已經改變,說明不可變類型值不可被修改,重新初始化");

self.aCopyStr=nil;

self.strongStr=nil;

NSLog(@"strOrigin輸出:%p,%@\n", strOrigin,strOrigin);

NSLog(@"aCopyStr輸出:%p,%@\n",_aCopyStr,_aCopyStr);

NSLog(@"strongStr輸出:%p,%@\n",_strongStr,_strongStr);

NSLog(@"weakStr輸出:%p,%@\n",_weakStr,_weakStr);

NSLog(@"------------------結論------------------------");

NSLog(@"當只有weakStr擁有C時,值依舊會被釋放,同非容器可變變量");


//以下是輸出

------------------不可變量實驗------------------------

2016-09-02 21:08:44.053 lbCopy[4297:488549] strOrigin輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.053 lbCopy[4297:488549] aCopyStr輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.054 lbCopy[4297:488549] strongStr輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.054 lbCopy[4297:488549] weakStr輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.054 lbCopy[4297:488549] strOrigin值內存引用計數3

2016-09-02 21:08:44.054 lbCopy[4297:488549] ------------------修改原值後------------------------

2016-09-02 21:08:44.054 lbCopy[4297:488549] strOrigin輸出:0x8c1f8,aaa

2016-09-02 21:08:44.054 lbCopy[4297:488549] aCopyStr輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.054 lbCopy[4297:488549] strongStr輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.055 lbCopy[4297:488549] weakStr輸出:0x7a2550d0,string 1

2016-09-02 21:08:44.055 lbCopy[4297:488549] ------------------結論------------------------

2016-09-02 21:08:44.055 lbCopy[4297:488549] strOrigin值值爲改變,但strOrigin和aCopyStr指針地址和指向都已經改變,說明不可變類型值不可被修改,重新初始化

2016-09-02 21:08:44.059 lbCopy[4297:488549] strOrigin輸出:0x8c1f8,aaa

2016-09-02 21:08:44.059 lbCopy[4297:488549] aCopyStr輸出:0x0,(null)

2016-09-02 21:08:44.060 lbCopy[4297:488549] strongStr輸出:0x0,(null)

2016-09-02 21:08:44.060 lbCopy[4297:488549] weakStr輸出:0x0,(null)

2016-09-02 21:08:44.060 lbCopy[4297:488549] ------------------結論------------------------

2016-09-02 21:08:44.061 lbCopy[4297:488549]當只有weakStr擁有C時,值依舊會被釋放,同非容器可變變量
//以上是輸出

此處我們將strOrigin拷貝給aCopyStr,strongStr,weakStr,然後輸出他們的值地址,發現他們四個的值地址一樣,且strOrigin值的引用計數爲3。修改strOrigin和發現strOrigin值地址改變,其他三個值地址不變,將aCopyStr,strongStr設爲nil後,發現weakStr隨之nil。

綜合上面現象NSString和NSMutableString(非容器可變變量)基本相同,除了copy,NSString爲淺拷貝,NSMutableString是深拷貝。那麼爲什麼NSString的copy是淺拷貝呢,也就是說爲什麼aCopyStr不自己開闢一個獨立的內存出來呢。答案很簡單,因爲不可變量的值不會改變,既然都不會改變,所以沒必要重新開闢一個內存出來讓aCopyStr指向他,直接指向原來值位置就可以了。示意圖如下


NSString拷貝示意圖4

所以非容器不可變量除了copy其他特性同非容器可變變量,copy是淺拷貝

2.3不可變容器變量

典型對象NSArray。該對象實驗自行實驗。但結論在這裏給出,其實不實驗也可以大概知道概率
在不可變容器變量中,容器本身都是淺拷貝包括copy,同NSString,容器裏面的數據都是淺拷貝,同NSMutableArray。

3.總結
copy,strong,weak,assign的區別。

可變變量中,copy是重新開闢一個內存,strong,weak,assgin後三者不開闢內存,只是指針指向原來保存值的內存的位置,storng指向後會對該內存引用計數+1,而weak,assgin不會。weak,assgin會在引用保存值的內存引用計數爲0的時候值爲空,並且weak會將內存值設爲nil,assign不會,assign在內存沒有被重寫前依舊可以輸出,但一旦被重寫將出現奔潰

不可變變量中,因爲值本身不可被改變,copy沒必要開闢出一塊內存存放和原來內存一模一樣的值,所以內存管理系統默認都是淺拷貝。其他和可變變量一樣,如weak修飾的變量同樣會在內存引用計數爲0時變爲nil。

容器本身遵守上面準則,但容器內部的每個值都是淺拷貝。

綜上所述,當創建property構造器創建變量value1的時候,使用copy,strong,weak,assign根據具體使用情況來決定。value1 = value2,如果你希望value1和value2的修改不會互相影響的就用用copy,反之用strong,weak,assign。如果你還希望原來值C(C是什麼見示意圖1)爲nil的時候,你的變量不爲nil就用strong,反之用weak和assign。weak和assign保證了不強引用某一塊內存,如delegate我們就用weak表示,就是爲了防止循環引用的產生。
另外,我們上面討論的是類變量,直接創建局部變量默認是Strong修飾

補充:delegate爲什麼要用weak或者assign而不用strong

a創建對象b,b中有C類對象c,所以a對b有一個引用,b對c有一個引用,a.b引用計數分別爲1,1。當c.delegate = b的時候,實則是對a有了一個引用,如果此時c的delegate用strong修飾則會對b的值內存引用計數+1,b引用計數爲2。當a的生命週期結束,隨之釋放對b的引用,b的引用計數變爲1,導致b不能釋放,b不能釋放又導致b對c的引用不能釋放,c引用計數還是爲1,這樣就造成了b和c一直留在了內存中。

而要解決這個問題就是使用weak或者assign修飾delegate,這樣雖然會有c仍然會對b有一個引用,但是引用是弱引用,當a生命週期結束的時候,b的引用計數變爲0,b釋放後隨之c的引用消失,c引用計數變爲0,釋放。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章