集合深淺拷貝以及經常遇到的坑(面試常問)

之前只是知道淺拷貝只是拷貝地址,深拷貝是內容的拷貝,具體怎麼使用,卻不是很清楚。這篇文章對深淺拷貝做了通俗易懂的講解,還列出了作者踩過的坑,希望對讀者有一些幫助。

引言

根據拷貝內容的不同,分爲深淺拷貝

  • 深拷貝:指針賦值,且內容拷貝
  • 淺拷貝:只是簡單的指針賦值

蘋果爲什麼這麼設計呢?總結起來很簡單:即安全又省內存。但是要理解或者避免踩一些坑,還需要看下面的介紹

內存

不得不先說到內存,又不得不說內存分區:程序底層——程序如何在RAM ROM運行,內存分配與分區

看下面圖片:

這裏寫圖片描述

obj1是定義在函數外部的全局變量,處於全局區;obj2是定義在函數內的局部變量,處於棧區。它們都指向了處於堆區的對象。

obj1obj2是指針,它們指向的對象是內容,那麼現在再看深淺拷貝的現象,或者說執行的結果:淺拷貝只是多個指針指向同一對象內容,深拷貝就是每個指針都指向了一個對象內容,互不影響。

自定義對象需要自己實現NSCoping協議,一般情況下,自定義對象都是可變對象,本節討論的也都是針對系統對象
指針也是會存在堆區的,比如在block裏面我們知道,如果指針使用了__block修飾,那麼指針會存放在堆區。

返回值的一些基本規則

無論是集合對象還是非集合對象,在收到copy和mutableCopy消息時,都遵守以下規則:

  • 1 copy返回immutable對象;
  • 2 mutableCopy返回mutable對象;

那麼很簡單,可變與不可變對象的轉變:

  • 不可變對象→可變對象的轉換:不可變對象.mutableCopy。
  • 可變->不可變:可變對象.copy;

集合拷貝

系統提供的集合類型,比如字典、數組、NSSet等集合類型內存基本都是如下結構:集合內存結構圖

這裏寫圖片描述

我們可以上面代碼(代碼處於方法內)做個分析,加深對內存的理解。@"123"、@"456"是const屬性,因此處於常量區,指針str1、str2、arr局部變量指針處於棧區,@[]數組內容存放位置處於堆區,數組裏面的內容存放的是指針str1與str2,當然處於堆區

其實arr = @[str1,str2]相當於[arr addObject:str1];[arr addObject:str2];,數組裏面有兩個強指針指向了對象@"123"與@"456"

圖中只是字符串是常量所以在常量區,如果他們是NSDateUIView等等則會處於堆區

下面的分析也是基於三種程度的拷貝,記爲CopyLevel,拷貝層次,簡寫CL1、CL2、CL3

  • CL1:arr數組指針,如果只發生這層拷貝,則和非集合對象一樣,是淺拷貝
  • CL2:arr數組指針指向的的內容,即存儲的對象指針。發生本層拷貝,從非集合角度來說已經發生了內容拷貝,即深拷貝。但從集合角度來說,還是淺拷貝。
  • CL3:arr數組裏面存儲的指針指向的內容,如果發生本層拷貝,可以叫做集合的單層深拷貝。
    毫無疑問,CL1是肯定會進行的。重點就在於CL2CL3.

不可變集合的copymutableCopy

下面代碼,不可變集合arrM1copymutableCopyarrM2:mutableCopy,arr:copy

這裏寫圖片描述

  • 根據第一行打印結果:arrM2arr都進行CL1拷貝
  • 第二行打印結果:arrM2arrM1結果不同,說明進行了數組拷貝;arrarrM1結果相同,說明沒有,進行數組拷貝
  • 第三行打印結果:都相同,說明指向的內容沒有發生拷貝
    可變集合的copymutableCopy

下面代碼,可變集合arrM1copymutableCopyarrM2:mutableCopy,arr:copy

這裏寫圖片描述

  • 根據第一行打印結果:arrM2和arr都進行CL1拷貝
  • 第二行打印結果:結果均不同,說明都進行了數組拷貝;
  • 第三行打印結果:都相同,說明指向的內容沒有發生拷貝

一般結論

我們知道,對於非集合對象,有如下結論:

// 不可變,線程安全
[immutableObject copy] // 淺複製
[immutableObject mutableCopy] // 深複製,對於集合則是隻拷⻉貝數組的內容,數組的內容是指針,而指針的內容不會被拷⻉

// 可變對象,線程不安全
[mutableObject copy] //深複製,對於集合則是隻拷⻉貝數組的內容,數組的內容是指針,而指針的內容不會被拷⻉
[mutableObject mutableCopy] //深複製,對於集合則是隻拷⻉貝數組的內容,數組的內容是指針,而指針的內容不會被拷⻉

