第一篇 iOS 記憶體管理
1 似乎每個人在學習 iOS 過程中都考慮過的問題
- alloc retain release delloc 做了什麼?
- autoreleasepool 是怎樣實現的?
- __unsafe_unretained 是什麼?
- Block 是怎樣實現的
- 什麼時候會引起迴圈引用,什麼時候不會引起迴圈引用?
所以我將在本篇博文中詳細的從 ARC 解釋到 iOS 的記憶體管理,以及 Block 相關的原理、原始碼。
2 從 ARC 說起
說 iOS 的記憶體管理,就不得不從 ARC(Automatic Reference Counting / 自動引用計數) 說起, ARC 是 WWDC2011 和 iOS5 引入的變化。ARC 是 LLVM 3.0 編譯器的特性,用來自動管理記憶體。
與 Java 中 GC 不同,ARC 是編譯器特性,而不是基於執行時的,所以 ARC 其實是在編譯階段自動幫開發者插入了管理記憶體的程式碼,而不是實時監控與回收記憶體。
ARC 的記憶體管理規則可以簡述為:
- 每個物件都有一個『被引用計數』
- 物件被持有,『被引用計數』+1
- 物件被放棄持有,『被引用計數』-1
- 『引用計數』=0,釋放物件
3 你需要知道
- 包含 NSObject 類的 Foundation 框架並沒有公開
- Core Foundation 框架原始碼,以及通過 NSObject 進行記憶體管理的部分原始碼是公開的。
- GNUstep 是 Foundation 框架的互換框架
GNUstep 也是 GNU 計劃之一。將 Cocoa Objective-C 軟體庫以自由軟體方式重新實現
某種意義上,GNUstep 和 Foundation 框架的實現是相似的
通過 GNUstep 的原始碼來分析 Foundation 的記憶體管理
4 alloc retain release dealloc 的實現
4.1 GNU – alloc
檢視 GNUStep 中的 alloc 函式。
GNUstep/modules/core/base/Source/NSObject.m alloc:
1 2 3 4 5 6 7 8 9 |
+ (id) alloc { return [self allocWithZone: NSDefaultMallocZone()]; } + (id) allocWithZone: (NSZone*)z { return NSAllocateObject (self, 0, z); } |
GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject:
1 2 3 4 5 6 7 8 9 10 11 |
struct obj_layout { NSUInteger retained; }; NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) { int size = 計算容納物件所需記憶體大小; id new = NSZoneCalloc(zone, 1, size); memset (new, 0, size); new = (id)&((obj)new)[1]; } |
NSAllocateObject
函式通過呼叫 NSZoneCalloc
函式來分配存放物件所需的空間,之後將該記憶體空間置為 nil,最後返回作為物件而使用的指標。
我們將上面的程式碼做簡化整理:
GNUstep/modules/core/base/Source/NSObject.m alloc 簡化版本:
1 2 3 4 5 6 7 8 9 10 11 |
struct obj_layout { NSUInteger retained; }; + (id) alloc { int size = sizeof(struct obj_layout) + 物件大小; struct obj_layout *p = (struct obj_layout *)calloc(1, size); return (id)(p+1) return [self allocWithZone: NSDefaultMallocZone()]; } |
alloc 類方法用 struct obj_layout 中的 retained
整數來儲存引用計數,並將其寫入物件的記憶體頭部,該物件記憶體塊全部置為 0 後返回。
一個物件的表示便如下圖:
4.2 GNU – retain
GNUstep/modules/core/base/Source/NSObject.m retainCount:
1 2 3 4 5 6 7 8 9 10 |
- (NSUInteger) retainCount { return NSExtraRefCount(self) + 1; } inline NSUInteger NSExtraRefCount(id anObject) { return ((obj_layout)anObject)[-1].retained; } |
GNUstep/modules/core/base/Source/NSObject.m retain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (id) retain { NSIncrementExtraRefCount(self); return self; } inline void NSIncrementExtraRefCount(id anObject) { if (((obj)anObject)[-1].retained == UINT_MAX - 1) [NSException raise: NSInternalInconsistencyException format: @"NSIncrementExtraRefCount() asked to increment too far”]; ((obj_layout)anObject)[-1].retained++; } |
以上程式碼中, NSIncrementExtraRefCount
方法首先寫入了當 retained
變數超出最大值時發生異常的程式碼(因為 retained
是 NSUInteger 變數),然後進行 retain ++
程式碼。
4.3 GNU – release
和 retain 相應的,release 方法做的就是 retain --
。
GNUstep/modules/core/base/Source/NSObject.m release
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (oneway void) release { if (NSDecrementExtraRefCountWasZero(self)) { [self dealloc]; } } BOOL NSDecrementExtraRefCountWasZero(id anObject) { if (((obj)anObject)[-1].retained == 0) { return YES; } ((obj)anObject)[-1].retained--; return NO; } |
4.4 GNU – dealloc
dealloc 將會對物件進行釋放。
GNUstep/modules/core/base/Source/NSObject.m dealloc:
1 2 3 4 5 6 7 8 9 10 11 |
- (void) dealloc { NSDeallocateObject (self); } inline void NSDeallocateObject(id anObject) { obj_layout o = &((obj_layout)anObject)[-1]; free(o); } |
4.5 Apple 實現
在 Xcode 中 設定 Debug
-> Debug Workflow
-> Always Show Disassenbly
開啟。這樣在打斷點後,可以看到更詳細的方法呼叫。
通過在 NSObject 類的 alloc 等方法上設定斷點追蹤可以看到幾個方法內部分別呼叫了:
retainCount
__CFdoExternRefOperation
CFBasicHashGetCountOfKey
retain
__CFdoExternRefOperation
CFBasicHashAddValue
release
__CFdoExternRefOperation
CFBasicHashRemoveValue
可以看到他們都呼叫了一個共同的 __CFdoExternRefOperation
方法。
該方法從字首可以看到是包含在 Core Foundation,在 CFRuntime.c 中可以找到,做簡化後列出原始碼:
CFRuntime.c __CFDoExternRefOperation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int __CFDoExternRefOperation(uintptr_t op, id obj) { CFBasicHashRef table = 取得物件的雜湊表(obj); int count; switch (op) { case OPERATION_retainCount: count = CFBasicHashGetCountOfKey(table, obj); return count; break; case OPERATION_retain: count = CFBasicHashAddValue(table, obj); return obj; case OPERATION_release: count = CFBasicHashRemoveValue(table, obj); return 0 == count; } } |
所以 __CFDoExternRefOperation
是針對不同的操作,進行具體的方法呼叫,如果 op 是 OPERATION_retain
,就去掉用具體實現 retain 的方法。
從 BasicHash
這樣的方法名可以看出,其實引用計數表就是雜湊表。
key 為 hash(物件的地址) value 為 引用計數。
下圖是 Apple 和 GNU 的實現對比:
5 autorelease 和 autorelaesepool
在蘋果對於 NSAutoreleasePool 的文件中表示:
每個執行緒(包括主執行緒),都維護了一個管理 NSAutoreleasePool 的棧。當創先新的 Pool 時,他們會被新增到棧頂。當 Pool 被銷燬時,他們會被從棧中移除。
autorelease 的物件會被新增到當前執行緒的棧頂的 Pool 中。當 Pool 被銷燬,其中的物件也會被釋放。
當執行緒結束時,所有的 Pool 被銷燬釋放。
對 NSAutoreleasePool 類方法和 autorelease 方法打斷點,檢視其執行過程,可以看到呼叫了以下函式:
1 2 3 4 5 6 7 8 9 10 11 12 |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // 等同於 objc_autoreleasePoolPush id obj = [[NSObject alloc] init]; [obj autorelease]; // 等同於 objc_autorelease(obj) [NSAutoreleasePool showPools]; // 檢視 NSAutoreleasePool 狀況 [pool drain]; // 等同於 objc_autoreleasePoolPop(pool) |
[NSAutoreleasePool showPools]
可以看到當前執行緒所有 pool 的情況:
1 2 3 4 5 6 7 8 |
objc[21536]: ############## objc[21536]: AUTORELEASE POOLS for thread 0x10011e3c0 objc[21536]: 2 releases pending. objc[21536]: [0x101802000] ................ PAGE (hot) (cold) objc[21536]: [0x101802038] ################ POOL 0x101802038 objc[21536]: [0x101802040] 0x1003062e0 NSObject objc[21536]: ############## Program ended with exit code: 0 |
在 objc4 中可以檢視到 AutoreleasePoolPage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
objc4/NSObject.mm AutoreleasePoolPage class AutoreleasePoolPage { static inline void *push() { 生成或者持有 NSAutoreleasePool 類物件 } static inline void pop(void *token) { 廢棄 NSAutoreleasePool 類物件 releaseAll(); } static inline id autorelease(id obj) { 相當於 NSAutoreleasePool 類的 addObject 類方法 AutoreleasePoolPage *page = 取得正在使用的 AutoreleasePoolPage 例項; } id *add(id obj) { 將物件追加到內部陣列 } void releaseAll() { 呼叫內部陣列中物件的 release 方法 } }; void * objc_autoreleasePoolPush(void) { if (UseGC) return nil; return AutoreleasePoolPage::push(); } void objc_autoreleasePoolPop(void *ctxt) { if (UseGC) return; AutoreleasePoolPage::pop(ctxt); } |
AutoreleasePoolPage 以雙向連結串列的形式組合而成(分別對應結構中的 parent 指標和 child 指標)。
thread 指標指向當前執行緒。
每個 AutoreleasePoolPage 物件會開闢4096位元組記憶體(也就是虛擬記憶體一頁的大小),除了上面的例項變數所佔空間,剩下的空間全部用來儲存autorelease物件的地址。
next 指標指向下一個 add 進來的 autorelease 的物件即將存放的位置。
一個 Page 的空間被佔滿時,會新建一個 AutoreleasePoolPage 物件,連線連結串列。
6 __unsafe_unretained
有時候我們除了 __weak
和 __strong
之外也會用到 __unsafe_unretained
這個修飾符,那麼我們對 __unsafe_unretained
瞭解多少?
__unsafe_unretained
是不安全的所有權修飾符,儘管 ARC 的記憶體管理是編譯器的工作,但附有 __unsafe_unretained
修飾符的變數不屬於編譯器的記憶體管理物件。賦值時即不獲得強引用也不獲得弱引用。
來執行一段程式碼:
1 2 3 4 5 6 7 8 9 10 |
id __unsafe_unretained obj1 = nil; { id __strong obj0 = [[NSObject alloc] init]; obj1 = obj0; NSLog(@"A: %@", obj1); } NSLog(@"B: %@", obj1); |
執行結果:
1 2 3 |
2017-01-12 19:24:47.245220 __unsafe_unretained[55726:4408416] A: 2017-01-12 19:24:47.246670 __unsafe_unretained[55726:4408416] B: Program ended with exit code: 0 |
對程式碼進行詳細分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
id __unsafe_unretained obj1 = nil; { // 自己生成並持有物件 id __strong obj0 = [[NSObject alloc] init]; // 因為 obj0 變數為強引用, // 所以自己持有物件 obj1 = obj0; // 雖然 obj0 變數賦值給 obj1 // 但是 obj1 變數既不持有物件的強引用,也不持有物件的弱引用 NSLog(@"A: %@", obj1); // 輸出 obj1 變數所表示的物件 } NSLog(@"B: %@", obj1); // 輸出 obj1 變數所表示的物件 // obj1 變數表示的物件已經被廢棄 // 所以此時獲得的是懸垂指標 // 錯誤訪問 |
所以,最後的 NSLog 只是碰巧正常執行,如果錯誤訪問,會造成 crash
在使用 __unsafe_unretained
修飾符時,賦值給附有 __strong
修飾符變數時,要確保物件確實存在
第二篇 Block
花幾分鐘時間看下面三個小題目,寫下你的答案。
這個三個小題目,我在整理此片博文之前給了三位朋友去解答,最後的結果,除了一位朋友 3 題全部正確,其他兩個朋友均只答中 1 題。
說明還是有很多 iOS 的朋友對於 Block 並沒有透徹理解。本篇博文會對 Block 進行詳細的解說。
1 Block 使用的簡單規則
先了解簡單規則,再去分析原理和實現:
Block 中,Block 表示式截獲所使用的自動變數的值,即儲存該自動變數的瞬間值。
修飾為__block
的變數,在捕獲時,獲取的不再是瞬間值。
至於 Why,後面將會繼續說。
2 Block 的實現
Block 是帶有自動變數(區域性變數)的匿名函式。
Block 表示式很簡單,總體可以描述為:『^ 返回值型別 引數列表 表示式
』。
但是 Block 並不是 Objective-C 中才有的語法,這是怎麼一回事?
clang 編譯器提供給程式設計師瞭解 Objective-C 背後機制的方法,通過 clang 的轉換可以看到 Block 的實現原理。
通過 clang -rewrite-objc yourfile.m
clang 將會把 Objective-C 的程式碼轉換成 C 語言的程式碼。
2.1 Block 基本實現剖析
用 Xcode 建立 Command Line 專案,寫如下程式碼:
1 2 3 4 5 |
int main(int argc, const char * argv[]) { void (^blk)(void) = ^{NSLog(@"Block")}; blk(); return 0; } |
用 clang 轉換:
以上是轉換後的程式碼,不要方,一段一段看。
可以看到,Block 內部的內容,被轉換成了一個普通的靜態函式 __main_func_0
。
再看其他部分:
main.cpp __block_impl:
1 2 3 4 5 6 |
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; |
__block_impl
結構體包括了一些標誌、今後版本升級預留的變數、函式指標。
main.cpp __main_block_desc_0:
1 2 3 4 |
static struct __main_block_desc_0 { size_t reserved; size_t Block_size; } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; |
__main_block_desc_0
結構體包括了今後版本升級預留的變數、block 大小。
main.cpp __main_block_impl_0:
__main_block_impl_0
結構體含有兩個成員變數,分別是 __block_impl
和 __main_block_desc_0
例項變數。
此外,還含有一個構造方法。該構造方法在 main 函式中被如下呼叫:
main.cpp __main_block_impl_0 建構函式的呼叫:
1
2
|
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA));
|
去掉各種強制轉換,做簡化:
main.cpp __main_block_impl_0 建構函式的呼叫 簡化:
1 2 |
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA); struct __main_block_impl_0 *blk = &tmp; |
以上程式碼即:將 __main_block_impl_0
結構體例項的指標,賦值給 __main_block_impl_0
結構體指標型別的變數 blk
。也就是我們最初的結構體定義:
1 |
void (^blk)(void) = ^{NSLog(@"Block");}; |
另外,main 函式中還有另外一段:
1 |
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); |
去掉各種轉換:
1 |
(*blk->impl.FuncPtr)(blk); |
實際就是最初的:
1 |
blk(); |
本節所有程式碼在 block_implementation 中
2.2 Block 截獲外部變數瞬間值的實現剖析
2.1 中對最簡單的 無引數 Block 宣告、呼叫 進行了 clang 轉換。接下來再看一段『截獲自動變數』的程式碼(可以使用命令 clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 main.m
):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int main(int argc, const char * argv[]) { int val = 10; const char *fmt = "val = %d\n"; void (^blk)(void) = ^{printf(fmt, val);}; val = 2; fmt = "These values were changed, val = %d\n"; blk(); return 0; } |
clang 轉換之後:
和 2.1 節中的轉換程式碼對比,可以發現多了一些程式碼。
首先,__main_block_impl_0
多了一個變數 val
,並在建構函式的引數中加入了 val
的賦值:
main.cpp __main_block_impl_0:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; const char *fmt; int val; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; |
而在 main 函式中,對 Block 的宣告變為此句:
main.cpp __main_block_impl_0 建構函式的呼叫:
1 |
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val)); |
去掉轉換:
main.cpp __main_block_impl_0 建構函式的呼叫 簡化:
1 2 |
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, val); struct __main_block_impl_0 *blk = &tmp; |
_所以,在 Block 被宣告時,Block 已經將 val
作為 __main_block_impl_0
的內部變數儲存下來了。無論在在宣告之後怎樣更改 val 的值,都不會影響,Block 呼叫時訪問的內部 val 值。這就是 Block 捕獲變數瞬間值的原理。_
本節所有程式碼在 EX05 中
2.3 __block 變數的訪問實現剖析
我們知道,Block 中能夠讀取,但是不能更改一個區域性變數,如果去更改,Xcode 會提示你無法在 Block 內部更改變數。
Block 內部只是對區域性變數只讀,但是 Block 能讀寫以下幾種變數:
- 靜態變數
- 靜態全域性變數
- 全域性變數
也就是說以下程式碼是沒有問題的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int global_val = 1; static int static_global_val = 2; int main(int argc, const char * argv[]) { static int static_val = 3; void (^blk)(void) = ^ { global_val = 1 * 2; static_global_val = 2 * 2; static_val = 3 * 2; } return 0; } |
如果想在 Block 內部寫區域性變數,需要對訪問的區域性變數增加 __block 修飾。
__block 修飾符其實類似於 C 語言中 static、auto、register 修飾符。用於指定將變數值設定到哪個儲存域中。
具體 __block 之後究竟做了哪些變化我們可以寫程式碼測試:
EX07:
1 2 3 4 5 6 7 |
int main(int argc, const char * argv[]) { __block int val = 10; void (^blk)(void) = ^{val = 1;}; return 0; } |
clang 轉換之後:
跟 2.2 對比,似乎又加了非常程式碼。發現多了兩個結構體。
main.cpp __Block_byref_val_0:
1 2 3 4 5 6 7 |
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; |
很驚奇的發現,block 型別的 val
變成了結構體 Block_byref_val_0
的例項。這個例項內,包含了
isa指標、一個標誌位
flags、一個記錄大小的
size。最最重要的,多了一個
forwarding指標和
val 變數。這是怎麼回事?
在 main 函式部分,例項化了該結構體:
main.cpp main.m 部分:
1 2 3 4 5 |
__Block_byref_val_0 val = {(void*)0, (__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10}; |
我們可以看出該結構體物件初始化時:
- __forwarding 指向了結構體例項本身在記憶體中的地址
- val = 10
而在 main 函式中,val = 1
這句賦值語句變成了:
main.cpp val = 1;
對應的函式
1 |
(val->__forwarding->val) = 1; |
這裡就可以看出其精髓,val = 1,實際上更改的是 __Block_byref_val_0
結構體例項 val 中的 __forwarding
指標(也就是本身)指向的 val
變數。
而對 val
訪問也是如此。你可以理解為通過取地址改變變數的值,這和 C 語言中取地址改變變數類似。
所以,宣告 block 的變數可以被改變。至於 forwarding 的其他巨大作用,會繼續分析。
本節程式碼在 EX05 中
3 Block 的儲存域
Block 有三種型別,分別是:
- __NSConcreteStackBlock ————————棧中
- __NSConcreteGlobalBlock ————————資料區域中
- __NSConcreteMallocBlock ————————堆中
__NSConcreteGlobalBlock 出現的地方有:
- 設定全域性變數的地方有 Block 語法時
- Block 語法的表示式中不使用任何外部變數時
設定在棧上的 Block,如果所屬的變數作用域結束,Block 就會被廢棄。如果其中用到了 block,block 所屬的變數作用域結束也會被廢棄。
為了解決這個問題,Block 在必要的時候就需要從棧中移到堆中。ARC 有效時,很多情況下,編譯器會幫助完成 Block 的 copy,但很多情況下,我們需要手動 copy Block。
對不同儲存域的 Block copy 時,影響如下:
copy 時,對訪問到的 __block 型別物件影響如下:
此時可以看出
__forwarding
的巨大作用——無論 Block 此時在堆中還是在棧中,由於__forwarding
指向區域性變數轉換成的結構體例項的真是地址,所以都能確保正確的訪問。
具體的來說:
- 當 block 變數被一個 Block 使用時,Block 從棧複製到堆,block 變數也會被複制到,並被該 Block 持有。
- 在 block 變數被多個 Block 使用時,在任何一個 Block 從棧複製到堆時, block 變數也會被複制到堆,並被該 Block 持有。但由於
__forwarding
指標的存在,無論 block 變數和 Block 在不在同一個儲存域,都可以正確的訪問 block 變數。 - 如果堆上的 Block 被廢棄,那麼它所使用的 __block 變數也會被釋放。
前面說到編譯器會幫助完成一些 Block 的 copy,也有手動 copy Block。那麼 Block 被複制到堆上的情況有(此段摘自於『Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理』):
- 呼叫 Block 的 copy 方法時
- Block 作為返回值時
- 將 Block 賦值給附有
__strong
修飾符的成員變數時(id型別或 Block 型別)時 - 在方法名中含有
usingBlock
的 Cocoa 框架方法或 GCD 的 API 中傳遞 Block 時
4 Block 迴圈引用
Block 迴圈引用,是在程式設計中非常常見的問題,甚至很多時候,我們並不知道發生了迴圈引用,直到我們突然某一天發現『怎麼這個物件沒有呼叫 delloc』,才意識到有問題存在。
在『Block 儲存域』中也說明了 Block 在 copy 後對 __block 物件會 retain 一次。
那麼對於如下情況就會發生迴圈引用:
block_retain_cycle:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@interface MyObject : NSObject @property (nonatomic, copy) blk_t blk; @property (nonatomic, strong) NSObject *obj; @end @implementation MyObject - (instancetype)init { self = [super init]; _blk = ^{NSLog(@"self = %@", self);}; return self; } - (void)dealloc { NSLog(@"%@ dealloc", self.class); } @end int main(int argc, const char * argv[]) { id myobj = [[MyObject alloc] init]; NSLog(@"%@", myobj); return 0; } |
由於 self -> blk,blk -> self,雙方都無法釋放。
但要注意的是,對於以下情況,同樣會發生迴圈引用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
block_retain_cycle @interface MyObject : NSObject @property (nonatomic, copy) blk_t blk; // 下面是多加的一句 @property (nonatomic, strong) NSObject *obj; @end @implementation MyObject - (instancetype)init { self = [super init]; // 下面是多加的一句 _blk = ^{NSLog(@"self = %@", _obj);}; return self; } - (void)dealloc { NSLog(@"%@ dealloc", self.class); } @end int main(int argc, const char * argv[]) { id myobj = [[MyObject alloc] init]; NSLog(@"%@", myobj); return 0; } |
這是由於 self -> obj,self -> blk,blk -> obj。這種情況是非常容易被忽視的。
5 重審問題
我們再來看看最初的幾個小題目:
- 第一題:
由於 Block 捕獲瞬間值,所以輸出為
in block val = 0
- 第二題:
由於
val
為 __block,外部更改會影響到內部訪問,所以輸出為in block val = 1
- 第三題:
和第二題類似,
val = 1
能影響到 Block 內部訪問,所以先輸出in block val = 1
,之後在 Block 內部更改val
值,再次訪問時輸出after block val = 2
。
Other
我寫這篇文章是在我閱讀了『Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理』一書之後,博文中也有很內容源於『Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理』。
非常向大家推薦此書。這本書裡記錄了關於 iOS 記憶體管理的深入內容。但要注意的是,此書中的多處知識點並不是很詳細,需要你以擴充的心態去學習。在有解釋不詳細的地方,自己主動去探索,去擴充,找更多的資料,最後,你會發現你對 iOS 記憶體管理有了更多的深入的理解。
對於文章中的測試程式碼,全部在這裡。
有什麼問題都可以在博文後面留言,或者微博上私信我,或者郵件我coderfish@163.com。
博主是 iOS 妹子一枚。
希望大家一起進步。
我的微博:小魚周凌宇