加入RUN!PC粉絲團
最近新增的精選文章
 
最多人點閱的精選文章
 
 
精選文章 - 開發技術
分享到Plurk
分享到FaceBook
 
iOS程式開發與XCode4.X-iOS5中的記憶體管理
文‧圖/何孟翰 2011/11/2 下午 12:04:41

說到iOS5,除了最近如日中天的iCloud,也就是iOS跟雲端的結合(特別是在應用程式的層級),與整合過的訊息告知新介面(也就是許多人說長得神似某牌作業系統的Notification介面)之外,我想對開發者來說另一個重要的課題,就是iOS5中所帶來新的記憶體管理機制,也就是在前面數集時我們有題過的Automatic Reference Counting。由於Objective C是一個C語言的超集合,並且它也還不是如同C++, Java般的設計機制,因此在之前我們所討論的Objective C物件必須要自行作記憶體的管理,也就是說必須要自己作alloc, retain和release。雖然說對應的C++在物件的管理上也是有類似的新增/回收機制,但是時常可以發現如果記憶體管理沒有小心的處理,常常會造成系統的不穩定或者是記憶體的增加。因此在這一版的XCode與iOS SDK中,試著將之前Objective C中近似手動的記憶體管理,改成自動的記憶體物件管理。也就是說我們不需要,或者說我們也不可以在程式中再呼叫release/retain這些物件在記憶體中更動參照記數的函數。因此這個更改不論你是要產生新的專案,還是要發展現有的程式,都是必須要關注的課題。因此在這一回的文章中讓我們討論一下它的實務應用。

自動的記憶體參照計數
由於這種自動的記憶體管理是一個新增加的功能,並且會影響到之前專案建置的特性,所以Apple在XCode之前的一些beta版中就一直對這些專案的設定與預設值作一些微調。但不論初始值為何,這些設定都可以依照不同的專案實作出不同的組態,也就是說XCode可以讓你選擇這個專案是使用舊的記憶體管理,或者是新的自動的參數記數器,而不需要因為升級了最新的XCode而必須要熬夜一次修完整個程式碼。

XCode4.2GM中的組態
以目前的XCode4.2 GM(Build 4D199)的版本為例,當你新增了一個專案時,你可以看到在這個專案的新增精靈下面的選項有一個標籤,寫著使用自動的參照記數器(Use Automatic Reference Counting)如圖1。



▲ 圖1 圈選這個選項在新增的專案中使用自動的參照計數



如果你圈選了這個選項,則就代表你會使用新的記憶體管理機制。而舊的專案打開時預設是不會更動記憶體管理的。因此你不用擔心打開舊的專案會造成程式碼的建置失敗。然而,在仔細討論這個自動參照計數器的運作邏輯之前,先讓我們看看這個設定對於專案建置的影響。首先請你在XCode左側的專案導覽視窗(Project Navigator),並且點擊要建置的這個專案的圖示(也就是一個藍色的檔案符號),並且在主畫中選擇Target ,並且按下這個專案主要的Target,接著在選取Build Setting之後,你可以在Apple LLVM compiler 3.0下面找到一個Objective C Automatic Reference Counting的項目如圖2。



▲ 圖2 在XCode 4.2下自動的參照計數設定的位置



如圖2,點擊了Target你可以看到主編輯畫面下有很多標籤,有一個Build Settings是和建置有關的設定參數,你可以把它想像成是build.xml或者是Makefie。同時你可以看到runPC11的標籤下有一個YES的標示,這代表在這個專案底下我們是有開啟自動的參照計數的模式。然而在iOS Default底下你可以看到它的值是No,這代表預設XCode4.2在這一個版本底下是「沒有」預設啟動自動的參照計數。因此從這個實例中我們可以看到自動的參照計數器在XCode 4.2下預設是關閉的,但是由於我們有在新增專案精靈時勾選了自動的參照計數的這個設定,所以這個專案的設定,也就是在Resolved中是顯示為Yes。

自動的參照計數帶來的行為改變
既然我們已經有了一個專案是使用新的記憶體模式,要觀察這個自動的參照計數帶來的改變很簡單,讓我們用一個最簡單的物件作解釋。你可以在這個專案底下新增一個簡單的類別如圖3,它不用是任何特別物件的子類別,你只需要繼承至NSObject即可,也就是說它在Objective C中是一個再簡單也不過的物件Foo如圖3。




▲ 圖3 新增一個最簡單的Foo物件



雖然這個物件中沒有任何的屬性也沒有任何的功能,但是它的的確確就是一個NSObject的子物件,因此我們是可以對它進行實例化的動作的,也就是在記憶體中佔有一塊空間,並且初始化它的值。因此當你呼叫了alloc跟init之後,為了避免造成編譯的警告,你可以試著用NSLog()印出它的內容。依照之前使用Objective C的常規,讓我們使用之前的思考觀念,使用完畢的物件需要進行release的呼叫。然而當你一寫完release函數的呼叫之後,你會看到主畫面出現一個錯誤如圖4。



