自動引用計數(ARC, Automatic Reference Counting)
目錄
- 什麼是自動引用計數
- 記憶體管理的思考方式
- autorelease
- 所有權修飾符介紹
- ARC規則
- ARC實現(所有權修飾符作用詳解)
- 如何獲取引用計數值
- 總結
1. 什麼是自動引用計數
編譯器自動幫你在合適的地方插入retain/release程式碼, 不用你手動輸入.
2. 記憶體管理的思考方式
- 自己生成的物件, 自己持有.
- 非自己生成的物件, 自己也能持有.
- 不再需要自己持有物件時釋放.
- 非自己持有的物件無法釋放.
物件操作 | Objective-C方法 |
---|---|
生成並持有物件 | alloc/new/copy/mutableCopy等方法 |
持有物件 | retain方法 |
釋放物件 | release方法 |
廢棄物件 | dealloc方法 |
非自己生成的物件, 自己也能持有
如 :
1 2 |
id obj = [NSMutableArray array]; // 取得物件存在, 但自己並不持有物件 [obj retain]; // 自己持有物件 |
不再需要自己持有物件時釋放
如 :
1 2 |
id obj = [[NSObject alloc] init]; // 自己持有物件 [obj autorelease]; // 取得的物件存在, 但自己不持有物件 |
無法釋放非自己持有的物件
如 :
1 2 |
id obj = [NSMutableArray array]; // 取得物件存在, 但自己並不持有物件 [obj release]; // 釋放了非自己持有的物件!會導致應用程式崩潰 |
永遠不要去釋放非自己持有的物件!
1 2 3 4 |
- 在Objective-C的物件中存有引用計數這一整數值 - 呼叫alloc/retain方法後, 引用計數+1 - 呼叫release後, 引用計數-1 - 引用計數為0時, 呼叫dealloc廢棄物件 |
GNUstop將引用計數儲存在物件佔用記憶體塊頭部的結構體(struct obj_layout)變數(retained)中, 而蘋果的實現則是採用雜湊表(引用計數表)來管理引用計數.
GNUstop的好處 :
- 少量程式碼即可完成.
- 能夠統一管理引用計數用記憶體塊與物件用記憶體塊.
蘋果的好處 :
- 物件用記憶體塊的分配無需考慮記憶體塊頭部.
- 引用計數表各記錄中存有記憶體塊地址, 可從各個記錄追溯到各物件的記憶體塊.
3. autorelease
每一個RunLoop對應一個執行緒, 每個執行緒維護一個autoreleasepool.
RunLoop開始 -> 建立autoreleasepool -> 執行緒處理事件迴圈 -> 廢棄autoreleasepool -> RunLoop結束 -> 等待下一個Loop開始
可以用以下函式來列印AutoreleasePoolPage的情況
1 2 3 4 |
// 函式宣告 extern void _objc_autoreleasePoolPrint(); // autoreleasepool 除錯用輸出開始 _objc_autoreleasePoolPrint(); |
NSAutoreleasePool類的autorelease方法已被過載, 因此呼叫NSAutoreleasePool物件的autorelease會報錯.
4. 所有權修飾符
- __strong
- __weak
- __unsafe_unretained
- __autoreleasing
__strong
ARC中, id及其他物件預設就是__strong修飾符修飾
MRC中, 使用__strong修飾符, 不必再次鍵入retain/release. 持有強引用的變數超出其作用域時被廢棄, 隨著強引用的失效, 引用的物件會隨之釋放.
__weak
解決迴圈引用問題. 並且弱引用的物件被廢棄時, 則次弱引用將自動失效並等於nil
通過__weak變數訪問物件實際上必定是訪問註冊到autoreleasepool的物件, 因為該修飾符只持有物件的弱引用, 在訪問物件的過程中, 該物件可能被廢棄, 如果把要訪問的物件註冊到autoreleasepool中, 那麼在block結束之前都能確保該物件存在.
__unsafe_unretained
不安全的修飾符, 附有該修飾符的變數不屬於編譯器的記憶體管理物件. 該修飾符與__weak一樣, 是弱引用, 並不能持有物件.並且訪問該修飾符的變數時如果不能確保其確實存在, 則應用程式會崩潰!
__autoreleasing
物件賦值給__autoreleasing修飾的變數相當於MRC下手動呼叫autorelease方法.可理解為, ARC下用@autoreleasepool block代替NSAutoreleasePool類, 用__autoreleasing修飾符的變數代替autorelease方法.
但是, 顯式使用__autoreleasing修飾符跟__strong一樣罕見,
ps : id的指標或者物件的指標會被隱式附上__autoreleasing修飾符, 如 :
id *obj == id __autoreleasing *obj;
NSObject **obj == NSObject * __autoreleasing *obj;
編譯器特性
編譯器會檢查方法名是否以alloc/new/copy/mutableCopy開始, 如果不是則自動將返回值物件註冊到autoreleasepool(init方法返回值物件不註冊到autoreleasepool), 詳情見下面ARC規則之<須遵循記憶體管理的方法命名規則>
5. ARC規則
- 不能使用retain/release/retainCount/autorelease
- 不能使用NSAllocateObject/NSDeallocateObject
- 須遵循記憶體管理的方法命名規則
- 不要顯式呼叫dealloc(即不能手動呼叫的dealloc方法, 可以過載)
- 使用@autoreleasepool block代替NSAutoreleasePool
- 不能使用區域(NSZone)
- 物件型變數不能作為C語言結構體(struct/union)的成員
- 顯式轉換”id”和”void *”
不能使用NSAllocateObject/NSDeallocateObject
alloc實現實際上是通過直接呼叫NSAllocateObject函式來生成並持有物件, ARC下禁止使用NSAllocateObject函式與NSDeallocateObject函式.
須遵循記憶體管理的方法命名規則
只有作為alloc/new/copy/mutableCopy方法的返回值取得物件,才能自己生成並持有物件, 其餘情況均為”取得非自己生成並持有的物件”..以下為ARC下編譯器偷偷幫我們實現的事.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
+ (Person <em>)newPerson { Person </em>person = [[Person alloc] init]; return person; /* 該方法以new開始, 所以直接返回物件本身, 無需呼叫autorelease */ } + (Person <em>)onePerson { Person </em>person = [[Person alloc] init]; return person; /* 該方法不以alloc/new/copy/mutableCopy開始, 所以返回的是[person autorelease] */ } - (void)doSomething { Person <em>personOne = [Person newPerson]; // ... Person </em>personTwo = [Person onePerson]; // ... /* 當方法結束時, ARC會自動插入[personOne release]. <想想是為什麼?> */ } |
ARC下還有一條命名規則
- init
以init名稱開始的方法必須是例項方法(物件方法), 並且要返回物件, 返回的物件不註冊到autoreleasepool上. 實際上只是對alloc方法返回值的物件做初始化處理並返回該物件.
不能使用區域(NSZone)
在現在的執行時系統中, NSZone已被忽視
顯式轉換 id 和 void *
- __bridge轉換
- __bridge_retained轉換
- __bridge_transfer轉換
__bridge轉換
單純地轉換, 不安全.
1 2 3 |
id obj = [[NSObject alloc] init]; void *p = (__bridge void *)obj; id o = (__bridge id)p; |
__bridge_retained轉換
可使要轉換賦值的變數也持有所賦值的物件, 即
1 2 3 |
id obj = [[NSObject alloc] init]; void *p = (__bridge_retained void *)obj; // 相當於加上 [(id)p retain]; |
則obj與p同時持有該物件
__bridge_transfer轉換
與__bridge_retained相反, 被轉換的變數所持有的物件在該變數被賦值給轉換目標變數後隨之釋放.
1 2 3 |
id obj = [[NSObject alloc] init]; void *p = (__bridge_transfer void *)obj; // 相當於加上 [(id)p retain]; [obj release]; |
小結
__ bridge_retained轉換與retain類似, __ bridge_transfer轉換與release類似. 該兩種轉換多用於Foundation物件與Core Foundation物件之間的轉換
屬性
1 |
@property (nonatomic, strong) NSString *name; |
在ARC下, 以下可作為這種屬性宣告中使用的屬性來用.
屬性宣告的屬性 | 所有權修飾符 |
---|---|
assign | __unsafe_unretained修飾符 |
copy | __strong修飾符(但是賦值的是被複制的物件) |
retain | __strong修飾符 |
strong | __strong修飾符 |
unsafe_unretained | __unsafe_unretained修飾符 |
weak | __weak修飾符 |
以上各種屬性賦值給指定的屬性中就相當於賦值給附加各屬性對應的所有權修飾符的變數中.
6. ARC實現
ARC是由編譯器+執行時庫共同完成的.
__strong修飾符
1 2 3 4 5 6 7 |
{ id __strong obj = [[NSObject alloc] init]; } 可轉換為以下程式碼 id obj = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); objc_release(obj); |
1 2 3 4 5 6 7 |
{ id __strong obj = [NSMutableArray array]; } 可轉換為以下程式碼 id obj = objc_msgSend(NSMutableArray, @selector(array)); objc_retainAutoreleasedReturnValue(obj); objc_release(obj); |
objc_retainAutoreleasedReturnValue函式主要用於最優化程式執行. 它是用來持有返回註冊在autoreleasepool中物件的方法.這個函式是成對的, 另外一個objc_autoreleaseReturnValue函式則用於alloc/new/copy/mutableCopy方法以外的類方法返回物件的實現上, 如下 :
1 2 3 4 5 6 7 8 9 10 11 |
+ (id)array { return [[NSMutableArray alloc] init]; } 可轉換為以下程式碼 + (id)array { id obj = objc_msgSend(NSMutableArray, @selector(alloc)); objc_msgSend(obj, @selector(init)); return objc_autoreleaseReturnValue(obj); } |
那麼objc_autoreleaseReturnValue函式和objc_retainAutoreleasedReturnValue函式有什麼用?
可以這樣來總結 :
如果呼叫autorelease之後又緊接著呼叫retain的話, 這兩部就顯得多餘, 所以以上兩個函式就發揮其作用了.
- 用objc_autoreleaseReturnValue函式替代autorelease, 該函式檢測如果物件緊接著會呼叫retain, 他就不呼叫autorelease了(並設定一個標誌)
- 用objc_retainAutoreleasedReturnValue函式來替代retain, 該函式會檢測物件的標誌是否被設定, 如被設定, 則不呼叫retain; 反之則呼叫retain方法.
通過這兩個函式能優化程式, 減少不必要的多餘的操作.
__weak修飾符
- 若附有__weak修飾符的變數所引用的物件被廢棄, 則將nil賦值給該變數.
- 使用附有__weak修飾符的變數, 即是使用註冊到autoreleasepool中的物件.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ id __weak obj1 = obj; } 可轉換為以下程式碼 id obj1; objc_initWeak(&obj1, obj); objc_destroyWeak(&obj1); 也可轉換為以下程式碼 id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); objc_storeWeak(&obj1, 0); |
訪問__weak變數時, 相當於訪問註冊到autoreleasepool的物件
1 2 3 4 5 6 7 8 9 10 11 12 |
{ id __weak obj1 = obj; NSLog(@"%@", obj1); } 可轉換為以下程式碼 id obj1; objc_initWeak(&obj1, obj); id tmp = objc_loadWeakRetained(&obj1); objc_autorelease(tmp); NSLog(@"%@", tmp); objc_destroyWeak(&obj1); // objc_loadWeakRetained函式取出附有__weak修飾符變數所引用物件並retain |
需要注意的是, 通過__weak變數訪問所引用的物件幾次, 物件就被註冊到autoreleasepool裡幾次. (將附有__weak修飾符的變數賦值給附有__strong修飾符的變數後再使用可避免此問題)
__autoreleasing修飾符
將物件賦值給附有__autoreleasing修飾符的變數等同於MRC下呼叫物件的autorelease方法.
1 2 3 4 5 6 7 8 9 |
@autoreleasepool{ id __autoreleasing obj = [[NSObject alloc] init]; } 可轉換為以下程式碼 id pool = objc_autoreleasePoolPush(); id obj = objc_msgSend(NSObject, @selector(alloc)); objc_msgSend(obj, @selector(init)); objc_autorelease(obj); objc_autoreleasePoolPop(pool); |
那麼呼叫alloc/new/copy/mutableCopy以外的方法會怎樣呢?
1 2 3 4 5 6 7 8 9 |
@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); |
可見註冊autorelease的方法沒有改變, 仍是objc_autorelease函式
7. 如何獲取引用計數值
獲取引用數值的函式
uinptr_t _objc_rootRetainCount(id obj)
– (NSUInteger)retainCount;
該方法返回的引用計數不一定準確, 因為有時系統會優化物件的釋放行為, 在保留計數為1的時候就把它回收. 所以你用這個方法列印出來的引用計數可能永遠不會出現0. 我們不應該根據retainCount來除錯程式!!
8. 總結
我們現在的工程幾乎都執行在ARC下, 所以大部分記憶體管理程式碼都不需要我們自己寫, 而由編譯器幫我們搞定. 所以在ARC下我們只需要怎樣不要去破壞這個生態即可
- 避免迴圈引用(使用__weak修飾符)
- 遵循ARC方法命名規則
- 適時清空指標(賦值nil即可, 避免野指標錯誤)
- 如用到Core Foundation物件, 則在dealloc方法中釋放
- 在dealloc方法中只釋放引用並移除監聽(不能在dealloc中開啟非同步任務)
- 對於記憶體開銷較大的資源, 如file descriptor, socket, 大塊記憶體等應在不需要使用的時候呼叫close方法釋放掉而不是在dealloc中處理.
- 適當使用@autoreleasepool block來降低記憶體峰值(之前我寫的一篇文章中有demo)
- 必要時開啟”殭屍物件”除錯記憶體管理問題
附上另外兩篇文章的連結
Objective-C高階程式設計讀書筆記之blocks
Objective-C高階程式設計讀書筆記之GCD
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式