Effective Objective-C 2.0中文版讀書心得-主題1、11、29

主題01-熟悉Objective-C的根源:

Objective-C,是C語言的超集合,也就是說,它是基於C語言誕生的物件導向語言,跟C語言自然是百分百相容;在XCode裡面使用Objective-C寫作時,C語言是可以直接混寫並被編譯的。

開發者自己的形容:「就像是一層蓋在C語言上的薄紗」

雖然發展歷史悠久(1983年誕生),但因為只有Apple使用它作為主要開發語言,因此使用的只有Apple或Mac相關社群,直到近幾年Apple開創了行動裝置及App Store風潮後,使用者才有了突發性增加。

Objective-C的物件導向概念及語法,衍生自Smalltalk,與其他物件導向語言的差異是:

它採用的是「訊息傳遞」(messaging)架構,而非「函式呼叫」(function calling);而Smalltalk即為訊息傳遞架構的始祖。

維基百科有句話蠻貼切:

與其說是物件互相呼叫方法,不如說是物件之間傳遞訊息更為精確

也因此,它的語法與其他物件導向語言比起來相當怪異,呼叫物件功能的句型,是用方括號包起來的,也就是smalltalk的語法:

[someObject doThThingWithParameterpara1  WithParameterpara2];
   接收者         方法名稱:          參數1     方法名稱:   參數2

加上此語言在描述上,都是採用長而明確的名稱,不使用簡寫,所以常被其他語言經驗的人覺得麻煩而冗長,但這反而造就了極佳的可讀性

以我來說,以後應該都會維持這樣的習慣,
以保證可讀性,畢竟簡寫到最後大概很可能出現ghost variables

而訊息傳送架構,最關鍵的差異則為:C++等函式呼叫架構的語言,是由編譯器決定的;但是Objective-C,則是在執行時(Runtime)才動態的決定哪段程式碼要被執行,編譯器甚至不管被傳送的物件型別為何,反而是在執行時期才查詢,透過動態繫結機制來進行這些工作。

實際上,Objective-C大部分的繁重工作,不是由編譯器進行,而是透過Runtime元件進行的
Runtime其實就是一組程式碼(可以在類別裡import <objc/>就能看見該資料夾)

裡面包含了Objective-C所有的記憶體管理方法、物件的宣告、訊息的轉送機制;透過Runtime元件,膠合所有的程式碼(反過來說,所有撰寫的程式碼都會與它連結),只要Runtime被更新,應用程式的效能便跟著提昇,而透過編譯器進行較多工作的語言,則需要重新編譯過,才能得到效能改善。

由於Objective-C是C語言的超集,如果想寫出高效的Objective-C程式的話,還是必須理解C語言的記憶體模型,以及C語言的指標(pointer);這對了解Objective-C的參考計數為什麼要那樣運作很有幫助,也牽涉到為什麼Objective-C要使用指標來參照或指涉物件。

Objective-C的物件變數都是存放於記憶體堆疊段(stack)的指標,指向某個存放於堆積段(heap)的實體。  
Objective-C Runtime透過稱為「參考計數」(reference count)的記憶體管理架構,來配置及消滅物件的記憶體配置。

而對於非物件型別,如int、float、double、char等等,通常會採用c結構的方式儲存,如CGRect型別。

主題11-理解objc_msgSend的角色:

繼承自smalltalkObjective-C,與其他OO語言最大的差異就是「訊息傳遞」
在Objective-C的術語中,呼叫物件中的方法,叫做「傳遞訊息」,訊息具有名稱或選擇器(selector),接受參數,可以回傳值。

C語言的函式(function)是靜態繫結,編譯時位置就被寫死,速度較快;
Objective-C的方法(method)是動態繫結,要到執行時才會知道會呼叫哪一個方法,但是同時也要額外讀取方法的位址。

如同主題1所提到的,Objective-C的核心功能都是在執行時期才決定,透過Runtime元件動態產生、連結,但是底層依然是簡單的舊式C函式;可以在類別裡import <objc\/message.h>標頭檔,就可以點進去看了

Objective-C的方法:

id returnValue = [someObject messageName:parameter];

其中someObject被稱為接收者(receiver),
messageName: 被稱為選擇器(selector),
parameter就是參數
而選擇器結合參數,就是訊息了。

(receiver與selector,在起因於方法傳遞錯誤引起的錯誤訊息中,會常常出現這兩個單字)

當編譯器看到訊息後,就會轉成在message.h所定義的objc_msgSend函式,此函式就是C函式

void objc_msgSend(void /* id self, SEL op, ... */ )

第一個參數self,是接收者的位址
第二個參數op,就是選擇器SEL就是選擇器型別

類別中也可以使用SEL變數來傳遞方法位址

剩下的複數參數就是訊息的參數,以上面的方法來說,就是parameter

