第一章 自動引用計數
)1.1 什麼是自動引用計數
- 概念:在 LLVM 編譯器中設定 ARC(Automaitc Reference Counting) 為有效狀態,就無需再次鍵入
retain
或release
程式碼。
1.2 記憶體管理 / 引用計數
1.2.1 概要
-
引用計數就像辦公室的燈的照明
對照明裝置所做的動作 對OC物件所做的動作 開燈 生成物件 需要照明 持有物件 不需要照明 釋放物件 關燈 廢棄物件 -
其中,A生成物件時,引用計數為 1, 當多一個人需要照明,如B需要照明,則引用計數 +1, 以此類推。當A不需要物件,A釋放物件,引用計數 -1.當最後一個持有物件的人都不要這個物件了,則引用計數變為 0,丟棄物件。
1.2.2 記憶體管理的思考方式
-
客觀正確的思考方式:
- 自己生成的物件,自己所持有
- 非自己生成的物件,自己也能持有
- 不再需要自己持有的物件時釋放該物件
- 非自己持有的物件無法釋放
物件操作 OC方法 生成並持有物件 alloc/new/copy/mutableCopy等 持有物件 retain 釋放物件 release 廢棄物件 dealloc -
自己生成的物件,自己所持有:持有物件
- (id) allocObject {
// 自己生成並持有物件
id obj = [[NSObject alloc] init];
return obj;
}
複製程式碼 -
需要注意的是: NSMutableArray 類的 array 方法取得的物件不是自己所持有的。其內部實現原理為:
- (id)object {
// 自己生成並持有物件
id obj = [[NSObject alloc] init];
// 將物件註冊到 autoreleasepool 中, pool結束時會自動呼叫 release,這樣的方法自己就不會持有物件。
[obj autorelease];
// 返回這個自己不持有的物件。
return obj;
}
複製程式碼-
非自己生成的物件,自己也能持有:雖然一開始是不持有的,但是可以使用 retain 使其變成被自己所持有的,然後也可以使用 release 方法釋放物件。
// 取得非自己生成的物件
id obj = [NSMutableArray array];
// 取得的物件存在了,但是並非自己所持有的,引用計數還為 0, 但是該物件被放到了autoreleasepool 中,可以自動釋放
[obj retain];
// 此時,自己就持有了這個物件,引用計數為 1
[obj release];
// 此時釋放了這個物件,引用計數變為 0 ,物件就不可以再被訪問了,但是物件也沒有被立即廢棄
複製程式碼
-
-
無法釋放非自己持有的物件:例如
// 取得非自己持有的物件
id obj = [NSMutableArray array];
[obj release];
// 會導致程式崩潰
複製程式碼
1.2.3 alloc/retain/release/dealloc 實現
-
分析 GNU 原始碼來理解 NSObject 類中的方法。
-
首先是 alloc
id obj = [[NSObject alloc] init];
+ (id)alloc { // alloc 在內部呼叫 allocWithZone return [self allocWithZone:NSDefaultMallocZone()]; } + (id)allocWithZone:(NSZone *)zone { // allocWithZone 在內部呼叫 NSAllocateObject return NSAllocateObject(self, 0, z); } struct obj_layout { NSUInteger retained; }; inline id NSAllocateObject (Class aClass, NSUInteger extreBytes, NSZone *zone) { int size = 計算容納物件所需記憶體的大小; // 分配記憶體空間 id new = NSZoneMalloc(zone, size); // 將該記憶體空間中的值初始化為 0 memset(new, 0, size); // 返回作為物件而使用的指標 new = (id)&((struct obj_layout *) new)[1]; } /** 其中, NSZoneMalloc, NSDefaultMallocZone() 等名稱中包含的 Zone 是為了防止記憶體碎片化而引入的結構。對記憶體分配的區域本身進行多重化管理,根據物件使用的目的,大小,分配記憶體,從而提高記憶體管理的效率。 但是現在的執行時系統知識簡單的忽略了區域的概念,執行時系統中的記憶體管理本身已經機具效率,再使用區域來管理記憶體反而會引起記憶體使用效率低下的問題。 */ 複製程式碼
-
去掉NSZone後簡化的程式碼
struct obj_layout { NSUInteger retained; }; + (id)alloc { int size = sizeof(struct obj_layout) + 物件大小; // 這句的意思是,為 struct obj_layout 這個結構體分配一個 size 大小的記憶體空間,並且函式calloc()會將所分配的記憶體空間中的每一位都初始化為零,也就是這塊記憶體中所有的值都為 0 struct obj_layout *p = (struct obj_layout *)calloc(1, size); // 返回該物件指標 return (id)(p + 1); } 複製程式碼
-
[obj retain];
的實現- (id)retain { NSIncrementExtraRefCount(self); } inline void NSIncrementExtraRefCount(id anObject) { // 首先 (struct obj_layout *) anObject 找到的是這個物件的尾部, 所以需要 [-1] 減去該物件的大小,來定址到該物件的頭部,然後再判斷該結構體中 retained 這個變數的值是否已經大於了系統最大值,如果沒有,就 retained++, 使得引用計數 +1. if (((struct obj_layout *) anObject)[-1].retained == UINT_MAX - 1) { [NSException raise: NSInternalInconsistencyException format:@"NSIncrementExtraRefCount() asked to increment too far"]; ((struct obj_layout *) anObject) [-1].retained++; } } 複製程式碼
-
[obj release]
的實現- (void)release { if (NSDecrementExtraRefCountWasZero(self)) { [self delloc]; } } BOOL NSDecrementExtraRefCountWasZero(id anObject) { if (((struct obj_layout *) anObject)[-1].retained == 0) { return YES; } else { ((struct obj_layout *) anObject)[-1].retained--; return NO; } } 複製程式碼
-
[obj dealloc];
的實現- (void)dealloc { NSDeallocateObject(self); } inLine void NSDeallocateObject (id anObject) { // 指標 o 指向 anObject 的記憶體地址,然後釋放這個指標指向的記憶體 struct obj_layout *o = &((struct obj_layout *) anObject) [-1]; free(o); } 複製程式碼
-
1.2.4 蘋果的實現
-
首先看 alloc 的實現:
// 依次呼叫這四個方法
+ alloc
+ allocWithZone:
class_Instance
calloc
複製程式碼 -
retainCount / retain / release 的實現
- retainCount
__CFDoExtrernRefOperation
CFBaseicHashGetCountOfKey
- retain
__CFDoExternRefOperation
CFBasicHashAddValue;
- release
__CFDoExternRefOperation
CFBasicHashRemoveValue
// 這些函式的字首 CF 表示他們都包含於 Core Foundation 框架的原始碼中
複製程式碼所以其內部實現可能如下:
int __CFDoExternRefOperation(uintptr_r op, id obj) {
CFBasicHashRef table = 取得物件的雜湊表(obj);
int count;
switch (op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case OPERATION_retain:
CFBasicHashAddValue(table, obj);
return obj;
case OPERATION_release:
count = CFBasicHashRmoveValue(table, obj);
// 如果count == 0, 返回 YES, 則會呼叫 dealloc
return 0 == count;
}
}
// 舉例說明 retainCount
- (NSUInteger)retainCount {
return (NSUInteger)__CFDExternRefOperation(OPERATION_retainCount, self);
}
複製程式碼 -
由此可看出,蘋果在計數內部大概是以雜湊表的方式來管理引用計數的。複習雜湊表
-
比較
-
通過記憶體塊頭部管理引用計數的好處:
-
少量程式碼即可完成
-
能夠統一管理引用計數需要的記憶體塊和物件所用的記憶體塊
-
-
通過引用計數表管理引用計數的好處
- 物件所用的記憶體塊的分配不需要考慮它的頭部(跟記憶體塊頭部管理引用計數相比,就是少了一個用來計數的頭部)
- 引用計數表各記錄中存有記憶體塊的地址,可以從各個記錄追溯到各個物件的記憶體塊。這使得計時出現故障導致了物件所佔用的記憶體塊損壞了,在 記憶體塊頭部管理引用計數 時,我們這樣就沒有辦法訪問這塊記憶體了,但是在 引用計數表管理引用計數 時,我們就可以通過這個計數表來定址記憶體塊的位置。
- 另外,在利用工具檢測記憶體洩漏時,引用計數表也可以用來檢測各個物件是否有持有者
-
1.2.5 autorelease
-
autorelease 會像 C語言 的自動變數一樣來對待物件例項。當其超出作用域時,就會對物件進行release 的呼叫。
-
autorelease 的具體使用方法:
-
生成並持有 NSAutoreleasePool 物件
-
呼叫已經分配物件的 autorelease 例項方法
-
廢棄 NSAutoreleasePool 物件(對物件自動呼叫 release)
// 程式碼如下
NSAutoreleasePool pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain]; => 等價於 [obj release];
複製程式碼
-
-
我們在程式設計中,並不需要顯式的呼叫 pool 物件,因為在 RunLoop 中,這一切都為我們處理好了。在一個 RunLoop 迴圈中,會進行 NSAutoreleasePool 物件的生成,應用程式的主執行緒進行處理,廢棄 NSAutoreleasePool 物件。
-
儘管是這樣,我們有的時候也需要顯式的呼叫 NSAutoreleasePool 物件,因為有時會產生大量的 autorelease 物件,只要不廢棄 NSAutoreleasePool 物件,那麼這些生成的物件就不能被釋放,會導致記憶體瘋長的現象。最典型的例子就是在讀取大量影象的同時改變它的尺寸。
- 影象檔案讀到 NSData 物件,並且從中生成 UIImage 物件,改變這個物件的尺寸後,就會生成新的 UIIamge 物件。這種情況下就會產生大量的 autorelease 物件。這時就有必要在合適的地方生成,持有或廢棄 NSAutoreleasePool 物件。
-
另外,在 Cocoa 框架中也有很多類方法用於返回 autorelease 物件。比如
id array = [NSMutableArray arrayWithCapasity:1];
// 等價於
id array = [[[NSMuatbleArray alloc] initWithCapasity:1] autorelease];
複製程式碼
1.2.6 autorelease 的實現
-
首先來看 GNU 的原始碼
-
首先看一下 autorelease 方法的實現
[obj autorelease];
// 表面上的實現方法
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
/**
實際上, autorelease 內部是用 Runtime 的 IMP Caching 方法實現的。在進行方法呼叫時,為了解決類名/方法名幾區的方法執行是的函式指標,要在框架初始化時對他們進行快取
*/
id autorelease_class = [NSAutoreleasePool class];
SEL autorelease_sel = @selector(addObject:);
IMP autorelease_imp = [autorelease_class methodForSelector:autorelease_sel];
// 實際的方法呼叫時使用快取的結果值
- (id)autorelease {
(*autorelease_imp)(autorelease_class, autorelease_sel, self);
}
複製程式碼 -
再看 NSAutoreleasePool 的 addObject 類方法實現
+ (void)addObject:(id)obj {
NSAutoreleasePool *pool = 取得正在使用的 NSAutoreleasePool 物件;
if (pool) {
[pool addObject:anObj];
} else {
NSLog("不存在正在使用的 NSAutoreleasePool 物件");
}
}
複製程式碼- 注意:當多個 NSAutoreleasePool 物件巢狀使用時,理所當然會呼叫最裡層的 NSAutoreleasePool 物件
-
addObject 例項方法實現
// 當呼叫 NSObject類的 autorelease 例項方法時,這個物件就會被加到 NSAutoreleasePool 物件陣列中
- (void)addObject:(id)obj {
[array addObject:obj];
}
複製程式碼 -
drain 例項方法廢棄正在使用的 NSAutoreleasePool 物件的過程
// 執行順序: drain() -> dealloc() -> emptyPool() -> [obj release] -> [emptyPool release]
- (void)drain {
[self dealloc];
}
- (void)dealloc {
[self emptyPool];
[array release];
}
- (void)emptyPool {
for (id obj in array) {
[obj release];
}
}
複製程式碼
1.2.7 蘋果的實現
-
C++的實現
class AutoreleasePoolPage { static inline void *push() { // 生成或持有 NSAutoreleasePool 物件 } static inline id autorelease(id obj) { // 對應 NSAutoreleasePool 類的 addObject 類方法 AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的 AutoreleasePoolPage 例項; autoreleasePoolPage -> add(obj); } static inline void *pop(void *token) { // 廢棄 NSAutoreleasePool 物件 releaseAll(); } id *add(id obj) { // 新增物件到 AutoreleasePoolPage 的內部陣列中 } void releaseAll() { // 呼叫內部陣列物件的 release 類方法 } }; // 具體呼叫 void *objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } void *objc_autoreleasePoolPop(void *ctxt) { return AutoreleasePoolPage::push(ctxt); } id *objc_autorelease(void) { return AutoreleasePoolPage::autorelease(obj); } 複製程式碼
-
觀察 NSAutoreleasePool 類方法和 autorelease 方法的執行過程
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// == objc_autoreleasePoolPush()
id obj = [[NSObject alloc] init];
[obj autorelease];
// == objc_autorelease(obj)
[pool drain];
// == objc_autoreleasePoolPop(pool);
複製程式碼 -
另外:
[[NSAutoreleasePool showPools]];
可以用來確認已經被 autorelease 的物件的狀況。 -
問題: 如果
autorelease NSAutoreleasePool
物件會如何?- 答: 會崩潰。因為通常在使用 Foundation 框架時,無論呼叫哪個物件的 autorelease 方法,本質都是呼叫 NSObject 類的 autorelease 方法。 但是 autorelease 方法已經被 NSAutoreleasePool 類所過載。所以執行時會出現錯誤。
1.3 ARC 規則
1.3.1概要
- 實際上 引用計數式記憶體管理 的本質部分在 ARC 中並沒有改變,就像 自動引用計數 這個名稱一樣,ARC 所做的,只是自動的幫助我們處理了 引用計數 相關部分。
1.3.2記憶體管理的思考方式
- 引用計數式記憶體的思考方式就是思考 ARC 所引起的變化
- 自己生成的物件,自己所持有
- 非自己生成的物件,自己也能持有
- 不再需要自己持有的物件時釋放該物件
- 非自己持有的物件無法釋放
- 本質上和記憶體管理的思考方式一樣,只是實現方式上有些許不同
1.3.3所有權修飾符
-
ARC 有效時,id 型別和物件型別與 C語言 中的其他型別不同,其型別必須附加 所有權修飾符 。共有以下四種
- __strong 修飾符
- __weak 修飾符
- __unsafe_unretained 修飾符
- __autoreleasing 修飾符
-
__strong 修飾符
-
__strong 修飾符是 id 型別和物件型別預設的所有權修飾符
// 在 ARC 有效的環境下
id obj = [[NSObject alloc] init] <==> id __strong obj = [[NSObject alloc] init];
// 在 ARC 無效的環境下
{
id obj = [[NSObject alloc] init]
[obj release];
}
複製程式碼 -
當被__strong 修飾符修飾的,自己生成的,物件在超過其作用域時:
{
// 自己生成並持有物件
id __strong obj = [[NSObject alloc] init];
/**
因為變數 obj 為強引用,所以自己持有物件
*/
}
// 因為變數超出作用域,強引用失效,所以釋放物件,因為物件此時沒有其他的所有者了,物件被廢棄。
// 正好遵循記憶體管理的原則
複製程式碼 -
當物件的所有者和物件的生命週期是明確的,取得非自己生成並持有的物件時:
{
// 首先取得非自己生成的物件,但是由於__strong修飾符修飾著這個物件,所以自己持有這個物件
id __strong obj = [NSMutableArray array];
}
/**
當超出物件的作用域時,強引用失效,所以釋放物件。
但是由於 [NSMutableArray array] 所生成的物件並非自己所持有的,而是自動的加到 autoreleasePool 中,所以會在一個 RunLoop 週期結束後,自動廢棄物件。
*/
複製程式碼 -
有 __strong修飾符的變數之間可以相互賦值
// 首先 obj0 強引用指向 物件A , obj1 強引用指向 物件B,表示 obj1 持有 B, obj2 不持有任何物件
id __strong obj0 = [[NSObject alloc] init]; // 物件A
id __strong obj1 = [[NSObject alloc] init]; // 物件B
id __strong obj2 = nil;
// 此時 obj0 與 obj1 強引用同一個物件 B, 沒有人持有 物件A 了,所以 物件A 被廢棄。
obj0 = obj1;
// 此時 obj2 指向 obj0 所持有的物件, 所以 物件B 現在被三個引用所持有。
obj2 = obj0;
// 現在 obj1 對 物件B 的強引用失效,所以現在持有 物件B 的強引用變數為 obj0,obj2
obj1 = nil;
// 同理,現在只有 obj2 持有物件B
obj0 = nil;
// 沒有引用指向 物件B 了,廢棄 物件B
obj2 = nil;
複製程式碼 -
__strong修飾符 也可以修飾 OC類成員變數,也可以在方法的引數上,使用附有 _strong 修飾符的變數
@interface Test : NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (instancetype)init
{
self = [super init];
return self;
}
- (void)setObject:(id)obj {
obj_ = obj;
}
@end
// 呼叫函式
{
// 首先test 持有 Test 物件的強引用
id __strong test = [[Test alloc] init];
// Test物件 的 obj_ 成員,持有 NSObject 物件的強引用
[test setObject:[[NSObject alloc] init]];
}
/**
此時test強引用超出了其作用域,它失效了。
所以此時沒有強引用指向 Test物件 了, Test物件會被廢棄
廢棄 Test 物件的同時, Test物件 的 obj_ 成員也被廢棄。
所以它釋放了指向 NSObject 的強引用
因為 NSObject 沒有其他所有者了,所以 NSObject 物件也被廢棄。
*/
複製程式碼 -
_strong修飾符 與 _weak, _autoreleasing 修飾符一樣,初始化時,即使不明確指出,他們也都會自動將該引用指向nil。通過 _strong修飾符,完美的滿足了 引用計數的思考方式
-
id型別和物件型別的所有權修飾符預設都為 __strong 所以不需要再顯式的指明修飾物件的修飾符為 _strong
-
-
__weak 修飾符
-
_weak修飾符 的出現就是為了解決 _strong修飾符在記憶體管理中所帶來的迴圈引用問題。如上例:
@interface Test : NSObject
{
id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (instancetype)init
{
self = [super init];
return self;
}
- (void)setObject:(id)obj {
obj_ = obj;
}
@end
// 呼叫函式,列印變數結果如下:
{
// 首先test1 持有 Test 物件的強引用, test2 持有 Test 物件的強引用
id __strong test1 = [[Test alloc] init]; // 物件A
id __strong test2 = [[Test alloc] init]; // 物件B
/**
TestA 物件中的 obj_ 成員變數持有著 test2指向的 物件B, 同時,test2指向的物件B中的 obj_又強引用著 物件A, 所以造成了迴圈引用。
*/
[test1 setObject:test2];
[test2 setObject:test1];
}
/**
當跳出作用域後,test1釋放它對 物件A 的強引用
test2釋放它對 物件B 的強引用
但是此時 物件A中的 obj_A 對 物件B 的強引用本應該被釋放,但是由於在 物件B 中強引用了物件A,所以 obj_A 不會被釋放,會一直強引用 物件B, 而同理,物件B 中的 obj_B 也不會被釋放,所以它將一直強引用著 物件A, 所以此時外部沒有誰引用著 物件A 和 物件B, 但是他們自己在互相引用著,這樣就造成了記憶體洩漏!(所謂記憶體洩漏,指的就是應該被廢棄的物件,卻在超出其生存週期變數作用域時還繼續存在著)
*/
/**
列印變數結果如下:
其中 test1 物件中強引用 test2 物件, test2物件 又強引用 test1 物件,造成無盡的迴圈。
*/
複製程式碼
-
-
而下面這種情況:
{
id test = [[Test alloc] init];
[test setObject:test];
}
/**
當強引用test 的作用域結束後,它釋放了對 Test 物件的引用。
但是 Test物件 內部還保留著 對 Test物件 的強引用,所以 Test物件 被引用著,所以不會被回收
*/
// 也會發生記憶體洩漏!
複製程式碼
-
所以此時,就非常需要一個 __weak修飾符 來避免迴圈引用
// 弱引用與強引用正好相反,不能夠持有物件例項。
// 這樣寫會發出警告:Assigning retained object to weak variable; object will be released after assignment
// 表示因為沒有人持有著 NSObject 物件,所以該物件一旦被建立就會立即被銷燬
id __weak obj = [[NSObject alloc] init];
// 正確的使用弱引用的方式
{
// 自己生成並持有 NSObject 物件
id obj = [[NSObject alloc] init];
// 因為 NSObject 物件已經被 obj 強引用著, 所以此時 obj1 對它使用弱引用也沒有關係,
// 不會使它的引用計數 +1
id __weak obj1 = obj;
}
/**
當超出變數的作用域時, obj 對 NSObject物件 的強引用消失,
此時沒有人持有 NSObject物件 了。
NSObject物件 被廢棄
*/
複製程式碼 -
對上述迴圈引用的例子進行修改如下:
@interface Test : NSObject
{
id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end
@implementation Test
- (instancetype)init
{
self = [super init];
return self;
}
- (void)setObject:(id)obj {
obj_ = obj;
}
@end
// 呼叫函式,列印變數結果如下:
{
// 首先test1 持有 Test 物件的強引用, test2 持有 Test 物件的強引用
id __strong test1 = [[Test alloc] init]; // 物件A
id __strong test2 = [[Test alloc] init]; // 物件B
/**
TestA 物件中的 obj_ 成員變數弱引用著 test2指向的 物件B, 同時,test2指向的 物件B 中的 obj_又弱引用著 物件A。
*/
[test1 setObject:test2];
[test2 setObject:test1];
}
/**
當跳出作用域後,test1釋放它對 物件A 的強引用
test2釋放它對 物件B 的強引用
此時,由於 物件中的 obj_變數只擁有對物件的弱引用,所以 沒有誰持有著 物件A,和物件B,他們被釋放,沒有造成迴圈引用!
*/
複製程式碼 -
__weak修飾符 的另一優點:當持有某個物件的弱引用時,如果該物件被廢棄,則弱引用將自動失效,並且會被置為 nil的狀態(空弱引用)
id __weak obj1 = nil;
{
// 自己生成並持有物件
id __strong obj0 = [[NSObject alloc] init];
// obj1 現在也指向 NSObject物件
obj1 = obj0;
// 此時列印 obj1 有值
NSLog(@"A = %@", obj1);
}
/**
當變數 obj0 超出作用域,它不再持有 NSObject物件,
由於 obj1 是弱引用,所以它也不持有 NSObject物件
由於沒人持有 NSObject物件, NSObject物件被廢棄
被廢棄的同時, obj1 變數的弱引用失效, obj1 被重新賦值為 nil
*/
NSLog(@"B = %@", obj1);
/**
結果列印如下:
2017-12-14 15:16:39.859875+0800 littleTest[10071:1377629] A = <NSObject: 0x10054da70>
2017-12-14 15:16:39.860432+0800 littleTest[10071:1377629] B = (null)
*/
複製程式碼
-
__unsafe_unretained 修飾符
-
_unsafe_unretained 修飾符 是不安全的修飾符,在 iOS4 以前用來代替 _weak修飾符
id __unsafe__unretained obj1 = nil;
{
id __strong obj0 = [[NSObject alloc] init];
obj1 = obj0;
NSLog(@"A = %@", obj1);
}
NSLog(@"B = %@", obj1);
/**
該原始碼無法正確執行,因為 __unsafe_unretained修飾符 使變數既不強引用物件,也不弱引用物件。
當 obj0 超出作用域時, NSObject 無引用,所以被釋放
在此同時, obj1 有時會錯誤訪問物件,形成下面這種列印
2017-12-14 15:38:21.462724+0800 littleTest[10140:1399554] A = <NSObject: 0x10044eea0>
2017-12-14 15:38:21.463007+0800 littleTest[10140:1399554] B = <NSObject: 0x10044eea0>
有時會發生錯誤,直接使程式崩潰。
造成這兩種情況的本質為: obj1 訪問的物件已經被廢棄了,造成了 垂懸指標!
*/
複製程式碼 -
所以,需要注意的是,當使用 _unsafe_unretained 修飾符 訪問物件時,必須要確保該物件確實是真實存在的。
-
-
__autoreleasing 修飾符
-
在 ARC 有效時,我們不可以使用如下程式碼,因為這些程式碼是在非 ARC 環境下使用的:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
複製程式碼 -
作為替換,在 ARC 有效時, 我們會使用
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
複製程式碼 -
他們的對應關係如圖:
-
-
我們可以非顯式的使用 __autoreleasing 修飾符 。
-
情況一:當取得非自己生成並持有的物件時,雖然可以使用 alloc/new/copy/mutableCopy 以外的方法來取得物件,但是該物件已經被註冊到 autoreleasePool 中。這和在 ARC無效 時,呼叫 autorelease 方法取得的結果相同。這是因為編譯器會自動檢查方法名是否以alloc/new/copy/mutableCopy 開始,如果不是,則自動將物件註冊到 autoreleasePool 中。
@autoreleasepool {
// 首先取得非自己生成的物件,因為 obj 的強引用,所以它持有這個物件
// 因為這個物件的方法名不是以 alloc/new/copy/mutableCopy 開頭的,所以他被自動註冊到 autoreleasePool中了。
{
id __strong obj = [NSMutableArray array];
}
/**
當變數超出其作用域時,他失去對這個物件的強引用。所以它會釋放自己所持有的物件
但是此時 autoreleasePool 中還持有著對這個物件的引用,所以它不會立即被廢棄
*/
}
/**
當 autoreleasePool 的作用域也結束後,沒有人持有這個物件了,所以它被廢棄了。
*/
複製程式碼-
驗證上述說法,首先:建立物件的方式為
[NSMutableArray array]
:// 在 autoreleasepool 的作用域外定義一個 obj1 持有弱引用
id __weak obj1 = nil;
@autoreleasepool {
{
id __strong obj = [NSMutableArray array];
obj1 = obj;
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
/**
列印結果:
2017-12-14 16:32:07.118513+0800 littleTest[10242:1444479] (
)
2017-12-14 16:32:07.118819+0800 littleTest[10242:1444479] (
)
2017-12-14 16:32:07.118850+0800 littleTest[10242:1444479] (null)
結果表明:當obj0超出其作用域時,它失去了對物件的引用。但是由於該物件被自動註冊到 autoreleasepool 中,使得第二個 NSLog 列印時 obj1 依舊弱引用著這個物件,當第三個 NSLog 列印時,由於 autoreleasepool 已經被清空,所以這個物件也被銷燬了, obj1 又被重置為 nil
*/
複製程式碼
-
-
此時,建立物件的方式為:
[[NSMutableArray alloc] init]
id __weak obj1 = nil;
@autoreleasepool {
{
id __strong obj = [[NSMutableArray alloc] init];
obj1 = obj;
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
}
NSLog(@"%@", obj1);
/**
列印結果如下:
2017-12-14 16:36:09.584864+0800 littleTest[10257:1449554] (
)
2017-12-14 16:36:09.585131+0800 littleTest[10257:1449554] (null)
2017-12-14 16:36:09.585149+0800 littleTest[10257:1449554] (null)
這是因為,使用 alloc/new/copy/mutableCopy 方法建立物件時,不會將該物件自動的放入 autoreleasePool 中,這就使得當 obj0 超出其作用域後,就沒有人強引用著 NSMutableArray 物件了,該物件也就被廢棄了。
*/
複製程式碼 -
以下為 取得非自己生成並持有的物件 時所呼叫方法:
+ (id)array {
return [[NSMutableArray alloc] init];
}
/**
這段程式碼也沒有使用 __autorelease修飾符,所以這個方法內部的物件不會被註冊到 autoreleasePool 中。
*/
// 上述方法也可以寫成如下形式:
+ (id)array {
id obj = [[NSMutableArray alloc] init];
return obj;
}
/**
因為 return 使得 obj 超出作用域,所以它所指向的物件 NSMutableArray 會被自動釋放,但是因為 return 將這個物件作為函式的返回值返回給主調函式,所以這個物件不會被廢棄。並且由於這個物件的生成方法是將其作為返回值,不是由alloc/new/copy/mutableCopy 方法建立的,所以 NSMutableArray 物件會被自動新增到 autoreleasePool 中
*/
複製程式碼-
情況二: 在訪問有 __weak修飾符 的變數時,實際上必定會訪問註冊到 autoreleasePool 中的物件
id __weak obj1 = obj0;
NSLog(@"class=%@", [obj1 class]);
// 等價於
id __weak obj1 = obj0;
id _autoreleasing temp = obj1;
NSLog(@"class=%@", [temp class]);
/**
出現這種情況的原因是因為:__weak修飾符 只持有物件的弱引用,這樣沒法保證它訪問物件的過程中,物件不被廢棄。所以我們將他要訪問的物件放到 autoreleasePool 中,這樣就會使得 @autoreleasePool塊 結束之前都能保證該物件的存在。
*/
複製程式碼 -
情況三:由於
id obj <==> id __strong obj
所以我們希望能推出id *obj <==> id __strong *obj
但是實際上並非如此,實際情況是id *obj <==> id __autoreleasing obj
同理:NSObject **obj <==> NSObject * __autoreleasing *obj
,像這樣的,id 的指標或物件的指標在沒有顯示的指定時,會被附加上 __autoreleasing修飾符// 例如 NSString 中的這個方法
stringWithContentsOfFile:(nonnull NSString *) encoding:(NSStringEncoding) error:(NSError * _Nullable __autoreleasing * _Nullable)
// 使用這個方式的原始碼如下:
NSError *error = nil;
BOOL result = [obj performOperationWithError:&error];
// 函式宣告如下
- (BOOL)performOperationWithError:(NSError **)error;
// 等價於
- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error;
/**
之所以使用 __autoreleasing 作為修飾符,是因為我們這個方法宣告的引數的是 error 這個指標變數的指標,也就是 需要傳遞 error 的地址。而在這個方法內部的執行如下,它改變了 error 這個指標所指向的內容。使其指向了一個由 alloc 生成的物件。而我們需要明白的記憶體管理的思考方式為:除了由 alloc/new/copy/mutableCopy 生成的物件外,其他方式生成的物件都必需要註冊到 autoreleasePool 中。並取得非自己所持有的物件。所以將變數宣告為 (NSError * __autoreleasing *)error 就可以實現這一目的。
*/
- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error {
*error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
return NO;
}
/**
可能對於不熟悉 C語言 的小夥伴來說,不是很明白為什麼這裡非要將函式引數宣告為 “指標的指標”,這是因為當我們僅僅把引數宣告為指標時,方法就變為如下,當我們給函式傳遞指標時,預設會生成跟指標型別相同的例項變數。當我們在這個方法中操作指標時,我們以為操作的是指標,實際上只是這複製的例項變數。也就是說,在這個例子中 error 就是這個複製的例項變數。當這個方法結束時,error 會被釋放,其所指向的內容也會一併被釋放。所以此時外部的 error 依舊指向 nil。沒有任何改變。
而當我們使用 (NSError * __autoreleasing *)error 作為引數時,雖然複製的例項變數情況還是存在,但是這次複製的是“指標的指標”,也就是說,它指向跟引數指標相同指標地址, 在函式內部使用 *error 獲取到了指標地址,使其指向了 NSError物件 。這樣,雖然函式當出了其作用域時,那個複製的例項變數被銷燬了,但是它改變了函式外部 error 指標所指向的物件,使其從 nil 變成了 NSError物件。
*/
- (BOOL)performOperationWithError:(NSError *)error {
error = [[NSError alloc] initWithDomain:MyAppDomain code:errorCode userInfo:nil];
return NO;
}
複製程式碼
-
-
對於函式傳遞指標及指標的指標 還不明白的請看這裡
-
?的程式碼會產生編譯錯誤:
NSError *error = nil;
NSError **perror = &error;
//Pointer to non-const type 'NSError *' with no explicit ownership
複製程式碼 -
因為
// 需要改變perror的所有權修飾符
NSError *error = nil;
NSError *__strong *perror = &error;
// 對於其他型別的所有權修飾符也一樣
NSError __weak *error = nil;
NSError *__weak *perror = &error;
/**
可是我們剛剛在呼叫
- (BOOL)performOperationWithError:(NSError * __autoreleasing *)error
方法時,並沒有將 NSError *error 轉化為 __autoreleasing修飾符修飾的,這是為什麼?
*/
// 實際上,編譯器自動幫我們轉化了修飾符
NSError *error = nil;
NSError *_autoreleasing *temp = nil;
BOOL result = [obj performOperationWithError:&temp];
error = temp;
複製程式碼-
像 NSAutoreleasePool 一樣, autoreleasepool 也可以巢狀使用。例如在 iOS 程式中,整個程式都被包含在 @autoreleasepool塊 中。
-
NSRunLoop等實現無論 ARC 是否有效,都能夠隨時釋放註冊到 autoreleasepool 中的物件。
-
1.3.4規則
-
在 ARC 有效時編譯程式碼,必須遵守以下規則:
- 不能使用 retain/release/retainCount/autorelease
- 不能使用 NSAllocateObject/NSDeallocateObject
- 必須遵守記憶體管理方法命名規則
- 不能顯示的呼叫 dealloc
- 使用 @autoreleasepool塊 代替 NSAutoreleasePool
- 不能使用區域 NSZone
- 物件型變數不能作為 C語言 結構體的成員
- 顯示的轉換 id 和 void *
-
不能使用 retain/release/retainCount/autorelease
- 摘自蘋果的官方說明 "設定ARC有效時,無需再次鍵入release或retain程式碼" 否則就會編譯錯誤。
-
不能使用 NSAllocateObject/NSDeallocateObject
- 我們已經知道了當我們在呼叫 NSObject 類的 alloc 方法時,會生成並持有 OC 物件,如 GNUstep 所示,實際上 alloc 就是直接呼叫 NSAllocateObject 函式來生成持有物件的,但是在 ARC 環境下,如果我們也顯示的呼叫 NSAllocateObject 會產生編譯錯誤。
-
必須遵守記憶體管理方法命名規則
-
在 ARC 無效時,用於物件生成/持有需要遵循如下規則:alloc/new/copy/mutableCopy。以上述名稱開始的方法在返回物件時,必須得返回給呼叫方應當持有的物件。這點在 ARC 有效時也是一樣。
-
在 ARC 有效時,追加的一條命名規則:init
-
以 init 開始的方法規則要比 alloc/new/copy/mutableCopy 更加嚴格。該方法必須得是例項方法。不允許是類方法。並且返回物件應該為 id 型別或者該方法宣告類的物件型別,或者是該類的超類或子類。該返回物件並不註冊到 autoreleasepool 中。基本上只是對 alloc 方法返回值的物件進行初始化操作並返回該物件。
// 以下為使用該方法的原始碼: init 方法會初始化alloc 方法返回值,並且返回該物件
id obj = [[NSObject alloc] init];
// 下列是不允許的,因為它沒有返回物件
- (void)initTheData:(id)data;
// 另外,?方法雖然也有init, 但它不包含在命名規則裡,因為他是一個單詞 initialize
- (void)initialize;
複製程式碼
-
-
-
不能顯示的呼叫 dealloc
- 因為當物件廢棄時,無論如何都會呼叫物件的 dealloc 方法,所以不需要我們手動呼叫。而當我們手賤一下的去呼叫時,就會產生編譯錯誤
- dealloc 方法在大多數情況下用於刪除已經註冊的代理或者觀察者物件
- 在 ARC 無效時,必須在 dealloc 方法內部顯式的呼叫其父類的 dealloc 方法
[super dealloc];
- 在 ARC 有效時,這一切都是自動處理的。
-
使用 @autoreleasepool塊 代替 NSAutoreleasePool
- 在 ARC 中使用 NSAutoreleasePool 會引起編譯錯誤
-
不能使用區域 NSZone
- 無論 ARC 是否有效,NSZone在現在的執行時系統已經被完全忽略了。
-
物件型變數不能作為 C語言 結構體的成員
-
C語言 的結構體中如果存在 OC物件型變數 會引起編譯錯誤
-
如果非要將物件加入結構體,則可強制轉化為 void * 或者附加 _unsafe_unretained修飾符 ,因為被 _unsafe_unretained修飾符所修飾的物件,已經不屬於編譯器的記憶體管理物件了。
struct Data {
NSMutableArray __unsafe__unretained *array
}
複製程式碼
-
-
顯示的轉換 id 和 void *
-
當 ARC 無效時,將 id變數 強制轉化為 void *變數 不會出現問題
id obj = [[NSObject alloc] init];
void *p = obj;
// 將 void * 賦給 id變數 中,呼叫他的例項方法,執行時也不會出現問題
id o = p;
[o release];
複製程式碼 -
當 ARC 有效時,會引起編譯錯誤。此時,id型 或 物件型變數 賦值給 void * 或者逆向賦值時都需要進行特定的轉換,如果只是想單純的賦值則可以使用 bridge轉換
id obj = [[NSObject alloc] init];
// id轉化為 void *,它的安全性比 __unsafe__unretained 還要低,一不小心就會有垂懸指標
void *p = (__bridge void *)obj;
// void * 轉換為 id
id o = (__bridge id)p;
複製程式碼 -
__bridge轉換 中還包括 _bridge_retained轉換, _bridge_transfer轉換
id obj = [[NSObject alloc] init];
void *p = (__bridge_retained void *)obj
複製程式碼-
該程式碼在 非ARC 環境下
id obj = [[NSObject alloc] init];
void *p = obj;
// __bridge__retained轉變為了 retain,使得 p 和 obj 都持有了這個物件
[(id)p retain];
複製程式碼
-
-
一個其他的例子:
void *p = 0;
{
id obj = [[NSObject alloc] init];
p = (__bridge_retained void *)obj;
}
NSLog(@"class = %@", [(__bridge_retained)p class]);
複製程式碼-
該程式碼在 非ARC 環境下
void *p = 0;
{
id obj = [[NSObject alloc] init];
p = [obj retain];
[obj release];
}
/**
此時 p 依舊持有對 NSObject物件 的引用
*/
NSLog(@"class = %@", [(__bridge_retained)p class]);
複製程式碼
-
-
__bridge_transfer,被轉換的變數所持有的物件在該變數被賦值給轉換目標變數後釋放
id obj = (__bridge_transfer id)p;
複製程式碼-
非ARC 環境下
id obj = (id)p;
[obj retain];
[(id)p release];
複製程式碼
-
-
Objective-C 物件 與 Foundation物件
- Core Foundation 物件主要使用在用 C語言 編寫的 Core Foundation 框架中,並使用引用計數物件。在 ARC無效 時, Core Foundation 框架中的 retain、release 分別是 CFRetain,CFRelease
- Core Foundation 物件與 OC 物件的區別只在於是 Core Foundation 框架 還是 Foundation 框架所生成的。無論是由哪種框架生成的物件,一旦生成之後,就能在其它框架上使用。比如 Foundation 框架的 API 生成並持有的物件可以由 Core Foundation 框架的 API 進行釋放。
- Core Foundation 物件與 Objective-C 物件沒有區別,所以在 ARC無效 時,只用簡單的 C語言的轉換也能實現互換。另外這種互換不需要佔用 CPU 資源,所以也叫做 "免費橋"(Toll-Free Bridge)
-
1.3.5屬性
-
屬性宣告的屬性 與 所有權修飾符 對應的關係
屬性宣告的屬性 所有權修飾符 assign _unsafe_unretained copy __strong(賦值的是被複制的物件) retain __strong strong __strong unsafe_unretained _unsafe_unretained weak __weak
1.3.6陣列
-
靜態陣列的情況:
// 將附有各種修飾符的變數作為靜態陣列的使用情況
// 比如
id __weak obj[10];
// 除了 __unsafe__unretained修飾符之外的其他修飾符都是會將陣列元素的值預設初始化為nil
// 當陣列超出其變數作用域時,記憶體管理也同樣適用於他之中的各個物件
複製程式碼 -
動態陣列:在這種情況下,根據不同的目的選擇使用 NSMutableArray, NSMutableDictionary, NSMutableSet 等 Foundation 框架中的容器,這些容器會恰當的持有追加的物件並會為我們管理這些物件。
-
看一下動態陣列在 C語言 中的實現
// 首先,宣告一個動態陣列需要使用指標。來表示指標的地址
id __strong *array = nil;
//這裡是由於 id * 型別的指標預設修飾符為 id __autoreleasing * 型別, 所以有必要顯示的指定為 __strong 修飾符。另外,雖然保證了附有 __strong修飾符 的 id 型別變數被初始化為 nil, 但是不保證 array變數, 也就是 id指標型變數 被初始化為 nil
// 當型別是其他型別時,如下:
NSObject * __strong *array = nil;
// 之後,使用 calloc函式 確保想分配的,附有 __strong修飾符變數 的容量佔有的記憶體塊
array = (id __strong *)calloc(entries, sizeof(id));
// 其中 entries 表示記憶體塊的數量。並且 calloc 函式將陣列中的每個變數指向的物件都自動初始化為 nil
// 注意這裡如果使用了 malloc函式 來分配記憶體, 則需要手動的將每個變數所指向的物件都初始化為 0,注意這裡只能使用 memset等函式 來進行初始化賦值
// 然後,通過 calloc函式 分配的動態陣列就能完全按照靜態陣列的方法使用
array[0] = [[NSObject alloc] init];
// 但是在動態陣列中操作 __strong修飾符 的變數與靜態陣列有很大差異,需要自己手動釋放陣列,但是當它釋放時,必須手動的先將陣列的每個變數都置為nil,此時不能使用 memset等函式 將陣列中的元素值設為 0 。這也會記憶體洩漏
for (NSInteger i = 0; i < entries; ++i) {
array[i] = nil;
}
free(array);
複製程式碼
1.4 ARC 的實現
1.4.1 __strong修飾符
-
觀察賦值給附有 __strong修飾符 的變數在實際程式中到底是如何執行的,?程式碼(首先是正常的會使引用計數 +1 的 alloc/new/copy/mutableCopy 方法):
{
id __strong obj = [[NSObject alloc] init];
}
複製程式碼-
該段程式碼轉化為彙編程式碼後,為(具體如何轉化為彙編程式碼,請看我的另一篇文章):
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax
movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq _objc_msgSend
leaq -16(%rbp), %rdi
xorl %ecx, %ecx
movl %ecx, %esi
movq %rax, -16(%rbp)
callq _objc_storeStrong
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
.section __DATA,__objc_classrefs,regular,no_dead_strip
.p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_"
L_OBJC_CLASSLIST_REFERENCES_$_:
.quad _OBJC_CLASS_$_NSObject
.section __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_
.asciz "alloc"
.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
.p2align 3 ## @OBJC_SELECTOR_REFERENCES_
L_OBJC_SELECTOR_REFERENCES_:
.quad L_OBJC_METH_VAR_NAME_
.section __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_.1: ## @OBJC_METH_VAR_NAME_.1
.asciz "init"
.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
.p2align 3 ## @OBJC_SELECTOR_REFERENCES_.2
L_OBJC_SELECTOR_REFERENCES_.2:
.quad L_OBJC_METH_VAR_NAME_.1
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
複製程式碼 -
簡化後的模擬程式碼為:
// 首先傳送訊息給 NSObject 類,訊息內容為 alloc 指令,然後將結果賦值給 obj
id obj = objc_msgSend(NSObject, @selector(alloc));
// 然後將 init 訊息傳送給 obj
objc_msgSend(obj, @selector(init));
// 最後釋放 obj
objc_release(obj);
// 由此可知,在 ARC 有效時,自動插入了 release 方法
複製程式碼
-
-
取得 非自己生成的,但是自己持有的 物件:
id __strong obj = [NSMutableArray array];
複製程式碼-
轉化成組合語言:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
movq L_OBJC_CLASSLIST_REFERENCES_$_(%rip), %rax
movq L_OBJC_SELECTOR_REFERENCES_(%rip), %rsi
movq %rax, %rdi
callq _objc_msgSend
movq %rax, %rdi
callq _objc_retainAutoreleasedReturnValue
leaq -16(%rbp), %rdi
xorl %ecx, %ecx
movl %ecx, %esi
movq %rax, -16(%rbp)
callq _objc_storeStrong
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
.section __DATA,__objc_classrefs,regular,no_dead_strip
.p2align 3 ## @"OBJC_CLASSLIST_REFERENCES_$_"
L_OBJC_CLASSLIST_REFERENCES_$_:
.quad _OBJC_CLASS_$_NSMutableArray
.section __TEXT,__objc_methname,cstring_literals
L_OBJC_METH_VAR_NAME_: ## @OBJC_METH_VAR_NAME_
.asciz "array"
.section __DATA,__objc_selrefs,literal_pointers,no_dead_strip
.p2align 3 ## @OBJC_SELECTOR_REFERENCES_
L_OBJC_SELECTOR_REFERENCES_:
.quad L_OBJC_METH_VAR_NAME_
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
複製程式碼 -
簡化後的組合語言:
// 首先傳送 array 訊息給接收者 NSMutableArray, 然後將結果的返回值賦給 obj
id obj = objc_msgSend(NSMutableArray, @selector(array));
/**
obj_retainAutoreleasedReturnValue 函式主要用於最優化程式執行,顧名思義 obj_retainAutoreleasedReturnValue 表示的是 “持有 Autorelease 的返回值”,表示的是,它是用於自己持有物件的函式,但他持有的物件應為返回註冊在 autoreleasepool 中物件的方法,,或是函式的返回值。像這段原始碼一樣,也就是 obj 需要被 __strong 所修飾在呼叫 alloc/new/copy/mutableCopy 以外的方法時,由編譯器插入該函式
*/
objc_retainAutoreleasedReturnValue(obj);
// 釋放 obj
objc_release(obj);
複製程式碼 -
由於,objc_retainAutoreleasedReturnValue 函式總是成對出現的,所以實際上它還有一個姐妹:objc_autoreleaseReturnValue, 它主要用在 alloc/new/copy/mutableCopy 以外的方法生成物件時的返回物件上,也就是如?所示
+ (id)array {
return [[NSMutableArray alloc] init];
}
// 轉化成彙編後的簡化程式碼
+ (id)array {
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
// 此時,返回註冊到 autoreleasepool 中物件的方法:使用了 obj_autoreleaseReturnValue 函式來返回註冊到 autoreleasepool 中的物件,但是 obj_autoreleaseReturnValue 方法與 obj_autorelease 方法不同,一般不僅限於註冊物件到 autoreleasepool 中
return objc_autoreleaseReturnValue(obj);
}
/**
objc_autoreleaseReturnValue 方法會檢查使用該函式的方法或呼叫方的執行命令列表,
1.如果方法或函式的呼叫方在呼叫了方法或函式後緊接著呼叫了 objc_retainAutoreleasedReturnValue() 函式,那麼就不會將返回的物件註冊到 autoreleasepool 中,而是直接傳遞到方法或函式的呼叫方去。
2.如果方法或函式的呼叫方在呼叫了方法或函式後緊接著沒呼叫objc_retainAutoreleasedReturnValue() 函式,那麼就會將返回物件註冊到 autoreleasepool 中。
而 objc_retainAutoreleasedReturnValue() 函式與 objc_retain 函式不同,他即便不註冊到 autoreleasepool 中,也能正確的獲取物件。
通過 objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue 方法的協作,可以不將物件註冊到 autoreleasepool 中二直接傳遞,這一過程達到最優化
*/
複製程式碼
-
1.4.2 __weak修飾符
- 就像我們之前看到的:__weak修飾符 所提供的功能如魔法一般
- 若附有 __weak修飾符 的變數所引用的物件被廢棄,則將 nil 賦值給該變數
- 使用附有 __weak修飾符 的變數,即是使用註冊到 autoreleasepool 中的物件
若附有 __weak修飾符 的變數所引用的物件被廢棄,則將 nil 賦值給該變數 原理驗證:
-
下面我們來看看 __weak修飾符 原理實現:
{
id __weak obj1 = obj;
}
/**
編譯器的模擬程式碼
*/
id obj1;
// 首先通過 obj_initWeak 函式初始化附有 __weak 修飾符的變數
objc_initWeak(&obj1, obj);
// 然後在變數作用域結束時,通過 obj_destroyWeak 函式釋放該變數
objc_destroyWeak(&obj1);
/**
其中,objc_initWeak 函式的作用是:將附有 __weak修飾符 的變數初始化為 0 後,會將賦值的物件作為引數呼叫 objc_storeWeak 函式
obj_destroyWeak 函式的作用是:將 0 作為引數呼叫 obj_storeWeak 函式
*/
objc_initWeak(&obj1, obj); <==> obj1 = 0; objc_storeWeak(&obj1, obj);
objc_destroyWeak(&obj1) <==> objc_storeWeak(&obj1, 0);
/**
objc_storeWeak 函式把 第二個引數 的賦值物件的 地址 作為 "鍵值",將 第一個引數 的附有 __weak修飾符 的變數的"地址"註冊到 weak 表 中。如果第二個引數為 0 ,則把變數的地址從 weak 表中刪除
weak 表與引用計數表相同,實現方式都為"雜湊表"。如果使用 weak 表,將廢棄物件的地址作為鍵值進行搜尋,就能高速的獲取對應的附有 weak修飾符 的變數的地址。另外,由於一個物件可以同時賦值給多個附有 weak修飾符 的變數中,所以對於一個鍵值,可註冊多個變數的地址。
*/
複製程式碼 -
釋放物件時,廢棄沒人持有的物件的同時,程式是如何操作的,下面我們來跟蹤觀察,物件將通過 objc_release 方法釋放
- obj_release
- 引用計數為 0, 所以執行 dealloc
- _objc_RootDealloc
- object_dispose
- objc_destrctInstance
- objc_clear_deallocating
- 其中,objc_clear_deallocating 的動作如下
- 從 weak 表中獲取廢棄物件的地址作為鍵值的記錄
- 將包含在記錄中的所有附有 __weak修飾符 變數的地址賦值為 nil
- 從 weak 表中刪除該記錄
- 從引用計數表中刪除廢棄物件的地址作為鍵值的記錄
- 其中,objc_clear_deallocating 的動作如下
-
根據以上步驟可知:__weak修飾符 所修飾的變數所引用的物件被廢棄,該變數被置為 nil 得到實現。但是由此可知,如果大量使用附有 _weak修飾符修飾變數,將會產生效能問題。
-
在使用 __weak修飾符 時, 如果如下方式,會引起警告
id __weak obj = [[NSObject alloc] init];
// 因為該物件剛被建立就會被釋放
// 編譯器的模擬程式碼
id obj;
id tmp = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
// 雖然自己生成並持有的物件通過 objc_initWeak 函式被賦值給附有 __weak修飾符 的變數中,但是編譯器判斷它沒有持有者,所以該物件立即通過 objc_release 方法釋放
objc_initWeak(&obj, tmp);
objc_release(tmp);
objc_destroyWeak(&obj);
// 這樣一來, nil 就會被賦值給引用廢棄物件的附有 __weak修飾符 的變數
複製程式碼 -
關於立即釋放物件的一些思考
// 已知以下程式碼會引起編譯器的警告,這是因為編譯器判斷生成並持有的物件不能繼續持有,因為沒有強引用指向它
id __weak obj = [[NSObject alloc] init];
// ---------------------------------------------------------------------------------
// 附有 __unsafe_unretained 修飾符的變數會怎樣? 也會產生警告
id __unsafe_unretained obj = [[NSObject alloc] init];
// 轉換成編譯器的模擬程式碼:
id obj = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
// obj_release 函式立刻釋放了生成並持有的物件,這樣該物件的垂懸指標被賦給 obj
objc_release(obj);
// ---------------------------------------------------------------------------------
// 如果在生成物件的時候不把它賦給變數會怎樣?
// 在 非ARC 環境下,必然會發生記憶體洩漏
// 但是在 ARC 環境下,由於不能繼續持有該物件,會立即呼叫 obj_release 函式,由於 ARC 的處理,這樣的程式碼不會產生記憶體洩漏
[[NSObject alloc] init];
// ARC 下生成的程式碼
id tmp = obj_msgSend(NSObject, @selector(alloc));
objc_msgSend(tmp, @selector(init));
objc_release(tmp);
// ---------------------------------------------------------------------------------
// 是否可以呼叫被立即釋放掉的物件的例項方法?
(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);
// 所以,obj_release 方法是在該物件例項方法呼叫完成後才會被呼叫,所以可以呼叫被立即釋放的物件的例項方法
複製程式碼
使用附有 __weak修飾符 的變數,即是使用註冊到 autoreleasepool 中的物件 ,原理驗證:
-
看?程式碼
{
id __weak obj1 = obj;
NSLog(@"%@", obj1);
}
// 該原始碼可轉化為如下形式
id obj1;
objc_initWeak(&obj1, obj);
// objc_loadWeakRetained 取出附有 __weak修飾符 變數所引用的物件並 retain
id tmp = objc_loadWeakRetained(&obj1);
// 將物件註冊到 autoreleasepool 中
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);
複製程式碼 -
由此可知:因為附有 __weak修飾符 的變數所引用的物件像這樣被註冊到 autoreleasepool 中,所以在 @autoreleasepool 塊結束之前都可以放心的使用 _weak修飾的變數。但是,不能大量的使用附有 _weak修飾符 修飾的變數,否則會引起註冊到 autoreleasepool 中的物件大大增加,因此在使用附有 _weak修飾符 的變數時,最好先暫時賦給附有 _strong修飾符 的變數後再使用。若看不太懂則可以只看下面程式碼:
// 下面這段程式碼會使變數 o 所賦值的物件被註冊到 autoreleasepool 中 5 次
{
id __weak o = obj;
for (int i = 0; i < 5; ++i) {
NSLog(@"%d -- %@", i, o);
}
}
// 而下面這段程式碼只會使變數 o 所賦值的物件被註冊到 autoreleasepool 中 1 次
{
id __weak o = obj;
id tmp = o;
for (int i = 0; i < 5; ++i) {
NSLog(@"%d -- %@", i, tmp);
}
}
複製程式碼
不支援 __weak修飾符 的情況
- 在 iOS4 和 OS X Snow Leopard 不支援 __weak修飾符 。
- 不支援 __weak修飾符 的類:NSMachPort等, 這個類重寫了 retain/release 方法,並且實現了自己的引用計數。
- 不支援 __weak修飾符 的類在其類中宣告附加了 --attribute—((objc_arc_weak_reference_unavailable)) 這一屬性,同時定義了 NS_AUTOMATED_REFCOUNT_WEAK_UNAVAALIBLE
- 還有一種情況也不能使用 __weak修飾符 ,那就是當
allocWeakReference/retainWeakReference
例項方法返回 NO 的情況。(這種情況沒有被寫入 NSObject 類的介面說明文件中),也就是說,這兩個方法我們一般不會接觸到。
1.4.3 __autoreleasing修飾符
-
將物件賦值給附有 __autoreleasing修飾符 的變數等同於 ARC 無效時,呼叫物件的 autorelease 方法。
-
首先看一下使用 alloc/new/copy/mutableCopy 時的情況
@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_autoreleasPoolPop(pool);
複製程式碼 -
再看一下使用 alloc/new/copy/mutableCopy 以外的方法時的情況
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
// 模擬程式碼
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasPoolPop(pool);
// 雖然 obj 持有物件的方法變為 objc_retainAutoreleasedReturnValue, 但是將 obj 所引用的物件註冊到 autoreleasepool 中的方法並沒有改變
複製程式碼
1.4.4 引用計數
-
引用計數數值本身到底是什麼?
// 這個函式為獲得引用計數的函式數值
uintptr_t _objc_rootRetainCount(id obj);
{
id __strong obj = [[NSObject alloc] init];
NSLog(@"retain count = %d", _objc_rootRetainCount);
}
// 列印結果:retain count = 1
複製程式碼 -
我們實際上並不能完全信任 _objc_rootRetainCount 這個函式所取得的數值,因為有時對於已經釋放的物件以及不正確的物件地址,有時也會返回 1 。 並且在多執行緒中使用物件的引用計數數值,因為有競爭狀態的問題,所以取得的數值並不一定完全可信
1.5 總結
- 至此,我們所探究的 自動引用計數 已經完全講解完畢,如有疏漏或不正確,不準確的地方,還望大家批評指正,共同進步。