objective-c @property 與指定運算子的關係

小編 列車長 / 2017-05-23 / 發行者 ,
在coding objective-c時,我想都有撰寫以下程式碼的經驗。

code1
app-island.com © 2012, View objc source
  1. @interface MyClass:NSObject
  2. {
  3. NSString *text;
  4. }
  5. @property(nonatomic, retain) NSString *text;
end

接著會在.m檔加入@synthesize的宣告,這樣就完成成員變數NSString *text的完整宣告,接下來就可以安心的使用這個變數。

但coding的人員是否真的清楚加上@property@synthesize是什麼意義嗎?或許大部分的人知道,也了解這就是系統會幫我們實作setter與getter的運算子。但其實這其中還有包含防止memory leak的功用存在。
以前在學習C或JAVA等物件導向程式時,就很清楚的教導我們要使用setter與getter來封裝成員變數,但是在objective-c中,以上的宣告雖然也叫做setter與getter的宣告,但實際上是在處理變數的記憶體問題,利用系統的封裝函式來降低在做物件的賦值時所產生的memory leak的風險。

正式介紹之前,先來看一下下面的程式碼:

code2
app-island.com © 2012, View objc source
  1. text = @"test string";
  2. NSString *myString = @"another string";
  3. text = myString;
end

text是如上面宣告的NSString*

這段程式碼看得出來有什麼問題嗎?其實他已經造成memory leak了,先來看一下正確的寫法:

code3
app-island.com © 2012, View objc source
  1. self.text = @"test string";
  2. NSString *a = @"another string";
  3. self.text = a;
end


我想在C跟JAVA的程式中,這兩段程式碼是完全一模一樣的,也不會有任何問題(self.要改為this->),但是在object-c中,一定要用下面的寫法,在text變數之前加上self的運算子,這樣才不會有memory leak的問題。

在objective-c中變數的memory使用必須自己來控制,跟C一樣(JAVA有garbage collection),所以在C語言中,allocate與release coding時要很清楚,但在objective-c中是透過一個retain的數字來控制,所有繼承NSObject的物件,都有一個retainCount的成員變數,當這個物件的retainCount為0時,系統就會回收這物件所指的記憶體,所以coding者,只要控制好這個變數,就可以做好記憶體的管理工作。

當要使用變數的記憶體時,就呼叫retain的函式,retainCount就會+1,不用時就呼叫release函式,retainCount就會-1,就是這麼簡單的概念,記憶體的控制就操控在你的手裡。下面我們來看一段程式碼,並搭配記憶體的圖示,來看object-c的運作方式:

code4
app-island.com © 2012, View objc source
  1. NSString *a = [[NSString alloc] initWithString: @"A String"];
  2. self.text = a;
  3. [a release];
end




首先我們宣告一個字串物件a然後用alloc的方式配置一個記憶體,並給它一個值「A String」,所以a就是指到這塊記憶體的一個指標,如上圖的a所示,由於在這裡呼叫alloc的函式,會自動retain一次,所以這塊記憶體的retainCount就會+1而成為1。
text就是本文一開始所宣告的NSString的成員變數,目前是指到下面的一塊記憶體。接著把a的值用self的運算子賦予給text,此時,text會先把原來所指的記憶體release掉,所以這塊記憶體就可以讓系統回收了,然後a所指的這塊記憶體再retain一次retainCount就成為2,接著把a所指的記憶體賦予給text,完成self.text=a的操作,請記住,現在使用記憶體的retainCount的數量是2,最後a呼叫release的函式,將retainCount-1成為1,所以text指到的記憶體的retainCount就是1,就不會被系統回收了。當text又被賦予其他值時,就重複相同的操作方式,該塊記憶體就會被release掉,系統進行回收,所以只要用相同的模式來做變數的指定,就不會有memory leak的問題產生了。

當然這邊還有很多要討論的地方,請稍等,我們先回過頭來看看之前code2程式範例造成的memory leak的原因:
第一個程式範例與這個範例唯一不同的地方,就是在將a的值賦予text時,沒有採用self的運算子,如下所示:

code5
app-island.com © 2012, View objc source
  1. text = a;
end


