ARC到底幫我們做了哪些工作?
關於ARC
從iOS5開始, 就支援自動引用計數(Automatic Reference Counting, ARC)了, 所以就變得更為簡單了。ARC幾乎把所有記憶體管理事宜都交由編譯器來決定, 開發者只需專注於業務邏輯。
關於ARC的一些看法
1.ARC是不是和Java的GC類似,都會導致一部分效能損耗?
首先,ARC和GC是兩碼事,ARC是編譯時編譯器“幫你”插入了原本需要自己手寫的記憶體管理程式碼,而非像GC一樣執行時的垃圾回收系統。
2.ARC下自己不管理記憶體,它會不會出現記憶體洩露,或導致不可控的記憶體漲落?
瞭解ARC的原理後,就知道,ARC下編譯器插入的記憶體管理的程式碼是經過優化的,對於使用完的記憶體,多執行一行程式碼都不會浪費,可以這麼說,手寫的記憶體管理必須達到很嚴謹的水平才可能達到ARC自動生成的一樣完整且沒有疏漏。
3.ARC沒有一點風險嗎? 有沒有它不去管理記憶體的情況?
會有的, 具體請閱讀下面的ARC 不會優化的情景
。
OC 中的方法命名規則
若方法名以下列詞語開頭, 則其返回的物件歸呼叫者所有: alloc、new、copy、mutable Copy。若呼叫上述開頭的方法就要負責釋放返回的物件。也就是說, 這些物件在MRC中需要你手動的進行釋放。若方法名不以上述四個詞語開頭, 返回的物件就不需要你手動去釋放, 因為在方法內部將會自動執行一次 autorelease方法。
具體做了什麼可以閱讀另一篇博文Effective OC之記憶體管理。
ARC 做了哪些優化?
本文只是以一些例子來說明ARC所作的事情, 只是管中窺豹, ARC還做了其他很多的事情, 以及優化。
在使用ARC時一定要記住, 引用計數實際上還是要執行的, 只不過保留與釋放操作現在是由ARC自動為你新增。實際上, ARC在呼叫這些方法時, 並不通過普通的 Objective-C訊息派發機制, 而是直接呼叫其底層C語言版本。這樣做效能更好, 因為保留及釋放操作需要頻繁執行, 所以直接呼叫底層函式能節省很多CPU週期。
利用匯編尋找答案
在Xcode中的 Product->Perform Action->Assemble“SomeClass.m”,我們可以看到該 OC原始檔最終被編編譯生產的彙編程式碼,這裡就能詳細的檢視到底編譯器在我們的程式碼背後插入了哪些程式碼。
我建了一個類Zoo
@interface Zoo : NSObject
+ (instancetype)createZoo;
+ (instancetype)newZoo;
@end
@implementation Zoo
+ (instancetype)createZoo {
return [self new];
}
+ (instancetype)newZoo {
return [self new];
}
@end
將Zoo.m
轉化為彙編後的createZoo
和newZoo
方法如下:
來看createZoo
方法和newZoo
方法中, 有兩個 runtime的方法:
objc_msgSend
(34行和68行): 向類物件傳送訊息new。
objc_autoreleaseReturnValue
(39行): 這個函式的作用相當於代替我們手動呼叫 autorelease, 而且還有一些優化。編譯器會檢測之後的程式碼, 根據返回的物件是否執行 retain操作, 來設定全域性資料結構中的一個標誌位, 並決定是否執行 autorelease操作。
你可以先忽略這些作用, 後面有更詳細的介紹。在這裡只需要知道如何將程式碼轉化為彙編就可以, 我們繼續看。
VC中的程式碼:
@interface ViewController ()
@property (nonatomic, strong) Zoo *zoo;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self testForARC];
}
- (void)testForARC {
// 需要手動釋放返回物件的方法
[Zoo newZoo]; // 情景1
// id temp1 = [Zoo newZoo]; // 情景2
// self.zoo = [Zoo newZoo]; // 情景3
// 不需要手動釋放返回物件的方法
// [Zoo createZoo]; // 情景4
// id temp2 = [Zoo createZoo]; // 情景5
// self.zoo = [Zoo createZoo]; // 情景6
}
@end
需要手動釋放返回物件的方法
情景1、2、3中都是以new
開頭的方法, 返回的物件都歸呼叫者所有, 所以需要手動釋放返回物件的方法。當我們測試情景1
的時候, 要註釋掉其他情景的程式碼以增加彙編的可讀性。
情景1
情景1下, VC轉為彙編:
可以看到, 黃色框裡的是在ViewController.m
中的程式碼行是第35行, 即情景1
的程式碼。 有兩個runtime的函式, 即紅色框中的函式:
objc_msgSend
(83行): 向Zoo傳送訊息createZoo。
objc_release
: 釋放[Zoo newZoo]所返回的物件。
所以這時候, ARC自動新增程式碼應該是這樣的:
// Zoo
+ (instancetype)newZoo {
return [self new];
}
// VC
- (void)testForARC {
id temp = [Zoo newZoo];
objc_release(temp) ;
}
情景2
情景2下, VC轉為彙編:
所以這時候, ARC自動新增程式碼應該是這樣的:
// Zoo
+ (instancetype)newZoo {
return [self new];
}
// VC
- (void)testForARC {
id temp1 = [Zoo newZoo];
objc_storeStrong(&temp1, nil); //相當於release
}
objc_storeStrong
的內部實現如下:
void objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
情景3
VC反查彙編後的testForARC
方法:
VC反查彙編後的setZoo
方法:
所以, VC中的程式碼應該是這樣的:
// Zoo
+ (instancetype)newZoo {
return [self new];
}
// VC
- (void)testForARC {
id temp = [Zoo newZoo];
[self setZoo:temp];
objc_release(temp);
}
- (void)setZoo:(Zoo *zoo) {
objc_storeStrong(&_zoo, zoo);
}
不需要手動釋放返回物件的方法
情景4、5、6的命名規則, 返回的物件不歸呼叫者所有, 所以不需要手動釋放返回物件的方法。
情景4
情景4下, VC轉為彙編:
VC中的程式碼應該是這樣的:
// Zoo
+ (instancetype)createZoo {
id temp = [self new];
return objc_autoreleaseReturnValue(temp);
}
// VC
- (void)testForARC {
objc_unsafeClaimAutoreleasedReturnValue([Zoo createZoo]);
}
objc_autoreleaseReturnValue
: 這個函式的作用相當於代替我們手動呼叫 autorelease, 而且還有一些優化。編譯器會檢測之後的程式碼, 根據返回的物件是否執行 retain操作, 來設定全域性資料結構中的一個標誌位, 來決定是否會執行 autorelease操作。該標記有兩個狀態, ReturnAtPlus0
代表執行 autorelease, 以及ReturnAtPlus1
代表不執行 autorelease。
objc_unsafeClaimAutoreleasedReturnValue
: 這個函式的作用是替我們手動呼叫objc_release
函式,而且還有一些優化。編譯器會根據儲存的標記來決定需要不需要對其進行 release操作, 當然如果它建立時執行了 autorelease操作就不需要對其進行 release操作了。
情景5
情景5下, VC轉為彙編:
VC中的程式碼應該是這樣的:
// Zoo
+ (instancetype)createZoo {
id temp = [self new];
return objc_autoreleaseReturnValue(temp); //
}
// VC
- (void)testForARC {
id temp2 = objc_retainAutoreleasedReturnValue([Zoo createZoo]);
objc_storeStrong(&temp2, nil); // 相當於release
}
objc_retainAutoreleasedReturnValue
: 這個函式將替代 MRC中的 retain方法, 此函式也會檢測剛才提到的那個標誌位, 根據標誌位來決定是否執行 retain操作。
在這個例子中, 由於程式碼中沒有對物件進行保留, 所以建立時objc_autoreleaseReturnValue
函式設定的標誌位狀態是應該是ReturnAtPlus0
, 表示需要執行 autorelease操作的。所以, 該函式在此處是會進行 retain操作的。
情景6
VC反查彙編後的testForARC
方法:
VC反查彙編後的setZoo
方法:
VC中的程式碼應該是這樣的:
// Zoo
+ (instancetype)createZoo {
id temp = [self new];
return objc_autoreleaseReturnValue(temp);
}
// VC
- (void)testForARC {
id temp = _objc_retainAutoreleasedReturnValue([Zoo createZoo]);
[self setZoo:temp];
objc_release(temp);
}
- (void)setZoo:(Zoo *zoo) {
objc_storeStrong(&_zoo, zoo);
}
在這個例子中, 由於程式碼中 zoo屬性對物件進行了保留, 所以建立時objc_autoreleaseReturnValue
函式設定的標誌位狀態是應該是ReturnAtPlus1
, 表示不需要執行 autorelease操作的。所以, 該函式在此處是不會進行 retain操作的, 所以此時的引用計數就是建立時的那個, 並沒有做多餘的操作。而且通過ARC的這種設定並檢測標誌位的方法要比呼叫 autorelease和 retain更快。
對變數修飾符的優化
在應用程式中,修飾符來改變區域性變數與例項變數的語義: __strong
, __unsafe_unretained
, __weak
, __autorelease
。具體含義就不絮述了, 可以約我我之前的部落格:Effective OC之記憶體管理。
現在單純為了研究修飾符的語義, 就以需要手動釋放返回物件
的環境為背景來做比較, 修改VC中testForARC
的方法:
- (void)testForARC {
// 其它方法
id objc1 = [Zoo newZoo]; // 情景7(與情景2一致)
// __weak id objc2 = [Zoo newZoo]; // 情景8
// __unsafe_unretained id objc3 = [Zoo newZoo]; // 情景9
// __autoreleasing id objc4 = [Zoo newZoo]; // 情景10
}
__strong
(情景7)
在建立物件時, 預設都是strong
的, 所以在這些比較中, 情景7與情景2一致, 所以就不贅述了。
__weak
(情景8)
VC反查彙編後的testForARC
方法:
VC中的程式碼應該是這樣的:
- (void)testForARC {
id temp = [Zoo newZoo];
objc_initWeak(&objc2, temp);
objc_release(temp);
objc_destroyWeak(&objc2);
}
這個過程就是weak
指標的一個週期, 從建立到銷燬。這上面兩個新的runtime函式, objc_initWeak
和objc_destroyWeak
。這兩個函式就是負責建立weak
指標和銷燬weak
指標的。其實, 這兩個函式內部都引用另一個runtime函式, storeWeak
, 它是和storeStrong
對應的一個函式。
它們的原始碼如下:
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
__unsafe_unretained
(情景9)
VC反查彙編後的testForARC
方法:
VC中的程式碼應該是這樣的:
- (void)testForARC {
id temp = [Zoo newZoo];
// 指標objc3賦值過程
objc_release(temp);
}
__unsafe_unretained
型別,不具有所有權,所以只是簡單的指標賦值, 沒有runtime的函式使用。當臨時變數temp
銷燬後, 指標objc3
仍然是指向那塊記憶體, 所以是不不安全的。正如其名, unretained, unsafe。
__autoreleasing
(情景10)
VC反查彙編後的testForARC
方法:
VC中的程式碼應該是這樣的:
- (void)testForARC {
id objc4 = [Zoo newZoo];
objc_autorelease(objc4);
}
使用__autorelease
修飾後, 就相當於為其新增一個autorelease
, 當autoreleasepool
銷燬的時候, 將其釋放掉。
手動新增autoreleasepool
我們都知道, ARC
下編譯器會為我們新增一些的autorelease
會在系統建立autoreleasepool
中進行釋放, 釋放時機在自動釋放池pop
的時候進行的。而且手動新增的autoreleasepool
會在釋放池的作用域結束後立即pop
釋放。先來看程式碼:
- (void)testForARC {
@autoreleasepool {
id objc5 = [Zoo newZoo]; // 情景11
// __autoreleasing id objc6 = [Zoo newZoo]; // 情景12
// __autoreleasing id objc7 = [Zoo createZoo]; // 情景13
}
}
情景11
VC反查彙編後的testForARC
方法:
VC中的程式碼應該是這樣的:
- (void)testForARC {
@autoreleasepool {
id objc5 = [Zoo newZoo];
objc_storeStrong(&objc5, nil);
}
}
情景11
其實就是在情景2
的基礎上, 在外面包了一層autoreleasepool
, 結果其實差別不大, 這是多了一個objc_autoreleasePoolPush
和objc_autoreleasePoolPop
。瞭解自動釋放吃原理的你會明白, 手動新增的自動釋放池原來是因此才會出了作用域就會釋放的。我之前的部落格裡有寫到Autorelease機制及釋放時機, 這裡就不絮述了。
情景12
VC反查彙編後的testForARC
方法:
VC中的程式碼應該是這樣的:
- (void)testForARC {
@autoreleasepool {
id objc6 = [Zoo newZoo];
objc_autorelease(objc6);
}
}
當你在autoreleasepool
中新增一個__autorelease
修飾的變數後, 就相當於為其新增一個autorelease
, 當autoreleasepool
銷燬的時候, 將其釋放掉。這是個手動新增的autoreleasepool
, 所以當釋放池objc_autoreleasePoolPop
後就立即釋放了。下面的情景13
也是同樣的道理。
情景13
VC反查彙編後的testForARC
方法:
VC中的程式碼應該是這樣的:
- (void)testForARC {
@autoreleasepool {
id objc7 = _objc_retainAutoreleasedReturnValue([Zoo createZoo]);
objc_autorelease(objc7);
}
}
}
ARC 不會優化的情景
絕大部分ARC都是可以做好的, 但會有一些情況是例外的。如performSelector系列方法有很多, 都是帶有選擇子的。這種程式設計方式極為靈活,經常可用來簡化複雜的程式碼。不管哪種用法,編譯器都不知道要執行的選擇子是什麼,這必須到了執行期才能確定。
這種方式的確定很明顯。編譯器並不知道將要呼叫的選擇子是什麼,因此也就不瞭解其方法簽名及返回值,甚至連是否有返回值都不清楚。而且,由於編譯器不知道方法名,所以就沒辦法運用ARC的記憶體管理規則來判定返回值是不是應該釋放,鑑於此,ARC採用了比較謹慎的做法,就是不新增釋放操作。然而這麼做可能導致記憶體洩漏,因為方法在返回物件時 可能已經將其保留了。
來寫個例子, 在Zoo
中重寫dealloc
方法:
- (void)dealloc {
NSLog(@"dealloc: %@", self);
}
VC中:
// ARC不會為執行期的@selector新增記憶體管理語句
id zoo = [Zoo performSelector:@selector(newZoo)]; // 情景1
// id zoo = [Zoo performSelector:@selector(createZoo)]; // 情景2
NSLog(@"instance: %@", zoo);
在ARC環境
下, 使用情景二建立的例項物件可以正常的釋放, 而使用情景一建立的例項物件不會自動釋放, 從而造成了記憶體洩露。我的另外一篇部落格中說到了這個問題, 多用GCD, 少用performSelector系列方法。
runtime中記憶體管理函式的實現
在runtime的原始碼中, 有一些記憶體管理的函式, 它們的宣告存在於objc-internal.h
檔案中。比如, objc_alloc()
, objc_allocWithZone()
, objc_retain()
, objc_release()
, objc_autorelease()
, objc_autoreleasePoolPush
, objc_autoreleasePoolPop
, 這些函式應該看命名就知道了吧。
還有一些, 當然也包括我們上面提到的, objc_autoreleaseReturnValue()
, objc_unsafeClaimAutoreleasedReturnValue()
, objc_retainAutoreleasedReturnValue()
, objc_storeStrong()
, objc_weakStrong
, objc_initWeak()
, objc_destroyWeak()
它們的內部實現簡單來聊一聊。
1.objc_storeStrong()
void objc_storeStrong(id *location, id obj) {
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
當看到這個原始碼後, 才會發現這是ARC中做的優化。且看下面的程式碼, 假如 object在 release後的引用計數降為0, 從而導致系統將其回收, 接下來再執行 retain操作, 就會令應用程式崩潰。使用ARC之後, 就不可能發生這種疏失了。ARC自動的先保留新值, 再釋放舊值, 最後設定例項變數, 使其安全的儲存。
- (void)setObject:(id)object {
[object release];
_object = [object retain];
}
2.storeWeak()
它是和storeStrong
對應的一個函式:
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;
}
首先是根據weak指標找到其指向的老的物件, 然後獲取到與新舊物件相關的SideTable物件, 在老物件的weak表中移除指向資訊,而在新物件的weak表中建立關聯資訊, 接下來讓弱引用指標指向新的物件並返回。
3.objc_initWeak()
objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}
4.objc_destroyWeak()
void
objc_destroyWeak(id *location)
{
(void)storeWeak<DoHaveOld, DontHaveNew, DontCrashIfDeallocating>
(location, nil);
}
5.objc_retainAutoreleaseReturnValue()
// Prepare a value at +0 for return through a +0 autoreleasing convention.
id objc_retainAutoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;
// not objc_autoreleaseReturnValue(objc_retain(obj))
// because we don't need another optimization attempt
return objc_retainAutoreleaseAndReturn(obj);
}
6.objc_retainAutoreleasedReturnValue()
// 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);
}
虛擬碼如下:
id objc_retainAutoreleasedReturnValue(id object) {
if (get_flag(object)) {
clear_flag(object);
return object;
} else {
return [object retain];
}
}
7.objc_unsafeClaimAutoreleasedReturnValue()
// Accept a value returned through a +0 autoreleasing convention for use at +0.
id objc_unsafeClaimAutoreleasedReturnValue(id obj)
{
if (acceptOptimizedReturn() == ReturnAtPlus0) return obj;
return objc_releaseAndReturn(obj);
}
虛擬碼如下:
id objc_unsafeClaimAutoreleasedReturnValue(id object) {
if (get_flag(object)) {
return [object release];
} else {
clear_flag(object);
return object;
}
}
8.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);
}
虛擬碼如下:
id objc_autoreleaseReturnValue(id object) {
if ( //呼叫者將會執行retain ) {
set_flag(object);
return object;
} else {
return [object autorelease];
}
}
還有一些其他的執行時方法是可以正常使用的, 如objc_destructInstance()
, objc_duplicateClass()
, objc_destructInstance()
等等方法, 想了解的可以去下面的地址進行下載。