本文爲 《Objective-C 高級編程》1.4 中的內容編程
ARC 由如下工具、庫來實現:bash
經過 clang 彙編輸出和 objc4 庫(主要是 runtime/objc-arr.mm)的源代碼進行說明。架構
賦值給附有 __strong 修飾符的變量在實際的程序中究竟是怎樣運行的呢?app
{
id __strong obj = [[NSObject alloc] init];
}
複製代碼
在編譯器選項 「-S」 的同時運行 clang,可取得程序彙編輸出。看看彙編輸出和 objc4 庫的源代碼就可以知道程序是如何工做的。該源代碼實際上可轉換爲調用如下的函數。爲了便於理解,之後的源代碼有時也使用模擬源代碼。框架
/* 編譯器的模擬代碼*/
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
複製代碼
如原源代碼所示,2次調用 objc_msgSend 方法(alloc 方法和 init 方法),變量做用域結束時經過 objc_release 釋放對象。雖然 ARC 有效時不能使用 release 方法,但由此可知編譯器自動插入了 release。下面咱們來看看使用 alloc/new/copy/mutableCopy 之外的方法會是什麼狀況。ide
{
id __strong obj = [NSMutableArray array];
}
複製代碼
雖然調用了咱們熟知的 NSMutableArray 類的 array 類方法,但獲得的結果卻與以前稍有不一樣。函數
/* 編譯器的模擬代碼*/
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
複製代碼
雖然最開始的 array 方法的調用以及最後變量做用域結束時的 release 與以前相同,但中間的 objc_retainAutoreleasedReturnValue 函數是什麼呢?工具
objc_retainAutoreleasedReturnValue 函數主要用於最優化程序運行。顧名思義,它是用於本身持有(retain)對象的函數,但它持有的對象應爲返回註冊在 autoreleasepool 中對象的方法,或是函數的返回值。像該源代碼這樣,在調用 alloc/new/copy/mutableCopy 之外的方法,即 NSMutableArray 類的 array 類方法等調用以後,由編譯器插入該函數。優化
這種 objc_retainAutoreleasedReturnValue 函數是成對的,與之相對的函數是 objc_autoreleaseReturnValue。它用於 alloc/new/copy/mutableCopy 方法之外的 NSMutableArray 類的 array 類方法等返回對象的實現上。下面咱們看看 NSMutableArray 類的 array 類經過編譯器會進行怎樣的轉換。ui
+(id)array {
return [[NSMutableArray alloc] init];
}
複製代碼
如下爲該源代碼的轉換,轉換後的源代碼使用了 objc_autoreleaseReturnValue 函數。
+(id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
複製代碼
像該源代碼這樣,返回註冊到 autoreleasepool 中對象的方法使用了 objc_autoreleaseReturnValue 函數返回註冊到 autoreleasepool 中的對象。可是 objc_autoreleaseReturnValue 函數同 objc_autorelease 函數不一樣,通常不只限於註冊對象到 autoreleasepool 中。
objc_autoreleaseReturnValue 函數會檢查使用該函數的方法或函數調用方的執行列表,若是方法或函數的調用方在調用了方法或函數後緊接着調用 objc_retainAutoreleaseReturnValue() 函數,那麼就不將返回的對象註冊到 autorelease 中,而是直接傳遞到方法或函數的調用方。objc_retainAutoreleasedReturnValue 函數與 objc_retain 函數不一樣,它即使不註冊到 autoreleasepool 中而返回對象,也可以正確地獲取對象。經過 objc_autoreleaseReturnValue 函數和 objc_retainAutoreleasedReturnValue 函數的寫做,能夠不將對象註冊到 autorelease 中而直接傳遞,這一過程達到了最優化。
objc_autoreleaseReturnValue 實現以下:
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id objc_autoreleaseReturnValue(id obj) {
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
return objc_autorelease(obj);
}
複製代碼
prepareOptimizedReturn 是優化返回的關鍵,其實現以下:
// Try to prepare for optimized return with the given disposition (+0 or +1).
// Returns true if the optimized path is successful.
// Otherwise the return value must be retained and/or autoreleased as usual.
static ALWAYS_INLINE bool prepareOptimizedReturn(ReturnDisposition disposition) {
assert(getReturnDisposition() == ReturnAtPlus0);
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
if (disposition) setReturnDisposition(disposition);
return true;
}
return false;
}
複製代碼
gcc 的編譯特性使用 __builtin_return_address(level) 打印出一個函數的堆棧地址。其中 level 表明是堆棧中第幾層調用地址,__builtin_return_address(0) 表示第一層調用地址,即當前函數,__builtin_return_address(1) 表示第二層。
callerAcceptsOptimizedReturn 在 arm 架構上的實現以下:
static ALWAYS_INLINE bool callerAcceptsOptimizedReturn(const void *ra) {
// if the low bit is set, we're returning to thumb mode
if ((uintptr_t)ra & 1) {
// 3f 46 mov r7, r7
// we mask off the low bit via subtraction
// 16-bit instructions are well-aligned
if (*(uint16_t *)((uint8_t *)ra - 1) == 0x463f) {
return true;
}
} else {
// 07 70 a0 e1 mov r7, r7
// 32-bit instructions may be only 16-bit aligned
if (*(unaligned_uint32_t *)ra == 0xe1a07007) {
return true;
}
}
return false;
}
複製代碼
關於這段代碼的含義請看這篇文章,總結的比我好:How does objc_retainAutoreleasedReturnValue work?
objc_retainAutoreleasedReturnValue 實現以下,基本就是 objc_autoreleaseReturnValue 的逆操做。
// Accept a value returned through a +0 autoreleasing convention for use at +1.
id objc_retainAutoreleasedReturnValue(id obj) {
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
return objc_retain(obj);
}
複製代碼
就像前面咱們看到的同樣,__weak 修飾符提供的功能如同魔法通常。
這些功能像魔法同樣,到底發生了什麼,咱們一無所知。因此下面咱們來看看它們的實現。
{
id __weak obj1 = obj;
}
複製代碼
假設變量 obj 附加 __strong 修飾符且對象被賦值。
/* 編譯器的模擬代碼 */
id obj1;
objc_initWeak(&obj1, obj);
objc_destoryWeak(&obj1);
複製代碼
經過 objc_initWeak 函數初始化附有 __weak 修飾符的變量,在變量做用域結束時經過 objc_destroyWeak 函數釋放該變量。
如如下源代碼所示,objc_initWeak 函數將附有 __weak 修飾符的變量初始化爲0後,會將賦值的對象做爲參數調用 objc_storeWeak 函數。
obj1 = 0;
objc_storeWeak(&obj1, obj);
複製代碼
objc_destroyWeak 函數將0做爲參數調用 objc_storeWeak 函數。
objc_storeWeak(&obj1, 0);
複製代碼
即前面的源代碼與下列源代碼相同。
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
objc_storeWeak(&obj1, 0);
複製代碼
objc_storeWeak 函數把第二參數的賦值對象的地址做爲鍵值,將第一參數的附有 __weak 修飾符的變量的地址註冊到 weak 表中。若是第二參數爲 0,則把變量的地址從 weak 表中刪除。
weak 表與引用計數表相同,做爲散列表被實現。若是使用 weak 表,將廢棄對象的地址做爲鍵值進行檢索,就能高速地獲取對應的附有 __weak 修飾符的變量的地址。另外,因爲一個對象可同時賦值給多個附有 __weak 修飾符的變量中,因此對於一個鍵值,可註冊多個變量的地址。
釋放對象時,廢棄誰都不持有的對象的同時,程序的動做是怎樣的呢?下面咱們來跟蹤觀察。對象將經過 objc_release 函數釋放。
對象被廢棄時最後調用的 objc_clear_deallocating 函數的動做以下:
根據以上步驟,前面說的若是附有 __weak 修飾符的變量所引用的對象被廢棄,則將 nil 賦值給該變量這一功能即被實現。由此能夠,若是大量使用附有 __weak 修飾符的變量,則會消耗相應的 CPU 資源,良策是隻在須要避免循環引用時使用 __weak 修飾符。
使用 __weak 修飾符時,如下源代碼會引發編譯器警告。
id __weak obj = [[NSObject alloc] init];
複製代碼
由於該源代碼將本身生成並持有的對象賦值給附有 __weak 修飾符的變量中,因此本身不能持有該對象,這時會被釋放並廢棄,所以會引發編譯器警告。
編譯器會如何處理改源代碼呢?
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init))l
objc_initWeak(tmp);
objc_release(tmp);
objc_destroyWeak(&object);
複製代碼
雖然本身生成並持有對象經過 objc_initWeak 函數被賦值給附有 __weak 修飾符的變量中,但編譯器判斷其沒有持有者,故該對象當即經過 objc_release 函數被釋放和廢棄。
這樣一來,nil 就會被賦值給引用廢棄對象的附有 __weak 修飾符的變量中。下面咱們經過 NSLog 函數來驗證一下。
id __weak obj = [[NSObject alloc] init];
NSLog(@"obj=%@", obj);
複製代碼
如下爲該源代碼的輸出結果,其中用 %@ 輸出 nil。
obj=(null)
複製代碼
此次咱們再用附有 __weak 修飾符的變量來確認另外一功能:使用附有 __weak 修飾符的變量,便是使用註冊到 autoreleasepool 中的對象。
{
id __weak obj1 = obj;
NSLog(@"%@", obj1);
}
複製代碼
該源代碼可轉換爲以下形式:
/* 編譯器的模擬代碼 */
id objl
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);
複製代碼
與被賦值時相比,在使用附有 __weak 修飾符變量的情形下,增長了對 objc_loadWeakRetained 函數和 objc_autorelease 函數的調用。這些函數的動做以下:
由此可知,由於附有 __weak 修飾符變量縮陰用的對象像這樣被註冊到 autoreleasepool 中,因此在 @autoreleasepool 塊結束以前均可以放心使用。可是,若是大量地使用附有 __weak 修飾符的變量,註冊到 autorelease 的對象也會大量地增長,所以在使用附有 __weak 修飾符的變量時,最好先暫時賦值給附有 __weak 修飾符的變量後再使用。
{
id __weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);
}
複製代碼
相應地,變量 o 所賦值的對象也就註冊到 autoreleasepool 中 5 次。
將附有 __weak 修飾符的變量 o 賦值給附有 __strong 修飾符的變量後再使用能夠避免此類問題。
{
id __weak o = obj;
id tmp = o;
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
NSLog(@"4 %@", tmp);
NSLog(@"5 %@", tmp);
}
複製代碼
在 "tmp = 0;" 時對象僅登陸到 autoreleasepool 中1次。
在 iOS 4 和 OS X Snow Leopard 中是不能使用 __weak 修飾符的,而有時在其餘環境下也不能使用。實際上存在着不支持 __weak 修飾符的類。
例如 NSMachPort 類就是不支持 __weak 修飾符的類。這些類重寫了 retain/release 並實現該類肚子的引用計數機制。可是賦值以及使用附有 __weak 修飾符的變量都必須恰當地使用 objc4 運行時庫中的函數,所以獨自實現引用計數機制的類大多不支持 __weak 修飾符。
不支持 __weak 修飾符的類,在其類聲明中附加了 "__attribute ((objc_arc_weak_reference_unavailable))" 這一屬性,同時定義了 NS_AUTOMATED_REFCOUNT_WEAK_UNAVAILABLE。若是將不支持 __weak 聲明類的對象賦值給附有 __weak 修飾符的變量,那麼一旦編譯器檢驗出來就會報告編譯錯誤。並且在 Cocoa 框架類中,不支持 __weak 修飾符的類極爲罕見,所以沒有必要太過擔憂。
objc_initWeak 方法的實現以下:
/** * Initialize a fresh weak pointer to some object location. * It would be used for code like: * * (The nil case) * __weak id weakPtr; * (The non-nil case) * NSObject *o = ...; * __weak id weakPtr = o; * * This function IS NOT thread-safe with respect to concurrent * modifications to the weak variable. (Concurrent weak clear is safe.) * * @param location Address of __weak ptr. * @param newObj Object ptr. */
id
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
複製代碼
objc_destroyWeak 實現以下:
/** * Destroys the relationship between a weak pointer * and the object it is referencing in the internal weak * table. If the weak pointer is not referencing anything, * there is no need to edit the weak table. * * This function IS NOT thread-safe with respect to concurrent * modifications to the weak variable. (Concurrent weak clear is safe.) * * @param location The weak pointer address. */
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
複製代碼
/** * This function stores a new value into a __weak variable. It would * be used anywhere a __weak variable is the target of an assignment. * * @param location The address of the weak pointer itself * @param newObj The new object this weak ptr should now point to * * @return \e newObj */
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
複製代碼
看了上述代碼咱們能夠知道,objc_initWeak、objc_destroyWeak 和 objc_storeWeak 三個方法本質上都調用了 storeWeak 方法,其實現以下
template <HaveOld haveOld, HaveNew haveNew,
CrashIfDeallocating crashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj) {
assert(haveOld || haveNew);
if (!haveNew) assert(newObj == nil);
Class previouslyInitializedClass = nil;
id oldObj;
SideTable *oldTable;
SideTable *newTable;
// Acquire locks for old and new values.
// Order by lock address to prevent lock ordering problems.
// Retry if the old value changes underneath us.
retry:
if (haveOld) {
oldObj = *location;
oldTable = &SideTables()[oldObj];
} else {
oldTable = nil;
}
if (haveNew) {
newTable = &SideTables()[newObj];
} else {
newTable = nil;
}
SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);
if (haveOld && *location != oldObj) {
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
goto retry;
}
// Prevent a deadlock between the weak reference machinery
// and the +initialize machinery by ensuring that no
// weakly-referenced object has an un-+initialized isa.
if (haveNew && newObj) {
Class cls = newObj->getIsa();
if (cls != previouslyInitializedClass &&
!((objc_class *)cls)->isInitialized())
{
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
_class_initialize(_class_getNonMetaClass(cls, (id)newObj));
// If this class is finished with +initialize then we're good.
// If this class is still running +initialize on this thread
// (i.e. +initialize called storeWeak on an instance of itself)
// then we may proceed but it will appear initializing and
// not yet initialized to the check above.
// Instead set previouslyInitializedClass to recognize it on retry.
previouslyInitializedClass = cls;
goto retry;
}
}
// Clean up old value, if any.
if (haveOld) {
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
}
// Assign new value, if any.
if (haveNew) {
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
crashIfDeallocating);
// weak_register_no_lock returns nil if weak store should be rejected
// Set is-weakly-referenced bit in refcount table.
if (newObj && !newObj->isTaggedPointer()) {
newObj->setWeaklyReferenced_nolock();
}
// Do not set *location anywhere else. That would introduce a race.
*location = (id)newObj;
}
else {
// No new value. The storage is not changed.
}
SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
return (id)newObj;
}
複製代碼
如前所述,如下源代碼會引發編譯警告。
id __weak obj = [[NSObject alloc] init];
複製代碼
這是因爲編譯器判斷生成並持有的對象不能繼續持有。附有 __unsafe_unretained 修飾符的變量又如何呢?
id __unsafe_unretained obj = [[NSObject alloc] init];
複製代碼
與 __weak 修飾符徹底相同,編譯器判斷生成並持有的對象不能繼續持有,從而發出警告。
該源代碼經過編譯器轉換爲如下形式。
/* 編譯器的模擬代碼 */
id obj = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
複製代碼
objc_release 函數當即釋放了生成並持有的對象,這樣該對象的懸垂指針被賦值給變量 obj 中。
那麼若是最初不賦值變量又會如何呢?下面的源代碼在 ARC 無效時一定會發生內存泄漏。
[[NSObject alloc] init];
複製代碼
因爲源代碼不使用返回值的對象,因此編譯器發出警告。
可像下面這樣經過向 void 型轉換來避免發生警告。
(void)[[NSObject alloc] init];
複製代碼
不論是否轉換爲 void,該源代碼都會轉換爲如下形式。
/* 編譯器的模擬代碼 */
id tmp = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_release(tmp);
複製代碼
雖然沒有指定賦值變量,但與賦值給附有 __unsafe_unretained 修飾符變量的源代碼徹底相同。因爲不能繼續持有生成並持有的對象,因此編譯器生成了當即調用 objc_release 函數的源代碼。而因爲 ARC 的處理,這樣的源代碼也不會形成內存泄露。
另外,能調用被當即釋放的對象的示例方法嗎?
(void)[[[NSObject alloc] init] hash];
複製代碼
該源代碼可變爲以下形式:
/* 編譯器的模擬代碼 */
id tmp = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_msgSend(tmp, @selector(hash));
objc_release(tmp);
複製代碼
在調用了生成並持有對象的實例方法後,該對象被釋放。看來「由編譯器進行內存管理」這句話應該是正確的。
實際上還有一種狀況也不能使用 __weak 修飾符。
就是當 allowsWeakReference/retainWeakReference 實例方法(沒有寫入 NSObject 接口說明文檔中)返回 NO 的狀況。這些方法的聲明以下:
- (BOOL)allowsWeakReference;
- (BOOL)retainWeakReference;
複製代碼
在賦值給 __weak 修飾符的變量時,若是賦值對象的 allowsWeakReference 方法返回 NO,程序將異常終止。
即對於全部 allowsWeakReference 方法返回 NO 的類都絕對不能使用 __weak 修飾符。這樣的類一定在其參考說明中有所記述。
另外,在使用 __weak 修飾符的變量時,當被賦值對象的 retainWeakReference 方法返回 NO 的狀況下,該變量將使用「nil」。如如下的源代碼:
{
id __strong obj = [[NSObject alloc] init];
id __weak o = obj;
NSLog(@「1 %@」, o);
NSLog(@「2 %@」, o);
NSLog(@「3 %@」, o);
NSLog(@「4 %@」, o);
NSLog(@「5 %@」, o);
}
複製代碼
因爲最開始生成並持有的對象爲附有 __strong 修飾符變量 obj 所持有的強引用,因此在該變量做用域結束以前都始終存在。所以以下所示,在變量做用域結束以前,沒能夠持續使用附有 __weak 修飾符的變量 o 所引用的對象。
下面對 retainWeakReference 方法進行實驗。咱們作一個 MyObject 類,讓其繼承 NSObject 類並實現 retainWeakReference 方法。
@interface MyObject : NSObject
{
NSUInteger count;
}
@end
@implementation MyObject
- (id)init
{
self = [super init];
return self;
}
- (BOOL)retainWeakReference
{
if (++count > 3)
return NO;
return [super retainWeakReference];
}
@end
複製代碼
該例中,當 retainWeakReference 方法被調用4次或4次以上時返回 NO。在以前的源代碼中,將從 NSObject 類生成並持有對象的部分更改成 MyObject 類。
{
id __strong obj = [[MyObject alloc] init];
id __weak o = obj;
NSLog(@"1 %@", o);
NSLog(@"2 %@", o);
NSLog(@"3 %@", o);
NSLog(@"4 %@", o);
NSLog(@"5 %@", o);
}
複製代碼
如下爲執行結果。
從第4次起,使用附有 __weak 修飾符的變量 o 時,因爲所飲用對象的 retainWeakReference 方法返回 NO,因此沒法獲取對象。像這樣的類也一定在其參考說明中有所記述。
另外,運行時庫爲了操做 __weak 修飾符在執行過程當中調用 allowsWeakReference/retainWeakReference 方法,所以從該方法中再次操做運行時庫時,其操做內容會永久等待。本來這些方法並無計入文檔,所以應用程序編程人員不可能實現該方法羣,但若是因某些緣由而不得不實現,那麼仍是在所有理解的基礎上實現比較好。
將對象賦值給附有 __autoreleasing 修飾符的變量等同於 ARC 無效時調用對象的 autorelease 方法。咱們經過如下源代碼來看一下。
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
複製代碼
該源代碼主要將 NSObject 類對象註冊到 autoreleasepool 中,可做以下變換:
/* 編譯器的模擬代碼 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
複製代碼
這與蘋果的 autorelease 實現中的說明徹底相同。雖然 ARC 有效和無效時,其在源代碼上的表現有所不一樣,但 autorelease 的功能徹底同樣。
在 alloc/new/copy/mutableCopy 方法羣以外的方法中使用註冊到 autoreleasepool 中的對象會如何呢?下面咱們來看看 NSMutableArray 類的 array 類方法。
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
複製代碼
這與前面的源代碼有何不一樣呢?
/* 編譯器的模擬代碼 */
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
複製代碼
雖然持有對象的方法從 alloc 方法變爲 objc_retainAutoreleasedReturnValue 函數,但註冊 autoreleasepool 的方法沒有改變,還是 objc_autorelease 函數。