如果沒有使用self運算子時的差異,就是沒有套用settor的函式,此時就少做了release與retain的操作,當text原來指到的記憶體,在圖中的下方,由於沒有release所以retainCount就沒有-1,所以就沒有機會成為0,系統就永遠不會回收那塊記憶體,造成memory leak的問題,而當text直接指向a的記憶體時,由於沒有retain來+1,如果a不小心做了release的操作時,就會將retainCount-1就有可能讓retainCount的值降為0,此時這塊記憶體就會被系統回收,如果在程式執行中,用到text變數時,就會出現錯誤。所以不使用self運算子的風險,不僅是造成memory leak的問題,更會讓整個程式充滿不穩定的狀態。

完整範例程式碼:

code6
app-island.com © 2012, View objc source
  1. //myClass.h file
  2. @interface MyClass:NSObject
  3. {
  4. NSString *text;
  5. }
  6. @property(nonatomic, retain) NSString *text;
  7.  
  8. -(void) init;
  9. @end
  10.  
  11. //myClass.m file
  12. @implementation MyClass
  13. @synthesize text;
  14.  
  15. -(void) init
  16. {
  17. NSString *a = [[NSString alloc] initWithString: @"A String"];
  18. self.text = a;
  19. [a release];
  20. }
  21. -(void) dealloc
  22. {
  23. [text release];
  24. [super dealloc];
  25. }
  26.  
  27. @end
end


以上是本文完整的範例程式碼,其中想顯示出最重要的程式碼,是在函式dealloc部分,在這裡有做了[text release]的呼叫,這也是搭配@property的處理,下面會陸續點出原因。

在這一篇文章中objective-c記憶體管理準則-延伸篇 ,對於dealloc函式中使用release有更多的探討。

最後來說明objective-c在做setter時的作法,與retain、release的規定,了解之後就可以知道整個運作的細節了。
首先記住最重要的原則,當你使用alloc函式來初始化物件,最後一定要呼叫release函式,因為在alloc的函式中會呼叫retain,所以你相對的要自己呼叫release來還原retainCount,一加一減就不會有memory leak的風險。反之,沒有呼叫alloc,就不要使用release,這條規則比上一則更重要,因為有很多的內建物件,並不需要使用alloc來做初始化,例如:

code7
app-island.com © 2012, View objc source
  1. NSString *a = [NSString stringWithString: @"A String"];
end

如果是用這樣的方式來初始化NSString a 的話,就請千萬不要呼叫release,[a release]。因為系統會自行處理這物件的記憶體的管理問題。另外物件類別的成員變數,請在dealloc函式區塊中,做release的動作,來讓程式結束時可以回收此變數的記憶體。
接著我要完整的把系統settor的程式碼展示出來,這樣就更清楚流程與為什麼要這樣做的原因了:

code8
app-island.com © 2012, View objc source
  1. -(void) setText:(NSString*) val
  2. {
  3. if (text == val)
  4. {
  5. return;
  6. }
  7. NSString *tmp = text;
  8. text = [val retain];
  9. [tmp release];
  10. }
end

由上面的程式碼可以很清楚的看到運作流程,先將原來的值,用一個暫時的變數tmp來接,然後將新值先retain後,再賦予給原來的值text,最後把舊值,就是tmp release掉。
將這段settor程式碼再搭上之前的範例與說明是不是就比較清楚了,重新來說明一次,NSString *a = [[NSString alloc] initWithString:@"A String"];時,retainCount為1,selft.text = a;時,由於settor的關係,retainCount為2,接著[a release]後,retainCount又成為1,所以由a產生出來的記憶體賦予text後,就可以繼續使用,當最後程式結束時,在dealloc的區塊中會有[text release]的程式碼出現,將retainCount降為0,然後系統回收此塊記憶體。

NSString *a = [[NSString alloc] initWithString:@"A String"];[a release]是一個retain與release的搭配,而settor的retain則與dealloc中的[text release]搭配,完成記憶體的管理工作。

由以上的說明可以看到很簡單的settor運算,中間其實蘊含了objective-c的記憶體管理,但由於使用@property@synthesize包裝起來了,讓我們無法窺探到其中的奧祕,導致使用錯誤造成程式的不穩定,不知道是object-c的多此一舉還是節省程式人員的開發時間。
但在了解整個過程後,以後使用@property的過程中,相信就可以游刃有餘,真的達到objective-c設計的目的了。

參考文章:
Getters, setters and properties for the newbie

Assign, retain, copy: pitfalls in Obj-C property accessors


SIGN IN YOUR ACCOUNT TO HAVE ACCESS TO DIFFERENT FEATURES

CREATE ACCOUNT

FORGOT YOUR DETAILS?

`
TOP