Inside ARC — to see the code inserted by the compiler
前言
這是我在我們技術團隊所做的一次分享,稍作修改放到博客上來。
我們技術團隊會不定期(一般一個月1~2次)做技術分享,對我們團隊有興趣的可以私信我。
以下是正文。
這裏的主題是“Inside ARC”,顧名思義,主要是探討ARC在我們看不見的地方為我們做了什麼事情,以及怎麼做的。出發點是對底層實現的興趣,不了解這些也不妨礙寫好代碼,了解一點應該有益。
以下一些內容參考自Apple的官方文檔《Transitioning to ARC Release Notes》或維基百科,後續不再一一說明。
首先要明確的是ARC並不是GC,隻是把之前由程序員手工管理內存(如retain/release)的事情交給編譯器來處理,即編譯器為程序員在合適的時機插入一些內存管理代碼。
比較例外的是weak特性,除了編譯器外,還需要runtime的支持。這直接體現在OS X 10.7和iOS 5之後,才支持完整的ARC特性,包括弱引用的支持(為什麼弱引用需要runtime支持?)。
利用Xcode提供的匯編功能(Product - Perform Action - Assemble “filename”),我們可以初步了解(或推測)編譯器為我們添加的代碼。
不過我們看到的是AT&T匯編代碼,一片的數據在寄存器之間流動,不是很利於分析。
所以我們可以“在一個ARC項目中針對部分文件不采取ARC編譯”,比如針對“ARCObject.m”采用ARC方式編譯,而針對“nonARCObject.m”不采取ARC方式編譯,用來作比較。
棧上的變量默認初始化為nil
在ARC的編譯條件下,strong、weak和autoreleasing三種棧上的變量會被默認初始化為nil。


如上兩張代碼片段,圖-1的nonARCObject是不采用ARC編譯的,圖-2的ARCObject則采用ARC進行編譯。於是圖-2的obj會被初始化為nil,圖-1的則不會。

我們可以簡單地用if-else語句再加上一些日誌輸出來判斷是否真的初始化為nil,也可以通過匯編後的代碼來觀察,以便於由淺入深地探討後續內容。


圖-4是對應於圖-1的匯編代碼片段,從25行的“-[ARCObject testFunc]”可以看出;而圖-5則對應圖-2的匯編代碼片段。
兩者顯著的差別在於圖-4的L50-L51對應於“ARCObject.m:15:0”,這裏的一行匯編代碼“movl $0, -16(%ebp)”目測做的是初始化obj為nil的工作,應該是將立即數0放到基址指針偏移-16的地方去。
我們可以再增加一個變量來驗證下:

此時的匯編代碼關鍵片段如下:

由此可以驗證編譯器為我們插入了初始化為nil的代碼。
保證變量的釋放,不會有內存泄露
ARC的一個主要作用就是會幫我們合理釋放內存,避免內存泄露。
為了看下編譯器是怎麼做的,我們在既有代碼上增加內存的分配:


在非ARC的情況下,圖-8的代碼片段為obj分配了內存但沒有釋放,存在內存泄露。
在圖-9中,雖然代碼一模一樣,但在ARC模式下,編譯器會為我們釋放obj對象。


圖-10和圖-11都有兩次objc_msgSend的出現,對應於內存的分配和初始化:
NSObject *obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
不同的是對應於源代碼的L16,即表示作用域結束的花括號,圖-10是簡單的return出去,而圖-11多做了一次objc_storeStrong。
參考Clang的ARC文檔,objc_storeStrong的代碼如下:
idobjc_storeStrong(id*object,id value) {
value= [value retain];
id oldValue=*object;
*object= value;
[oldValue release];
return value;
}
即在retain新值的同時釋放舊值。
從圖-11可以推測出,在出作用域時,應該是執行了“objc_storeStrong(obj, nil)”。
我們可以對代碼稍作修改進一步驗證:

對應的匯編片段如下:

圖-13中的關鍵變化有:
對應於圖-12中的L17,這裏的賦值語句包含了一次objc_retain,這是由於默認為__strong類型,強引用產生的所有權需要對引用計數+1;
對應於圖-12中的L19,由於此時顯式指定了__unsafe_unretained類型,不具有所有權,所以隻是簡單的指針賦值;
對應於圖-12中的L20,此時出作用域,調用了兩次objc_storeStrong進行釋放。
所以,對象分配、賦值產生的引用計數變化,以及出作用域時對象的釋放,都得到了驗證。
命名風格決定的對象所有權
在我們還沒完全遷移到ARC的時候,我們擬定的編碼規範中提到了——這時候我去翻閱了下發現竟然沒有!雖然我印象中很深刻地有這麼一條,可能是在群裏有說
在非ARC的時候我們約定,以alloc/new等詞開頭的方法應該返回對象的所有權,即無需加入自動釋放池。
在ARC的時候,編譯器就是這麼做的。

