IOS設計模式淺析之原型模式(Prototype)


原型模式的定義                                                                                        

  “使用原型實例指定創建對象的種類,並通過復制這個原型創建新的對象”最初的定義出現於《設計模式》(Addison-Wesley,1994)

  簡單來理解就是根據這個原型創建新的對象,而且不需要知道任何創建的細節。打個比方,以前生物課上面,有一個知識點叫細胞分裂,細胞在一定條件下,由一個分裂成2個,再由2個分裂成4……,分裂出來的細胞基於原始的細胞(原型),這個原始的細胞決定了分裂出來的細胞的組成結構。這種分裂過程,可以理解為原型模式。

結構圖                                  

 

  從上圖可以看到,Prototype類中包括一個clone方法,Client調用其拷貝方法clone即可得到實例,不需要手工去創建實例。ConcretePrototype1ConcretePrototype2Prototype的子類,實現自身的clone方法,如果Client調用ConcretePrototype1clone方法,將返回ConcretePrototype1的實例。

淺復制與深復制                            

  • 淺復制:只復制了指針值,並沒有復制指針指向的資源(即沒有創建指針指向資源的副本),復制后原有指針和新指針共享同一塊內存。
  • 深復制:不僅復制了指針值,還復制了指針指向的資源。

  下面的示意圖左邊為淺復制,右邊為深復制。

 

  Cocoa Touch框架為NSObject的派生類提供了實現深復制的協議,即NSCopying協議,提供深復制的NSObject子類,需要實現NSCopying協議的方法(id)copyWithZone:(NSZone *)zoneNSObject有一個實例方法(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后,修改prototypename,對prototypeDeepCopyname值沒有影響,可判斷為深復制;
  • 使用直接賦值得到的prototypeShallowCopy,內存地址與prototype一樣,只是簡單的指針復制,另外從修改了prototypename值同時也影響了prototypeShallowCopyname值也可以看出,這種為淺復制。

  【說明】:大家看完這個例子,可能感覺怎么和原型模式的結構圖不太一樣?實際上是一樣的,這里的Prototype類相當於是結構圖里面的ConcretePrototypeNSCopying相當於是結構圖里面的Prototype

  下載源碼

assigncopy 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

  現在,咱們針對剛才示例的輸出結果來分別對assigncopyretain進行說明:

  assign:默認值,應用assign后,t.strName1strTest具有相同的內容0x1001157f0,並且retainCount沒有增加,可以理解t.strName1strTest的別名;

  copy:應用copy后,會在堆上重新分配一段內存來存儲@"abc",地址為0x100400460,同時也會在棧上分配一段內存用來存儲t.strName,比如地址為0xBBBB,內容為0x100400460,這時strTest管理0x1001157f0這段內存;t.strName管理0x100400460這段內存。t.strNamestrTestretainCount均為1

  retain:應用retain后,可以看到retainCount增加了1,說明在棧上重新分配了一段內存來存儲t.strName2,比如地址為0xCCCC,內容為0x1001157f0。此時,strTestt.strName2共同管理0x1001157f0這段內存。

  想必這樣介紹完,大家對於這三個屬性應該是了解的比較清楚了。這里再順便說一下atomicnonatomic,這兩個屬性用來決定編譯器生成的gettersetter是否為原子操作。

  atomic:默認值,提供多線程安全。在多線程環境下,原子操作是必要的,否則有可能引起錯誤的結果。加了atomicsetter函數在操作前會加鎖。

  nonatomic:禁用多線程的變量保護,提高性能。

  atomicOC中使用的一種線程保護技術,用來防止在寫操作未完成的時候被另外一個線程讀取,造成數據錯誤。但是這種機制是耗費系統資源的,所以如果沒有使用多線程的通訊編程,那么nonatomic是一個非常好的選擇。

  【小思考】:將本示例中的所有NSMutableString替換成NSString后,結果是不一樣的,大家可以試驗一下,然后思考這是為什么?(答案在下一小節會有解說)

  下載源碼

IOS中的深復制                              

  像NSStringNSDictionary這些類,本身已經實現了copyWithZone:(NSZone *)zone方法,直接使用如[NSString copy]調用即可。在復制后得到的副本,又可以分為可變副本(mutable copy)和不可變副本(immutable copy)。通常在NSCopying協議規定的方法copyWithZone中返回不可變副本,在NSMutableCopying協議規定的方法mutableCopyWithZone中返回可變副本,然后調用copymutableCopy方法來得到相應的不可變和可變副本。

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,兩者內存地址不一致,即是copyNSMutableString類型進行了深復制,當嘗試修改strCopy里面的值時,發現報錯了,無法修改,可以確定副本strCopy是不可變副本。

  【總的結論】:

  對於系統中已經實現的同時支持NSCopying協議和NSMutableCopying協議的NSStringNSDictionary等,copy總是返回不可變副本,mutableCopy總是返回可變副本。

何時用原型模式                            

  • 需要創建的對象應獨立於其類型與創建方式。
  • 要實例化的類是在運行時決定的。
  • 不想要與產品層次相對應的工廠層次。
  • 不同類的實例間的差異僅是狀態的若干組合。因此復制相應數量的原型比手工實例化更加方便。
  • 類不容易創建,比如每個組件可以把其他組件作為子節點的組合對象。復制已有的組合對象並對副本進行修改會更加容易。

  以下兩種特別常見的情形,我們會想到用原型模式:

  • 有很多的相關的類,其行為略有不同,而且主要差異在於內部屬性,如名稱等;
  • 需要使用組合(樹)對象作為其他對象的基礎,比如,使用組合對象作為組件來構建另一個組合對象。

  返回目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM