1. 程式人生 > >NSArray與NSMutableArray應該使用copy還是strong?

NSArray與NSMutableArray應該使用copy還是strong?

先說標題的正確答案:

@property (nonatomic,copy) NSArray *immutableArray;

@property (nonatomic,strong) NSMutableArray *mutableArray;

其他集合:

NSArray,NSDictionary,NSSet -> copy
NSMutableArray,NSMutableDictionary,NSMutableSet->strong
``

    Objective-C中物件的拷貝分為深拷貝和淺拷貝。另外還有容器類物件及非容器類物件的差別: 
1. 對非容器類物件(如NSString、NSMutableString類物件)使用淺拷貝:拷貝的是物件的地址,沒有新的記憶體被分配,只是原來的那塊內容多了一個指標指向。也就是說新物件跟原物件都是指向的同一個記憶體地址,那麼內容當然一樣。 
2. 對非容器類物件(如NSData、NSMutableData類物件)使用深拷貝:拷貝的是整個物件內容了,是通過給新物件分配了一塊新的記憶體,然後將原物件對應記憶體中的內容一模一樣在新的記憶體中寫一份。所以內容是一樣的,但是此時新物件與原物件的記憶體地址是不同的。 
3. 對容器類物件(如NSArray、NSMutableArray類物件)使用淺拷貝:新的容器類物件也是指向的新的記憶體地址,但是容器內儲存的物件沒有進行拷貝,指向的記憶體地址還是和原容器物件內儲存的物件指向的記憶體地址是一樣的。也就是說你改了其中一個容器物件中的元素物件,那麼另一個容器物件中的元素物件也會相應修改(是同一個記憶體地址嘛)。 
4. 對容器類物件(如NSDictionary、NSMutableDictionary類物件)使用深拷貝:是需要對容器物件中的每一個元素都進行拷貝

**重點

在ARC下,我們是不可以對物件呼叫retain方法修改記憶體的引用計數的。我們需要先理解一下MRC下的retain、copy和mutableCopy的特點:

retain:始終是淺拷貝,讓新物件指標指向原物件,只是原來的記憶體地址多了一個指標指向,引用計數增加了1(但是系統會進行各種優化,不一定會加,像常量的引用計數就一直保持-1,不會變動,所以對常量string,進行retain也還是不會變)。返回物件是否可變與被複制的物件保持一致。(與ARC中的strong一樣) 
 
copy:對於可變物件為深複製(開闢新記憶體,與原物件指向的不是一個物件了);對於不可變物件是淺複製(不開闢新記憶體,只是原記憶體地址加了一個新的指標指向,引用計數加1)。返回的物件始終是一個不可變物件。 
 
mutableCopy:始終是深複製(開闢新記憶體,與原來物件指向的記憶體空間不是同一處了)。返回的物件始終是一個可變物件。


retain始終是淺拷貝(記憶體地址指向的是同一處);

copy對於不可變物件是淺拷貝(記憶體地址指向同一處,引用計數加1(常量區資料固定為-1),指向的是同一個物件)
copy對於可變物件是深拷貝(開闢新記憶體,已經不是指向同一個記憶體空間同一個物件了)
所以NSArray應該使用copy
// 不可變物件使用copy
@property(nonatomic, copy) NSArray *imutableArray;


就算是將可變物件使用setter方法賦值給它,也不怕因為原來的可變物件改變,造成新的物件也改變。因為,對可變物件使用copy方法賦值給新物件的時候,使用的是深拷貝(copy在可變物件呼叫的時候是深拷貝)。所以兩個物件已經不是同一個了,你改了也不關我什麼事。

如果是將不可變物件賦值給它的話,雖然這個時候進行的是淺拷貝,新指標指向的記憶體地址跟就物件是一樣的(copy在不可變物件呼叫的時候是淺拷貝,大概是省得佔用新記憶體的意思?),但是同樣不用擔心由於一個物件的改變,造成另一個物件也改變,因為原來的物件本來就是不可變物件嘛,另外新物件使用copy返回的,copy返回的都是不可變的物件呀。