▲ 圖4 主畫面指出release是一個錯誤。



為了能夠更仔細的看到這個錯誤的訊息,你可以切換到議題導覽器Issue Navigator,所有在XCode下發生的警告與錯誤都會分門別類的放置在這個畫面如圖5,大致上你可以把它解讀成在這種自動的參數計數模式下,是「不可以」呼叫release的這個訊息的,因為它必須由編譯器依照程式的語意來生成。




▲ 圖5 關於在自動的參照計數模式下使用release造成錯誤的解析



解決原本的函數呼叫帶來的錯誤
要能夠解決這個問題,你可以試著直接將這一行註解掉,再重新編譯一次,這時你就可以得到一個正確運行的結果。你或許會困惑雖然這樣子可以得到編譯正確的結果,但是這樣子的行為是不是會造成memory的leakage(依照之前的邏輯,alloc過的就要release,不然reference count不會是0)。為了確定這樣不會造成我們所想像的記憶體洩露情形,你可以使用選單上的「Product」「Analyze」來確認使用clang沒有報告出任何記憶體異常的情形。因此依照目前的結果你可以想像在新的記憶體管理模式下,像這樣子的程式碼與組態對有啟動自動的參照計數的專案來說是正常的。

自動的參照計數與物件
然而,你一定會和筆者一樣覺得這樣子的程式設計並不是非常平衡。事實上有一句俗話說「出來混的總是要還」我覺得很適合用來理解這種新的記憶體管理情境,雖然是一句玩笑話,但你可以想像,一個Objective C的記憶體被配置了之後,還是需要有釋放的機制。既然我們沒有主動去寫,你可以想像它就是由編譯器幫助我們在適當的地方填上。因此正如同你看到此處時的想法,編譯器不是在執行時期生成這些記憶體的釋放機制(所以也就是說它並不是一般Java的那種動態的 garbage collection),因此不會有那種釋放記憶體是CPU/IO會有瞬間峰值的反應。而是在你的程式碼在編譯時期來完成這些機制,所以它會檢查原始碼,分析物件的彼此關聯,看看這些物件在何時還會被呼叫到,而在它們「不再被呼叫到」之後,就會進行記憶體的釋放。並且你可以想像它和一般的我們所知道的Objective C記憶體管理機制使用同樣的參照記數法則,因此你可以想像從XCode 4.2開始,如果你使用新的記憶體組態設定,不再是由我們自己作物件的擷取與釋放,而是由編譯器幫你在適當的地方加上retain和release(與autorelease)。因此,看到這裡你可能會覺得Objective C的新機制幫了我們大忙,這樣以後我們就不用再仔細的檢查程式中的release個數與retain了,但是真的有這麼好的事情嗎?

物件的影響
如果只是如此簡單的物件記憶體管理機制,XCode4.2就不用大費周章的還在專案精靈中新增一個選項並且讓使用者有彈性可以選擇了,它只要檢查你是不是有遺失的release並且幫你增加就可以,但是事實卻沒有這麼簡單。因為有時並不是那麼容易的判定物件的生命週期,特別是當一個物件的成員變數是另一個物件時,此時它們彼此的生命週期就必須要由程式開發者給定他們正確的行為。因此我們如果要能夠得到正確的結果,就必須要給編譯器關於物件關係更多的訊息,它才能夠幫助我們作正確的判斷。

如同上一個小節你知道的,既然release是不需要被呼叫,那跟它相呼應的另外一個函數retain,這個在XCode4.2的自動的參照計數中也一樣是被禁止的。因此在物件的成員變數屬性的宣告上,我們也應該要作出適當的更改。因此XCode提出了一些物件成員變數屬性的新增修飾子,讓我們能夠更明確的定義出這些物件彼此之間的關聯,以便編譯器作出適當的記憶體相關程式碼生成。

簡單說來,在XCode4.2的機制下物件有分為兩種,一種是weak的參照,一種是strong的參照。weak的參照就像是一般的C下面的記憶體參照, 純粹就是一個記憶體的指標,指向一個記憶體中的物件。同時在自動的參照記數下,如果它所指向的物件在空間中的被移除配置(deallocate)了之後,它的值也會被設成nil,這樣才不會變成一個指向不合法空間的指標。因此你可以想像這種weak的用法就像是以前的assign,只是現在的記憶體管理機制會幫你把這個變數的參照設定成nil。

而相較於weak,strong所代表的就像是我們所熟悉的retain,因此你可以確保這個成員變數在它的母物件尚未被釋放前都是依然有效的。

