原型模式的定義
“使用原型實例指定創建對象的種類,並通過復制這個原型創建新的對象”。最初的定義出現於《設計模式》(Addison-Wesley,1994)。
簡單來理解就是根據這個原型創建新的對象,而且不需要知道任何創建的細節。打個比方,以前生物課上面,有一個知識點叫細胞分裂,細胞在一定條件下,由一個分裂成2個,再由2個分裂成4個……,分裂出來的細胞基於原始的細胞(原型),這個原始的細胞決定了分裂出來的細胞的組成結構。這種分裂過程,可以理解為原型模式。
結構圖
從上圖可以看到,Prototype類中包括一個clone方法,Client調用其拷貝方法clone即可得到實例,不需要手工去創建實例。ConcretePrototype1和ConcretePrototype2為Prototype的子類,實現自身的clone方法,如果Client調用ConcretePrototype1的clone方法,將返回ConcretePrototype1的實例。
淺復制與深復制
- 淺復制:只復制了指針值,並沒有復制指針指向的資源(即沒有創建指針指向資源的副本),復制后原有指針和新指針共享同一塊內存。
- 深復制:不僅復制了指針值,還復制了指針指向的資源。
下面的示意圖左邊為淺復制,右邊為深復制。
Cocoa Touch框架為NSObject的派生類提供了實現深復制的協議,即NSCopying協議,提供深復制的NSObject子類,需要實現NSCopying協議的方法(id)copyWithZone:(NSZone *)zone。NSObject有一個實例方法(id)copy,這個方法默認調用了[self copyWithZone:nil],對於引用了NSCopying協議的子類,必須實現(id)copyWithZone:(NSZone *)zone方法,否則將引發異常,異常信息如下:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Prototype copyWithZone:]: unrecognized selector sent to instance 0x100114d50'
原型模式的示例
新建Prototype類,Prototype.h如下:
1 @interface Prototype : NSObject<NSCopying> 2 3 //設置一個屬性,用來檢測復制的變化 4 5 @property(nonatomic, strong) NSString *name; 6 7 @end
實現深復制,Prototype.m文件如下:
1 #import "Prototype.h" 2 3 @implementation Prototype 4 5 - (id)init 6 7 { 8 9 if (self = [superinit]) 10 11 { 12 13 //初始化Prototype類時,將name設置如下 14 15 self.name = @"My name is Prototype"; 16 17 } 18 19 returnself; 20 21 } 22 23 //實現NSCopying中的方法 24 25 - (id)copyWithZone:(NSZone *)zone 26 27 { 28 29 //調用allocWithZone方法,復制一個新對象 30 31 return [[[selfclass] allocWithZone:zone] init]; 32 33 } 34 35 @end
測試代碼如下:
1 // 創建Prototype實例 prototype 2 3 Prototype *prototype = [[Prototypealloc] init]; 4 5 // 通過prototype深復制出一個新的對象prototypeCopy 6 7 Prototype *prototypeDeepCopy = [prototype copy]; 8 9 // 通過prototype直接賦值,其實就是復制了指針(可以理解為取了個別名),屬於淺復制,引用計數不變 10 11 Prototype *prototypeShallowCopy = prototype; 12 13 // 打印 14 15 NSLog(@"修改前========"); 16 17 NSLog(@"原始對象:%p,%@",prototype, prototype.name); 18 19 NSLog(@"淺復制對象:%p,%@",prototypeShallowCopy, prototypeShallowCopy.name); 20 21 NSLog(@"深復制對象:%p,%@",prototypeDeepCopy,prototypeDeepCopy.name); 22 23 prototype.name = @"My name is new Prototype"; 24 25 // 打印 26 27 NSLog(@"修改后========"); 28 29 NSLog(@"原始對象:%p,%@",prototype, prototype.name); 30 31 NSLog(@"淺復制對象:%p,%@",prototypeShallowCopy, prototypeShallowCopy.name); 32 33 NSLog(@"深復制對象:%p,%@",prototypeDeepCopy,prototypeDeepCopy.name);
輸出結果如下(省略時間及項目名):
修改前========
原始對象:0x1001143f0,My name is Prototype
淺復制對象:0x1001143f0,My name is Prototype
深復制對象:0x1001155a0,My name is Prototype
修改后========
原始對象:0x1001143f0,My name is new Prototype
淺復制對象:0x1001143f0,My name is new Prototype
深復制對象:0x1001155a0,My name is Prototype
【結論】:
- 我們使用copyWithZone:(NSZone *)zone方法實現了深復制,通過copy方法(該方法默認調用copyWithZone方法)復制得到prototypeDeepCopy,從輸出結果來看,內存地址與prototype是不一樣的,另外深復制得到prototypeDeepCopy后,修改prototype的name,對prototypeDeepCopy的name值沒有影響,可判斷為深復制;
- 使用直接賦值得到的prototypeShallowCopy,內存地址與prototype一樣,只是簡單的指針復制,另外從修改了prototype的name值同時也影響了prototypeShallowCopy的name值也可以看出,這種為淺復制。
【說明】:大家看完這個例子,可能感覺怎么和原型模式的結構圖不太一樣?實際上是一樣的,這里的Prototype類相當於是結構圖里面的ConcretePrototype,NSCopying相當於是結構圖里面的Prototype。
assign、copy 和retain
我們還是通過一個示例來說明這三者的區別,定義一個類,類里面只有三個屬性,如下所示:
1 @interface Test : NSObject 2 3 4 5 @property (nonatomic, copy)NSMutableString *strName; 6 7 @property (nonatomic, assign)NSMutableString *strName1; 8 9 @property (nonatomic, retain)NSMutableString *strName2;
調用代碼:
1 Test *t = [[Testalloc] init]; 2 3 NSMutableString *strTest = [[NSMutableStringalloc] initWithString:@"abc"]; 4 5 NSLog(@"strTest retainCount:%ld strTest:%p %@",[strTest retainCount],strTest,strTest); 6 7 t.strName1 = strTest; // assign 8 9 NSLog(@"after assign: strTest retainCount:%ld t.strName1:%p %@ ",[strTest retainCount],t.strName1,t.strName1); 10 11 t.strName = strTest; // copy 12 13 NSLog(@"after copy: strTest retainCount:%ld t.strName:%p %@ ",[strTest retainCount],t.strName,t.strName); 14 15 t.strName2 = strTest; // retain 16 17 NSLog(@"after retain: strTest retainCount:%ld t.strName2:%p %@ ",[strTest retainCount],t.strName2,t.strName2);
輸出結果如下所示(省略時間及項目名):
start: strTest retainCount:1 strTest:0x1001157f0 abc
after assign: strTest retainCount:1 t.strName1:0x1001157f0 abc
after copy: strTest retainCount:1 t.strName:0x100400460 abc
after retain: strTest retainCount:2 t.strName2:0x1001157f0 abc
首先,咱們分析一下這行代碼:NSMutableString *strTest = [[NSMutableStringalloc] initWithString:@"abc"];這行代碼實際上進行了兩個操作:
- 在棧上分配一段內存用來存儲strTest,比如地址為0xAAAA,內容為0x1001157f0;
- 在堆上分配一段內存用來存儲@"abc",地址為0x1001157f0,內容為abc。
現在,咱們針對剛才示例的輸出結果來分別對assign、copy和retain進行說明:
assign:默認值,應用assign后,t.strName1和strTest具有相同的內容0x1001157f0,並且retainCount沒有增加,可以理解t.strName1是strTest的別名;
copy:應用copy后,會在堆上重新分配一段內存來存儲@"abc",地址為0x100400460,同時也會在棧上分配一段內存用來存儲t.strName,比如地址為0xBBBB,內容為0x100400460,這時strTest管理0x1001157f0這段內存;t.strName管理0x100400460這段內存。t.strName和strTest的retainCount均為1。
retain:應用retain后,可以看到retainCount增加了1,說明在棧上重新分配了一段內存來存儲t.strName2,比如地址為0xCCCC,內容為0x1001157f0。此時,strTest和t.strName2共同管理0x1001157f0這段內存。
想必這樣介紹完,大家對於這三個屬性應該是了解的比較清楚了。這里再順便說一下atomic和nonatomic,這兩個屬性用來決定編譯器生成的getter和setter是否為原子操作。
atomic:默認值,提供多線程安全。在多線程環境下,原子操作是必要的,否則有可能引起錯誤的結果。加了atomic,setter函數在操作前會加鎖。
nonatomic:禁用多線程的變量保護,提高性能。
atomic是OC中使用的一種線程保護技術,用來防止在寫操作未完成的時候被另外一個線程讀取,造成數據錯誤。但是這種機制是耗費系統資源的,所以如果沒有使用多線程的通訊編程,那么nonatomic是一個非常好的選擇。
【小思考】:將本示例中的所有NSMutableString替換成NSString后,結果是不一樣的,大家可以試驗一下,然后思考這是為什么?(答案在下一小節會有解說)
IOS中的深復制
像NSString、NSDictionary這些類,本身已經實現了copyWithZone:(NSZone *)zone方法,直接使用如[NSString copy]調用即可。在復制后得到的副本,又可以分為可變副本(mutable copy)和不可變副本(immutable copy)。通常在NSCopying協議規定的方法copyWithZone中返回不可變副本,在NSMutableCopying協議規定的方法mutableCopyWithZone中返回可變副本,然后調用copy和mutableCopy方法來得到相應的不可變和可變副本。
NSString類已經遵循NSCopying協議及NSMutableCopying協議,下面還是通過示例來進行測試。
示例一:
1 NSString *strSource = [NSStringstringWithFormat:@"I am %@",@"ligf"]; 2 3 // 使用copy方法,strSource和strCopy內存地址一致,strSource引用計數加1 4 5 NSString *strCopy = [strSource copy]; 6 7 NSLog(@"原始字符串:%p,%@",strSource,strSource); 8 9 NSLog(@"復制字符串:%p,%@",strCopy,strCopy);
輸出結果:
原始字符串:0x1001156c0,I am ligf
復制字符串:0x1001156c0,I am ligf
【結論】:
由[strSource copy]得到的strCopy,兩者內存地址一致,由於copy返回的是不可變副本,系統只生成一份內存資源,此時的copy只是淺復制,和retain作用一樣。(上一小節小思考里面留下的問題就是這個原因)
示例二:
1 NSString *strSource = [NSStringstringWithFormat:@"I am %@",@"ligf"]; 2 3 // 使用mutableCopy方法,strSource和strCopy內存地址不一致,兩者的引用計數均為1 4 5 NSString *strCopy = [strSource mutableCopy]; 6 7 NSLog(@"原始字符串:%p,%@",strSource,strSource); 8 9 NSLog(@"復制字符串:%p,%@",strCopy,strCopy);
輸出結果:
原始字符串:0x1001156c0,I am ligf
復制字符串:0x100114fb0,I am ligf
【結論】:
由[strSource mutableCopy]得到的strCopy,兩者內存地址不一致,由於mutableCopy返回的是可變副本,系統生成了新的內存資源,此時的mutableCopy是深復制。
【示例三】:
1 NSMutableString *strSource = [NSMutableStringstringWithFormat:@"I am %@",@"ligf"]; 2 3 // NSMutableString使用copy方法,strSource和strCopy內存地址不一致,兩者的引用計數均為1 4 5 NSMutableString *strCopy = [strSource copy]; 6 7 NSLog(@"原始字符串:%p,%@",strSource,strSource); 8 9 NSLog(@"復制字符串:%p,%@",strCopy,strCopy); 10 11 [strCopy appendString:@"hello"];
輸出結果:
原始字符串:0x100115470,I am ligf
復制字符串:0x100115690,I am ligf
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendString:'
【結論】:
由[strSource copy]得到的strCopy,兩者內存地址不一致,即是copy對NSMutableString類型進行了深復制,當嘗試修改strCopy里面的值時,發現報錯了,無法修改,可以確定副本strCopy是不可變副本。
【總的結論】:
對於系統中已經實現的同時支持NSCopying協議和NSMutableCopying協議的NSString、NSDictionary等,copy總是返回不可變副本,mutableCopy總是返回可變副本。
何時用原型模式
- 需要創建的對象應獨立於其類型與創建方式。
- 要實例化的類是在運行時決定的。
- 不想要與產品層次相對應的工廠層次。
- 不同類的實例間的差異僅是狀態的若干組合。因此復制相應數量的原型比手工實例化更加方便。
- 類不容易創建,比如每個組件可以把其他組件作為子節點的組合對象。復制已有的組合對象並對副本進行修改會更加容易。
以下兩種特別常見的情形,我們會想到用原型模式:
- 有很多的相關的類,其行為略有不同,而且主要差異在於內部屬性,如名稱等;
- 需要使用組合(樹)對象作為其他對象的基礎,比如,使用組合對象作為組件來構建另一個組合對象。