在非ARC的情況下,這兩個函數的匯編代碼是一樣的,但在ARC的情況下則不然:


如圖-15,“testObj”方法在為obj分配內存並初始化後,調用了一次objc_retain對引用計數+1,在出作用域時調用objc_storeStrong釋放了一次,最後調用objc_autoreleaseReturnValue將對象加入自動釋放池。所以對於返回出去的testObj,外部默認是沒有所有權的。
而對於圖-16,即“newTestObj”方法,最後並沒有調用objc_autoreleaseReturnValue,因為編譯器根據方法名前綴“new”判斷出此時返回的對象,外部應該擁有所有權。
那麼,是否返回對象所有權有什麼區別呢?

如圖-17,分別調用“testObj”和“newTestObj”,對應的匯編片段如下:

對於圖-17中的L15,編譯器增加了objc_retainAutoreleasedReturnValue,然後立即調用objc_release進行釋放。
而對於圖-17中的L17,編譯器直接調用objc_release進行釋放。
這可以根據方法名來進行決策。
自動釋放池
上麵討論了__strong(默認)和__unsafe_unretained,接著先討論__autoreleasing,再討論__weak。


可以看出,對於__autoreleasing變量,編譯器會自動調用objc_autorelease,將對象添加到最內層的自動釋放池中。
而如果添加了@autorelease_pool代碼塊,編譯器還會增加一對調用,分別是objc_autoreleasePoolPush和objc_autoreleasePoolPop,前者創建一個新的自動釋放池作為當前自動釋放池,而後者將之前注冊的對象一一釋放,並將上一層自動釋放池設為當前釋放池。
弱引用
由於ARC並不是GC,所以沒辦法檢測出循環引用並消除,所以提供了__weak弱引用機製來解決這個問題。
__weak通常用於兩個互相引用的對象的其中一方,比如delegate。
另外,使用__weak訪問對象時會將對象加入到自動釋放池中,避免因為沒有強引用被立即釋放,導致後續代碼邏輯出錯。


這裏出現了objc_initWeak和objc_destroyWeak兩個調用,代碼實現分別如下:
idobjc_initWeak(id*object,id value) {
*object=nil;
return objc_storeWeak(object, value);
}
voidobjc_destroyWeak(id*object) {
objc_storeWeak(object,nil);
}
objc_storeWeak的函數原型為“id objc_storeWeak(id *object, id value);”,它的作用是:
當value為nil時,或者object指向的對象開始析構時,object會被置為nil,並且從weak table中移除;
當value不為nil時,object會被注冊到weak table中。
這裏可以插一下上次我和子通討論的break retain-cycle的問題,跟__weak、retain-cycle和dealloc都有關。
析構
以圖-21中的L16為例,考慮到可能有多個__weak指針指向obj對象,在obj對象析構時需要將這些__weak指針全部置為nil,那麼weak table將維護著以obj對象地址為key的表項,該項的值應該是一個數組,維護著多個指向obj對象的__weak指針。
當obj對象被銷毀時,以obj對象地址為key去weak table尋找對應的表項進行操作並移除。
之前和慕華討論過在ARC模式下,不需要在dealloc中釋放成員變量。當時有個點是runtime在何時為我們釋放成員變量,根據Clang的文檔(ARC下的dealloc)可以得知成員變量的銷毀是在dealloc之後。
當然,這並不意味著不需要寫“dealloc”方法。
如果我們在dealloc方法中設置斷點進行調試,那麼我們會發現有個方法名叫“.cxx_destruct”,這裏有篇文章對其進行探究。
參考Apple的開源文件,當一個對象的引用計數變為0時,會調用dealloc,接著是_objc_rootDealloc - object_dispose - objc_destructInstance - objc_clear_deallocating,在最後一個調用中清理weak table。
關於block
我之前有分享過一個主題叫《iOS中block實現的探究》,其中談到根據內存位置區分,block可以分為三種,分別是_NSConcreteGlobalStack、_NSConcreteStackBlock和_NSConcreteMallocBlock,對應著全局、棧和堆三種不同的內存位置。
在非ARC的情況下,我們在傳遞block對象時需要使用Block_copy進行拷貝,而不能隻是使用strong屬性做強引用,因為當棧展開後,指向的block對象就不在了,我們需要提前將其拷貝到堆上。
而在ARC下,編譯器自動為我們做了處理:


可以看到在進行賦值時,由於默認是__strong類型,所以編譯器調用了objc_retainBlock。
這個方法的作用是,如果block對象在棧上則將其拷貝到堆上;如果block對象已經在堆上,則做retain操作。
考慮到現在默認是__strong類型的變量賦值,所以block中如果引用外部變量都是強引用,但是否需要全部采用弱引用呢?這裏可以供參考。
最後更新:2017-04-03 08:26:14