因此關於物件的記憶體管理,我們只要使用alloc來創建一個物件,則它的刪除與釋放就可以交於系統來處理。同時在程匡中我們也不能呼叫如同dealloc,retain,release,retainCount或者是auto release,不然你會同樣得到一個錯誤訊息如圖6。



▲ 圖6 使用retain一樣會得到錯誤訊息



至於一般函數中的變數,我們也可以使用一些修飾子來作為它們生命週期的控制,例如像是__strong, __weak等等。這些在程式函數中一般變數的修飾子預設值是使用__strong,而__weak就像是之前所說的弱參照,也就是說如果之後沒有其它人再參照它,它就會被設定成nil。因此一般的堆疊變數如果你把它設定成__weak,而且之後也沒有其它的強參照,則它就是一被生成就會自動被回收,所以你看到的值在它生成之後馬上 就會變成是nil,因此就算你在初始化它的值之後試著去取回它的值,還是不會得到正確的結果。

因此回到我們原先這個例子的物件設定,假設foo是父物件而bar是子物件,則父物件到子物件的參照必須要是strong,就如同之前的retain,這樣可以確保在父物件的生命週期,子物件都是存在的。而子物件對於父物件的參照可以是weak,這樣子當父物件被deallocate時,它的這個參照會自動變成nil,而在Objective C底下有一個特性,就是對nil的物件發送訊息會是沒有任何反應,這樣子比較不會造成其它的副作用。

對效能可能造成的影響
一般說來,使用自動的參照計數對執行時期的效能不會有太大的影響,因為編譯器會試著減少呼叫retain/release的這些呼叫,所以理論上對於Objective C的執行時期是有助益的。並且由於這種惰境和執行時期的 GC並不相同,所以你也可以預期使用這種機制在執行時期並不會造成記憶體和效能不穩定的波動。同時對程式開發者而言,一般說來memory leak都是一般開發者不小心都會碰到的議題,因此在這樣的模式下,我們所需要關注的就不再只是要release/retain物件並且小心的不要造成記憶體的洩露,反之,我們是想像物件彼此之間的關聯結構,並且注意在物間之間彼此聯動的演算法上面,依照Apple的文件和WWDC的一些資料,看起來這種更動對開發者長遠來說是有助益的。

另外值得一題的,由於這種自動參照計數的運作,所以在自定類別的設計上,我們必須要將父類別的建構子中所傳回的值指定到這個類別的實例變數,也就是self。因此你可以發現在之前的objective C實作中我們可以直接在init的函數中呼叫[super init],但是現在如果你使用了新的記憶體管理機制,則你一定要呼叫self = [super init],也就是說父類別初始化之後要設定至自己的這個物件的參照,再進行自己這個類別的客制化動作。然而,這個看似在新的記憶體管理中所要求的程式碼更動並不是特別針對這個特性而作的修改,而是一個原本在Objective C中類別的設計時的良好規範,特別是當父類別的建構子會傳回一個客制化的物件時。因此不管你是不是要馬上套用這個記憶體管理的機制,你都可以檢視一下自己的(近似建構子)init函數,並且檢查它的寫法是不是能符合未來移植時期的規範。


既有專案的修改事實上像這種語言特性的改變由於牽涉過大,所以通常在實際的專案操作時會碰到新舊專案使用不同的特性的情形。如果為了長治久安,通常必須要作一些程式碼重構的工作。事實上在XCode4.2中也有工具來幫助開發者作這種轉移,如果你是之前舊的專案,你可以新版的 XCode4.2打開它,並且呼叫 Edit下面的Refactor的選單,則你就可以看到一個項目叫作「Convert to Objective C ARC...」如圖7:



▲ 圖7 使用XCode的工具進行專案至自動參照計數的轉換



接著你可以看到這個專案精靈會列出需要修改的部份,你可以檢視之後作必要的修改再完成程式碼的修正。舉例而言,當你的程式中依舊使用到retain於成員函數的屬性宣告時,雖然說(在目前的這個版本)在成員變數的屬性宣告時你還是可以使用retain,但是在這個轉換的工具下,你可以看到它會幫你列出需要改進的地方如圖8:



▲ 圖8 使用Convert to Object C ARC時提出的建議



雖然說這種轉換看似有一對一的對應,所以理論上轉換過的程式碼理所當然可以直接執行,但是畢竟由程式作的refactor,還是必須經過嚴謹的測試,確定沒有造成其它不可預期的副作用之後,再進行程式上架的動作。

後記在本文中我們討論了iOS5/XCode4.2中的記憶體管理機制(潛在)的改變,事實上在iOS5中還有一些新增的功能也是會讓開發者的我們有更有利的工具來開發出更吸引人的軟體。在之後的文章中我們會再陸續討論其它值得注意的課題。