當Objective-C物件的方法被呼叫時,就會透過這個函式開始進行以下步驟:

  • 尋找接收者所屬類別是否有此方法,有的話就跳進該實作
  • 該類別沒有的話,就順著繼承結構回逤到超類別階層繼續尋找
  • 到最後都沒有的話,就由訊息轉送機制介入(message forwarding)。(在主題12解說)
    基本上找到的結果,都會快取在該類別的fast map中,所以不會是效能瓶頸的原因。
    
    而message.h裡面,也看的到一些針對特定狀況所使用的函式,例如:
  • 傳遞C struct (objc_msgSend_stret
  • 浮點數值(objc_msgSend_fpret)
  • 傳遞給父類別(objc_msgSendSuper
    這些函式特意設計的非常相似,就是為了要讓尋找實作的效率變高,尤其在連續搜尋時,能夠利用尾端呼叫最佳化的方式減少對stack區段的負載, 以減少記憶體負擔。
    

主題29-理解參考計數:

記憶體管理,一直都是所有OO語言的重要概念(感覺所有程式幾乎都是在跟記憶體容量搏鬥),了解各程式語言的記憶體管理模型,對寫出有效率、無臭蟲的程式至關重要。

目前Objective-C的記憶體管理,從iOS5之後預設都是使用自動參考計數(Auto Reference Count)來管理,之前則都是手動參考計數(Manual Reference Count),但是了解手動參考計數還是必要的。

參考計數(Reference Count)與垃圾收集(Garbage Collection)是不一樣的機制喔

「參考計數」簡單來說就是:

  • 每個物件都會有一個計數器,可以遞增或遞減
  • 當你想讓物件持續存在的時候,就遞增這個計數器
  • 當你用完這個物件的時候,就遞減這個計數器
  • 但是當物件的計數器遞減到0的時候,就表示這個物件已經沒有任何東西需要使用他了,該物件就會被「標記」為可以被銷燬,也就是該物件實體所在的記憶體區塊,隨時都有可能被覆寫。
  • 注意:當計數器遞減到0以後,就不能再被遞增了
  • 注意:因為計數為零,只是標記為可銷燬,但未必會立即銷燬,因此要注意計數為零後不要再去存取該物件實體,不然會出現不好找出發生點的程式崩潰。

參考計數最主要有三個方法:

  • retain 遞增參考計數
  • release 遞減參考計數
  • autorelease 稍後才遞減保留計數,也就是在自動釋放緩衝池(autorelease pool)進行排放(drain)方法的時候。 (主題34會講到autorelease pool)
  • 還有另一個:retaincount 可以取得目前該物件的參考計數的值,但是官方不建議使用,也不能作為精準的參考計數判斷。

參考計數的幾個基本運作:

*當物件被建立的時候,參考計數至少為1
*然後當這個物件不再被特定程式碼關注的時候,就呼叫release或autorelease來遞減計數,一旦計數為0,當初指向此物件實體的的參考就失效。

在應用程式的生命週期中,可以看做是很多物件被建立起來,然後彼此關連(參考);
如果物件A中存放了指向物件B的強式參考(strong reference),則物件A被稱為擁有(own)物件B;
強式參考會使得物件B一直到它被使用完為止後,才會被釋放。

在MRC的狀況下的範例:

NSMutableArray *array = [[NSMutableArray alloc] init];  //array物件參考的保留計數至少為1

NSNumber *number = [NSNumber numberWithInt:1337];  //number參考的保留計數至少為1
[array addObject: number]; //number的保留計數再加1
[number release]; //現在不再需要number物件,因此release減1,但是array依然在參考,物件並未被標記銷燬。

/* 使用array做了某些事情 */

[array release]; //做完以後遞減array的保留計數

這段程式碼只能在關閉ARC的專案裡面跑,因為開啟ARC會禁用retain及release方法。

注意:盡量不要對已經release後的物件進行操作,被標記可銷燬的物件未必會在第一時間消失,而且,因為物件太早釋放變成zombie object,導致後續存取失敗產生的bug,很難偵錯。

ARC的使用則比較簡單,只需要把確定不再使用的物件指向nil即可進行釋放。

而透過特性存取器(property)的strong特性的強式參考,所產生的setter方法就像這樣:

-(void)setFoo : (id)foo{
  [foo retain];  //先將傳入的新值保留
  [_foo release];  //再釋放舊的Foo
  _foo = foo; //最後將新的值賦給實體變數_foo
}
  • 變數名稱前線加上底線是直接存取實體變數,而不透過存取器
  • 其實_foo指向的,就是heap中存放的物件實體,只是不經過存取器。
  • 不能在init方法或setter\getter方法中透過存取器(self.)取得變數。

這順序非常重要,不能顛倒,如果先釋放了舊值,結果傳進來的參數剛好也是相同的值,這樣就會導致物件實體被釋放,結果_foo就變成了懸宕指標(殭屍物件,實體早已不存在)

自動釋放緩衝池(Auto Release Pool)

說到Objective-C的參考計數,最重要的功能就是這個,使用者除了自己手動傳遞release訊息之外,也可以傳遞autorelease訊息,讓auto release pool在稍後的某個時間點替使用者釋放該物件。
這種方法尤其常用在:當你的方法需要回傳物件的時候。

例如以下範例:

-(NSString *)getString{
   NSString *string = [NSString stringWithFormat: @”I am this : %@” , self];
   return string;
}

在MRC的狀況下,string應該要在回傳以後進行release,但是return之後方法就跳出了,而回傳:

return [string release]; //錯誤方式,不可以先release之後才回傳

其實就跟先把 string release之後再回傳,其實是一樣危險的方式(可能在回傳之前,就已經被釋放),這時候就可以使用:

return [string autorelease];

來延遲string物件被釋放的時機,這樣它會在緩衝池進行drain(排放)之前存活,也就能回傳給當初呼叫此方法的物件。

保留循環

但是參考計數還是有一個相當棘手的情節:保留循環
例如有三個物件A、B、C;A參考B、B參考C、C參考A,

於是這三個物件的保留計數絕對不會是0,即使這三個物件已經沒有與其他物件相關聯,但是因為循環參照,所以不會被釋放,造成memory leak,這在有垃圾收集(GC)的語言中也被稱為孤島效應,但是GC能夠處理這種情況;參考計數則需要使用弱式參考避免這種情形,或是另外強迫放棄保留,才能解決這種狀況。