但是NSArray使用strong修飾的話就可能有問題了
如果你打算宣告一個不可變的NSArray,這樣使用strong宣告:

// 不可變物件使用copy
@property(nonatomic, strong) NSArray *array;

ARC裡面的strong就類似MRC中retain是一樣的。這樣在setter方法賦值的時候,是使用淺拷貝的,也就是指標指向同一個地址,而且可變與不可變根據你用來賦值的物件而定的。

所以如果使用了下面的方式對這個strong的array賦值的話就會出問題:

NSMutableArray *mutableArray = [NSMutableArray array];
self.array = mutableArray;

這裡我們是將一個NSMutableArray型別的,可變陣列型別的物件賦值給了array屬性。而且屬性使用的是strong修飾。所以內部實現是:先保留新值(防止新舊值相同,先進行release的話,再retain會出錯)、再release舊值、最後retain新值。 
簡單來說,最後的屬性指向的是mutableArray這個物件的同一塊地址。然而這塊地址是可以通過修改mutableArray物件而修改的,這樣就會造成array被同步修改。所以會問題的。

所以NSArray應該使用copy!

copy方法拷貝的,不管是可變物件還是不可變物件,最終返回的都是不可變的物件
NSMutableArray應該使用strong
如果ARC下有一個NSMutableArray屬性是使用copy這樣定義的:

@property(nonatomic, copy) NSMutableArray *mutableArray;

那麼如果你在程式碼中這樣呼叫set方法,那麼會有如下的問題:

// 這樣呼叫set方法
self.mutableArray = [NSMutableArray array];
// 隨後這樣呼叫
[self.mutableArray addObject:@"XXX"]; // 這裡是會報錯的,提示這個物件沒有這個方法

這個就是我們前面講過的,對可變陣列進行了深複製,但是copy方法返回的始終是不可變的物件。也就是說self.mutableArray = [NSMutableArray array];這句程式碼在你使用copy宣告屬性的情況下,對應的set方法的實現其實是通過_mutableArray = [[NSMutableArray array] copy];這樣一句語句實現的。所以你會發現我們定義的“可變的”陣列,其實事實上是不可變的!所以自然也就不能呼叫addObject:的方法了!

所以,在需要可變陣列屬性可以改變的時候,需要使用strong修飾屬性!

mutableCopy方法拷貝的,不管是可變物件還是不可變物件,最後返回的都是可變物件
比如,如下程式碼中的tempArray雖然宣告的時候寫的是NSArray的不可變型別,也是對不可變型別imutableArr執行的mutableCopy方法,但是最後得到的物件還是可以執行replaceObjectAtIndex:withObject:方法的.(當然直接呼叫是不能的,但是執行時呼叫還是可以的,最後看的是執行時的物件型別):

// 不可變型別
NSArray *imutableArr = [NSArray arrayWithObjects:@"111", @"222", @"333", nil];
// 呼叫mutableCopy方法,得到的是可變型別的(執行時就知道)
NSArray *tempArray = [imutableArr mutableCopy];

// 這裡是呼叫執行時的objc_msgSend方法
((void (*) (id, SEL, NSUInteger, NSObject *)) objc_msgSend)(tempArray, @selector(replaceObjectAtIndex:withObject:), 0, @"XXXX");

同時這裡也給了我們怎樣使用performSelector傳遞多個引數(尤其是基礎型別引數)的方法(直接呼叫底層的msgSend方法)。
 

討論兩個問題:
1. 使用strong修飾NSArray會有什麼問題?
2. 使用copy修飾NSMutableArray會有什麼問題?

1. 使用strong修飾NSArray的問題

#import <Foundation/Foundation.h>

@interface StrongCopyTest : NSObject

@property (nonatomic,strong) NSArray *immutableArray;

@property (nonatomic,strong) NSMutableArray *mutableArray;

- (void)doTest;

@end
#import "StrongCopyTest.h"

@implementation StrongCopyTest