集合的單層深拷貝,CL3層的拷貝(one-level-deep copy)

我們需要使用- (instancetype)initWithArray:(NSArray<ObjectType> *)array copyItems:(BOOL)flag;方法,且flag爲YES

這裏寫圖片描述

可以看到,三行打印結果都不一樣,即發生了CL3層的拷貝。

此方法執行後,arrM1集合裏的每個對象都會收到 copyWithZone: 消息。如果集合裏的對象遵循 NSCopying 協議,那麼對象就會被深拷貝到新的集合,如果沒有遵循就直接崩潰了。

等一等,好像有另一個問題:此方法只是會給集合的每個對象發送copyWithZone:方法,那麼對於不可變對象,copyWithZone:的執行還是淺拷貝。讀者大概也注意到了,圖中示例代碼,arrM1數組存的也是可變對象dict1,所以有CL3層的拷貝。那如果arrM1存的不是可變對象呢?結果就是沒有CL3層的拷貝,大家可以用代碼測試下!

爲啥叫單層深複製呢? 因爲它只給arrM1數組存的對象發送了copyWithZone:方法,而沒有對dict1發送copyWithZone:方法,dict1也是集合,它裏面也存放着對象呢。。。即集合裏面存放的集合。。。好繞,哈哈

另外,除了此方法,集合的解檔歸檔,也是可以實現單層深拷貝的。

繞的東西就到這裏,下面看些感興趣的東西:

一些坑

  • Mutable變copy的坑

有一點需要注意了:copy返回值爲不可變對象,如果使用可變對象的接口就會crash。例如:

- (void)arrMCopyTest {
    NSMutableArray *arrM = [NSMutableArray arrayWithObjects:@"123",@"456", nil];
    NSMutableArray *arr = [arrM copy];
    // 下面代碼崩潰
    [arr addObject:@"789"];
}

[arrM copy];返回的是不可變類型,即NSArray,向一個NSArray對象發送addObject消息當然方法找不到崩潰。

另一個問題,arr是NSMutableArray類型,它指向父類NSArray編譯器爲什麼不報錯呢?copy返回的是id類型,編譯器不會對id(俗稱萬能指針)進行類型檢查,所以會經常看到推薦使用instancetype,而不是id

下面的類似錯誤就很常見了:

@property (nonatomic, copy) NSMutableArray *arr;

- (void)arrMCopyTest {
    NSMutableArray *arrM = [NSMutableArray arrayWithObjects:@"123",@"456", nil];
    self.arr = arrM;
    // 下面代碼崩潰
    [self.arr addObject:@"789"];
}

因爲self.arr爲copy修飾,那麼self.arr = arrM就相當於_arr = [arrM copy]

  • 屬性指定爲copy,卻沒有被copy
@property (nonatomic, copy) NSString *str;
- (void)viewDidLoad {
    NSMutableString *str = [NSMutableString stringWithFormat:@"123"];
    // self.str = str;
    _str = str;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        [str appendString:@"456"];
        NSLog(@"change");
    });
}

這裏在block裏面對str進行操作,居然沒有對它進行__block修飾!!!感興趣可以看看這篇博客:iOS中block的使用、實現底層、循環引用、存儲位置

打印結果:

2017-07-23 00:33:06.344 CopyTest[95611:31912803] 123
2017-07-23 00:33:07.518 CopyTest[95611:31912803] change
2017-07-23 00:33:08.636 CopyTest[95611:31912803] 123456

都是123456,self.str被意外改變了,如果將代碼_str = str;-->self.str = str;值就不會改變了。因爲相當於_str = [str copy];

所以建議除了在初始化時(init方法中),蘋果推薦我們使用_下劃線的方式直接訪問變量,其它地方儘量使用self.來訪問。另外我們還經常getter或者setter方法裏面做一些自定義操作,如果_方式則這些自定義操作就不會被執行。而且在block裏面使用_方式訪問變量會更隱蔽的引起循環引用的問題!

  • setter方法
@property (nonatomic, copy) NSString *str;

- (void)setStr:(NSString *)str {
    // _str = str; 不要這樣寫
    _str = [str copy];
}

講了這些,大家會不會猛然想到

@property (nonatomic, weak) id delegate;
_delegate = obj;

這樣會不會造成_delegate爲指向的對象引用計數爲0時,系統還會不會將_delegate置爲nil?答案是,您多慮了,會的。這和copy不一樣。爲啥不一樣?牽涉到runtime哈希表什麼的就不在展開了。。。


原文:http://www.cnblogs.com/mddblog/p/7236138.html#_label0

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