談談Objective-C的物件拷貝

STzen發表於2018-05-17

通常我們在使用@property宣告屬性的時候,對於NSStringNSArrayNSDictionary經常會使用copy,以及block的時候也會使用copy,接下來就是和所說copy和mutableCopy。先來思考幾個問題:

  1. copy與mutableCopy有什麼區別?
  2. 使用copy/mutableCopy和直接賦值有什麼區別?
  3. 深淺拷貝的區別?
  4. 自定義物件如何實現NSCopying協議?
  5. block為什麼需要使用copy?

copy和mutableCopy

在需要複製物件的時候,會用到NSObject類提供的copy和mutableCopy方法,通過這兩個方法即可複製已有物件的副本。最常用的是賦值NSString、NSArray、NSDictionary這一類物件,那麼copy和mutableCopy究竟是什麼?它們有何區別?

copy

copy拷貝出來的物件型別總是不可變型別(例如, NSString, NSArray, NSDictionary等等)

mutableCopy

mutableCopy拷貝出來的物件型別總是可變型別(例如, NSMutableString, NSMutableArray, NSMutableDictionary等等)

程式碼舉例:

    NSString * str = @"hello world";
    [str copy]; // 拷貝出內容為hello world的NSString型別的字串
    [str mutableCopy]; // 拷貝出內容為hello world的NSMutableString型別的字串
複製程式碼

列印出類名:

談談Objective-C的物件拷貝

同樣的,對於不可變的NSArray和可變的NSMutableArray來說,這樣的關係總是成立的:

[NSMutableArray copy] => NSArray
[NSArray mutableCopy] => NSMutableArray
複製程式碼

使用copy/mutableCopy和直接賦值有什麼區別?

先看一個例子

    NSMutableArray * arr1 = [NSMutableArray array];
    [arr1 addObject:@"A"];
    
    NSArray * arr2 = [NSArray array];
    arr2 = arr1;
    
    [arr1 addObject:@"C"];
    
    NSLog(@"arr1 = %@", arr1);
    NSLog(@"arr2 = %@", arr2);
複製程式碼

這段程式碼輸入如下:

談談Objective-C的物件拷貝

arr1是可變陣列,arr2是一個不可變陣列,明明可變陣列新增物件在賦值之後,arr2也被影響到了。

如果將arr2 = arr1修改為如下:

arr2 = [arr1 copy];
複製程式碼

然後輸出就正常了

談談Objective-C的物件拷貝

這是為什麼呢?

原因其實是和OC的多型特性有關,表面上arr2是一個NSArray型別的物件,實際上是指向一個NSMutableArray型別的物件,也就是arr1。 我們通過列印arr1arr2兩個物件來看就知道了:

  • 在直接賦值的方式下列印:

    談談Objective-C的物件拷貝

  • 在使用copy的方式下列印:

    談談Objective-C的物件拷貝

一目瞭然,直接賦值之後,arr1arr2完全就是同一個物件,指向同一個地址,所以賦值之後再給arr1新增物件,列印出的結果肯定也是一樣的。而如果使用copy之後賦值,就是兩個完全不一樣的物件,後續的操作也不會有影響。


深拷貝(deep copy)與淺拷貝(shallow copy)的區別?

首先得清楚什麼是深拷貝和淺拷貝?

深拷貝

拷貝出來的物件與原物件地址不一致修改拷貝物件的值對源物件的值沒有任何影響。 深拷貝是直接拷貝整個物件內容到另一塊記憶體中。

淺拷貝

拷貝出來的物件與原物件地址一致修改拷貝物件的值會直接影響源物件的值。

可以用一句話總結:淺複製就是指標拷貝;深複製就是內容拷貝

或許會聽過這樣的說法:copy都是淺拷貝, mutableCopy都是深拷貝 這種淺顯的理解是錯誤的,可以看到之前使用copy的方式下列印出來的物件的地址是不一樣的,是深拷貝,這說明用從一個可變物件copy出一個不可變物件時, 是深拷貝而不是淺拷貝

在Foundation框架中,所有的collectioon類在預設的情況下都執行淺拷貝,也就是說只拷貝容器物件本身,不復制其中的資料。這樣做的目的是,容器內的物件未必都能拷貝,而且呼叫者也未必想在拷貝容器時一併拷貝其中的某個物件。

不過通常情況下,執行的都是淺拷貝,如果你所寫的物件需要深拷貝,那麼可以考慮增加一個專門執行深拷貝的方法。


自定義物件如何實現NSCopying協議

雖然copy方法是在NSObject中的,如果我們自定義一個類(比如Person),向該類的物件傳送copy訊息,會得到如下結果:

    Person *p = [[Person alloc] init];
    Person *p2 = [p copy];
複製程式碼

談談Objective-C的物件拷貝

檢視蘋果官方文件會發現,如果自定義的類要實現copy功能,需要實現copyWithZone方法,(如果想要區分copy和mutableCopy,那麼copyWithZone:應該返回不可變副本,而mutableCopyWithZone:應該返回可變副本)。這個時候可以在Person類中新增如下程式碼:

@implementation Person

- (instancetype)copyWithZone:(NSZone *)zone {
    Person *p = [[Person alloc] init];
    p.age = self.age;
    p.name = self.name;
    return p;
}
複製程式碼

之後就不會報錯,能正常的使用copy了。

但是在蘋果官方文件上還說了一個注意事項:

If a subclass inherits NSCopying from its superclass and declares additional instance variables, the subclass has to override copyWithZone: to properly handle its own instance variables, invoking the superclass’s implementation first.

意思是:如果你的類可以產生子類,那麼copyWithZone:方法將被繼承,子類中也必須重寫copyWithZone:方法,並且要先呼叫父類的copyWithZone:

這個時候在demo中增加一個Person的子類,並增加一個college屬性,那麼父類Person的copyWithZone:方法需要改為:

- (instancetype)copyWithZone:(NSZone *)zone {
    Person *p = [[[self class] allocWithZone:zone] init];
    p.age = self.age;
    p.name = self.name;
    return p;
}
複製程式碼

同時在子類Student中可以這樣實現:

@implementation Student

- (instancetype)copyWithZone:(NSZone *)zone {
    Student *stu = [super copyWithZone:zone];
    if (stu) {
        stu.college = self.college;
    }
    return stu;
}

@end
複製程式碼

如果實現一個類的copyWithZone:方法,而該類的超類也實現了協議,那麼應該先呼叫超類的copy方法以複製繼承來的例項變數,然後加入自己的程式碼以複製想要新增到該類中的任何附加的例項變數。


block中為什麼要使用copy修飾?

在使用block作為屬性的時候,通常使用的是copy

@property (copy) void (^clickBlock)(NSString * name);
複製程式碼

使用copy修飾block其實是從MRC遺留下來的,在MRC時期,作為全域性變數的block在初始化時是被存放在棧區的,這樣在使用時如果block內有呼叫外部變數,那麼block無法保留其記憶體,如果在出了block的初始化作用域內使用,就會引起崩潰,使用copy可以將block的記憶體推入堆中,這樣讓其擁有儲存呼叫的外部變數的記憶體的能力。

在ARC下,對NSStackBLock用strong進行強引用的話,好像會自動對其進行copy一份,變成NSMallocBLock,所以不會crash。在ARC下,其實不使用copy修飾block也是可以的。

詳細的block的實現可以參考唐巧關於block的講解:談Objective-C block的實現


blog

相關文章