- (void)doTest
{
    self.mutableArray = [NSMutableArray arrayWithObject:@"111"];
    self.immutableArray = [NSArray array];
    
    NSLog(@"before mutableArray=%@,immutableArray=%@",self.mutableArray,self.immutableArray);
    
    self.immutableArray = self.mutableArray;
    [self.mutableArray addObject:@"222"];
    
    NSLog(@"after mutableArray=%@,immutableArray=%@",self.mutableArray,self.immutableArray);
    
}
@end

輸出結果如下

2018-05-14 17:28:15.872188+0800 StudyRuntime[861:40479] before mutableArray=(
    111
),immutableArray=(
)
2018-05-14 17:28:15.872308+0800 StudyRuntime[861:40479] after mutableArray=(
    111,
    222
),immutableArray=(
    111,
    222
)
Program ended with exit code: 0

可以看出,immutabeArray和mutableArray都被修改了,immutableArray不是預料的結果。

原因分析:
self.immutableArray = self.mutableArray;這行程式碼將可變陣列的指標賦值給了不可變陣列指標,由於OC中都是C的指標操作,並且是弱型別,因此這是合法的,編譯器不會報錯。甚至一個 id x = self.mutableArray也是可以的,因此弱型別是有潛在風險的。此時immutableArray和mutableArray都是指向了同一個記憶體地址,即mutableArray的記憶體地址,因此後面修改mutableArray的資料也會導致immutableArray的資料修改,因為是同一塊記憶體地址。
這是一個陷阱。

2. 使用copy修飾NSMutableArray會有什麼問題?

#import <Foundation/Foundation.h>

@interface StrongCopyTest : NSObject

//@property (nonatomic,copy) NSArray *immutableArray;

@property (nonatomic,copy) NSMutableArray *mutableArray;

- (void)doTest;

@end
#import "StrongCopyTest.h"

@implementation StrongCopyTest

- (void)doTest
{
    self.mutableArray = [NSMutableArray arrayWithObject:@"111"];
//    self.immutableArray = [NSArray array];
//    
//    NSLog(@"before mutableArray=%@,immutabxrleArray=%@",self.mutableArray,self.immutableArray);
//    
//    self.immutableArray = self.mutableArray;
    [self.mutableArray addObject:@"222"];
    
    //NSLog(@"after mutableArray=%@,immutableArray=%@",self.mutableArray,self.immutableArray);
    
}
@end

輸出結果如下

2018-05-14 17:59:37.794905+0800 StudyRuntime[944:61946] -[__NSSingleObjectArrayI addObject:]: unrecognized selector sent to instance 0x10061c540
2018-05-14 17:59:37.796059+0800 StudyRuntime[944:61946] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSSingleObjectArrayI addObject:]: unrecognized selector sent to instance 0x10061c540'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff2a17032b __exceptionPreprocess + 171
    1   libobjc.A.dylib                     0x00007fff517eac76 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff2a208e04 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    3   CoreFoundation                      0x00007fff2a0e6870 ___forwarding___ + 1456
    4   CoreFoundation                      0x00007fff2a0e6238 _CF_forwarding_prep_0 + 120
    5   StudyRuntime                        0x0000000100001b91 -[StrongCopyTest doTest] + 161
    6   StudyRuntime                        0x0000000100001a85 testStrongCopy + 53
    7   StudyRuntime                        0x0000000100001ad5 main + 53
    8   libdyld.dylib                       0x00007fff52404015 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

原因分析:
self.mutableArray = [NSMutableArray arrayWithObject:@“111”];這句程式碼將得到一個不可變的陣列(copy屬性導致的).在此處打一個斷點,然後輸出其資料型別:

(lldb) po self.mutableArray
<__NSSingleObjectArrayI 0x102a01ed0>(
111
)

後面的異常就顯而易見了。

self.mutableArray = [NSMutableArray arrayWithObject:@"111"];
這句話相當於

NSMutableArray *arr =  [NSMutableArray arrayWithObject:@"111"];
self.mutableArray = [arr copy];