Objective-C記憶體管理:Block
-
Block作為屬性宣告時為什麼都宣告為Copy?
-
Block為什麼能儲存外部變數?
-
Block中
__block
關鍵字為何能同步Block外部和內部的值? -
Block有幾種型別?
-
什麼時候棧上的Block會複製到堆?
-
Block的迴圈引用應該如何處理?
-
Block外部
__weak typeof(self) weakSelf = self;
Block 內部typeof(weakSelf) strongSelf = weakSelf;
,為什麼需要這樣操作?
Block測試:
以下Block在ARC環境下能正常執行嗎?若能分別列印什麼值?
void exampleA_addBlockToArray(NSMutableArray*array) { char b = 'A'; [array addObject:^{ printf("%c\n", b); }]; } void exampleA() { NSLog(@"---------- exampleA ---------- \n"); NSMutableArray *array = [NSMutableArray array]; exampleA_addBlockToArray(array); void(^block)(void) = [array objectAtIndex:0]; block(); } 複製程式碼
void exampleB_addBlockToArray(NSMutableArray *array) { [array addObject:^{ printf("B\n"); }]; } void exampleB() { NSLog(@"---------- exampleB ---------- \n"); NSMutableArray *array = [NSMutableArray array]; exampleB_addBlockToArray(array); void(^block)(void) = [array objectAtIndex:0]; block(); } 複製程式碼
typedef void(^cBlock)(void); cBlock exampleC_getBlock() { char d = 'C'; return^{ printf("%c\n", d); }; } void exampleC() { NSLog(@"---------- exampleC ---------- \n"); cBlock blk_c = exampleC_getBlock(); blk_c(); } 複製程式碼
NSArray* exampleD_getBlockArray() { int val = 10; return [[NSArray alloc] initWithObjects:^{NSLog(@"blk1:%d",val);}, ^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk0:%d",val);}, nil]; } void exampleD() { NSLog(@"---------- exampleD ---------- \n"); typedef void (^blk_t)(void); NSArray *array = exampleD_getBlockArray(); NSLog(@"array count = %ld", [array count]); blk_t blk = (blk_t)[array objectAtIndex:1]; blk(); } 複製程式碼
NSArray* exampleE_getBlockArray() { int val = 10; NSMutableArray *mutableArray = [NSMutableArray new]; [mutableArray addObject:^{NSLog(@"blk0:%d",val);}]; [mutableArray addObject:^{NSLog(@"blk1:%d",val);}]; [mutableArray addObject:^{NSLog(@"blk2:%d",val);}]; return mutableArray; } void exampleE() { NSLog(@"---------- exampleE ---------- \n"); typedef void (^blk_t)(void); NSArray *array = exampleE_getBlockArray(); NSLog(@"array count = %ld", [array count]); blk_t blk = (blk_t)[array objectAtIndex:1]; blk(); } 複製程式碼
void exampleF() { NSLog(@"---------- exampleF ---------- \n"); typedef void (^blk_f)(id obj); __unsafe_unretained blk_f blk; { id array = [[NSMutableArray alloc] init]; blk = ^(id obj) { [array addObject:obj]; NSLog(@"array count = %ld", [array count]); }; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); } 複製程式碼
void exampleG() { NSLog(@"---------- exampleG ---------- \n"); typedef void (^blk_f)(id obj); blk_f blk; { id array = [[NSMutableArray alloc] init]; blk = ^(id obj) { [array addObject:obj]; NSLog(@"array count = %ld", [array count]); }; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); } 複製程式碼
void exampleH() { NSLog(@"---------- exampleH ---------- \n"); typedef void (^blk_f)(id obj); blk_f blk; { id array = [[NSMutableArray alloc] init]; id __weak weakArray = array; blk = ^(id obj) { [weakArray addObject:obj]; NSLog(@"array count = %ld", [weakArray count]); }; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); } 複製程式碼
void exampleI() { NSLog(@"---------- exampleI ---------- \n"); typedef void (^blk_g)(id obj); blk_g blk; { id array = [[NSMutableArray alloc] init]; __block id __weak blockWeakArray = array; blk = [^(id obj) { [blockWeakArray addObject:obj]; NSLog(@"array count = %ld", [blockWeakArray count]); } copy]; } blk([[NSObject alloc] init]); blk([[NSObject alloc] init]); } 複製程式碼
什麼是Block
Objective-C中的Block中文名閉包,是C語言的擴充功能,是一個匿名函式並且可以截獲(儲存)區域性變數。通過三個小節來解釋這個概念。
其他語言中的Block概念
程式語言 | Block的名稱 |
---|---|
Swift | Closures |
Smalltalk | Block |
Ruby | Block |
LISP | Lambda |
Python | Lambda |
Javascript | Anonymous function |
為什麼Block的寫法很彆扭?
因為Block是在模仿C語言函式指標的寫法:
int func(int count) { return count + 1; } // int (^tmpBlock)(int i) = ... int (*funcptr)(int) = &func; 複製程式碼
但是Block的寫法依舊非常難記,國外的朋友更是專門寫了一個叫fuckingblock網頁提供Block的各種寫法。
截獲區域性變數(或叫自動變數)
// 演示擷取區域性變數 int tmpVal = 10; void (^blk)(void) = ^{ printf("val = %d", tmpVal); // val = 10 }; tmpVal = 2; blk(); 複製程式碼
這裡依舊顯示 val = 10
,Block會擷取當前狀態下 val
的值。至於為什麼能截獲區域性變數的值,我們下一節中討論。
Block實現原理
Block結構
通過 clang -rewrite-objc main.m
將上面的示例程式碼翻譯成C,關鍵程式碼如下:
// Block基礎結構 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; 複製程式碼
Block如何擷取區域性變數
// 根據示例中blk的實現,生成不同的 __main_block_impl_0 結構體。 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int tmpVal; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int flags=0) : tmpVal(_tmpVal) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 複製程式碼
根據上面的程式碼能解決我們3個疑惑:
-
__block_impl
中有isa
指標,那麼Block
也是一個物件。 - 生成不同的
__main_block_impl_0
,這裡結構裡面包含int tmpVal
就是我們區域性變數,而__main_block_impl_0
的建構函式中是值傳遞。所以block內部截獲的變數不受外部影響。 -
__main_block_impl_0
建構函式中有個void *fp
函式指標指向的就是block實現。
我們向上面示例程式碼再新增多一些變數型別:
static int outTmpVal = 30; // 靜態全域性變數 int main(int argc, char * argv[]) { int tmpVal = 10;// 區域性變數 static int localTmpVal = 20;// 區域性靜態變數 NSMutableArray *localMutArray = [NSMutableArray new];// 區域性OC物件 void (^blk)(void) = ^{ printf("val = %d\n", tmpVal); // val = 10 printf("localTmpVal = %d\n", localTmpVal); // localTmpVal = 21 printf("outTmpVal = %d\n", outTmpVal); // outTmpVal = 31 [localMutArray addObject:@"newObj"]; printf("localMutArray.count = %d\n", (int)localMutArray.count); // localMutArray.count = 2 }; tmpVal = 2; localTmpVal = 21; outTmpVal = 31; [localMutArray addObject:@"startObj"]; blk(); } 複製程式碼
對應輸出結果為:
val = 10
localTmpVal = 21
outTmpVal = 31
localMutArray.count = 2
clang -rewrite-objc main.m
後關鍵程式碼如下:
static int outTmpVal = 30; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int tmpVal; int *localTmpVal; NSMutableArray *localMutArray; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int *_localTmpVal, NSMutableArray *_localMutArray, int flags=0) : tmpVal(_tmpVal), localTmpVal(_localTmpVal), localMutArray(_localMutArray) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; 複製程式碼
因為涉及到OC物件,這裡還會有2個新的方法,這2個方法會放到後面講:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src){ _Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/); } 複製程式碼
-
static int outTmpVal = 30;
儲存在記憶體中的.data
段,static
限制了作用域,該檔案作用域內可修改。 -
static int localTmpVal = 20;
在int main(int argc, char * argv[]) { }
作用域可修改,注意__main_block_impl_0
建構函式中是傳遞的*_localTmpVal
指標,所以外部修改Block內部同樣有效,因為是static
所以,Block內部也可以修改localTmpVal
的值。 -
NSMutableArray *localMutArray
向__main_block_impl_0
傳遞的是指向的地址,所以localMutArray
內部操作對於block內同樣有效。
- 靜態變數的這種方式同樣也可以作用到區域性變數上,傳遞一個指標到block內,通過指標來讀取指向的值,通知也可以修改。但是這種方式在block離開區域性變數所在作用域後再呼叫就會出現問題,因為區域性變數已經被釋放。
-
static int localTmpVal = 20;
能通過指標的方式修改值,NSMutableArray *localMutArray
修改指向的值為什麼不可以? 這是clang對於Block內修改指標的一個保護措施。
總結下:
-
靜態變數
、靜態全域性變數
、全域性變數
都可以訪問,修改,保持同一份值。 - OC物件,可以進行內部操作。但不能修改OC物件的值(指向的記憶體地址)。
__block關鍵字如何實現?
同樣的方式,我們先看 __block
用C是怎麼實現的,下面是一段使用 __block
的程式碼:
int main(int argc, char * argv[]) { __block int val = 10; void (^blk)(void) = ^{ val = 1; printf("val = %d", val); }; blk(); } 複製程式碼
翻譯成C,只保留關鍵程式碼:
struct __Block_byref_val_0 { void *__isa; __Block_byref_val_0 *__forwarding; int __flags; int __size; int val; }; 複製程式碼
這就是 __block
對應C中的新結構體:
-
*__forwarding
是一個與自己同類型的指標。 -
int val;
這個變數就是為了儲存原本__block int val = 10;
的值。 - 並且
__block int val = 10;
對應的結構體__Block_byref_val_0
也是和之前一樣建立在棧上的。
接下來繼續看, blk
的結構:
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_val_0 *val; // by ref __main_block_impl_0.... // 和之前的__block_impl構造方式一致 }; 複製程式碼
blk
結構內部新增了 __Block_byref_val_0 *val
作為成員變數,和之前原理一致。
blk
的實現 val = 1;
:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_val_0 *val = __cself->val; // bound by ref (val->__forwarding->val) = 1; printf("val = %d", (val->__forwarding->val)); } 複製程式碼
(val->__forwarding->val) = 1;
這句非常重要,不是直接通過 val->val
進行賦值操作,而是經過 __forwarding
指標進行賦值,這帶來非常大的靈活性,現在是 blk
和 __block int val
都是在棧上, __forwarding
也都指向了棧上的 __Block_byref_val_0
。以上程式碼解決了在Block內修改外部區域性變數的值。
__block
新增了2個方法: __main_block_copy_0
和 __main_block_dispose_0
:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } 複製程式碼
通過方法命名和引數,可以大致猜出是對 Block
的拷貝和釋放。
Block和__block的儲存區域
通過以上 clange
的編譯,Block和__block都是有isa指標的,兩者都應該是Objective-C的物件。isa指向的就是它的類物件。在ARC下大致有以下幾種,根據名字可以知道對應儲存空間:
- _NSConcreteStackBlock 棧上
- _NSConcreteGlobalBlock 全域性 對應的是.data段
- _NSConcreteMallocBlock 堆上
clang轉出的結果和執行程式碼時 Block 實際顯示的isa型別是不一樣的,在實際的編譯過程中已經不會經過clang翻譯成C再編譯。
_NSConcreteGloalBlock
有兩種情況下可以生成:
- 宣告的是全域性變數Block。
- 在作用域內但是不截獲外部變數。
_NSConcreteStackBlock
因為在棧上,在函式作用域內宣告的Block。
_NSConcreteMallocBlock
正因為 _NSConcreteStackBlock
的作用域在棧上,超出作用域後想要繼續使用Block,這就得複製到堆上。那些情況會觸發這種複製:
- ARC下大多數情況會自動複製。比如,棧上
block
賦值給Strong
修飾的屬性時。Block
作為一個返回值時(超出作用域還能使用,autorelease處理物件生命週期)。 - 需要手動copy。 向方法或函式的引數中傳遞Block時 ,編譯器無法判斷是什麼樣的情況,因為從Block從棧上覆制到堆上很消耗cpu。所以編譯器並沒有幫忙
copy
。 - Cocoa框架的方法且方法名中含有
usingBlock
等時,不用外部copy
。內部已經進行copy。 -
GCD
的Api,也不用外部copy
。
這裡有個比較經典的例子(摘自《Objective-C高階程式設計》):
- (id)getBlockArray { int val = 10; return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk1:%d",val);}, nil]; } { id obj = [self getBlockArray]; typedef void (^blk_t)(void); blk_t blk = (blk_t)[obj objectAtIndex:0]; blk(); } // crash 複製程式碼
在ARC情況下,NSArray 陣列類會有2個元素,第一個在堆上,第二個棧上。在超出getBlockArray作用域後,第二棧上的block會變成野指標。在所有作用域結束時,Array會釋放陣列內所有元素。野指標物件執行銷燬時會觸發崩潰。 正常情況下 NSArray
應該持有陣列內所有元素。但使用 initWithObjects:
方法時,發現只有第一個元素進行了持有操作,第二個 Block
依舊在棧上。當我使用 NSMutableArray
的 addObject:
方法時,每個Block都會進行持有賦值到堆上。我懷疑應該是 initWithObjects:
方法中多參形式比較特殊。
反覆提到Block就是OC的物件,對於物件Copy會帶來哪些變化:
Block類 | 原來儲存域 | 複製產生的影響 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧複製到堆 |
_NSConcreteGlobalBlock | .data | 無變化 |
_NSConcreteMallocBlock | 堆 | 引用計數增加 |
__block的儲存區域
Block是一個OC物件,所以涉及到從棧到堆,引用計數的變更等,常見OC物件記憶體管理的問題。同時Block在堆上時又會對 __block
進行持有,那麼對於 __block
同樣也是OC物件,記憶體管理有什麼區別呢?
Block從棧複製到堆時對__block變數產生的影響:
__block 儲存域 | Block 從棧複製到堆時對__block的影響 |
---|---|
棧 | 從棧複製到堆並被Block持有 |
堆 | 被Block持有 |
-
__block
從棧上覆制到堆上後,原本棧上的__block
依舊會存在,被複制到堆上的__block
會被Block持有__block
的引用計數會增加,棧上的__block
會因為作用域結束而釋放,堆上的__block
會在引用計數歸零後釋放。 - 堆上的
__block
的記憶體管理就是OC物件的引用計數管理方式,沒有被其他Block持有時引用計數歸0後釋放。
上面提到當 __block
從棧上覆制到堆上,會有兩個 __block
產生,一個棧上的一個堆上的。這兩個不同儲存區域的 __block
是如何實現資料同步的?
這就利用 ofollow,noindex">__block關鍵字如何實現? 中提到的指向自己的 *__forwarding
,當持有 __block
的Block沒有從棧上拷貝到堆上時: *__forwarding
指向棧上的 __block
, 當持有 __block
的Block拷貝到堆上時後,棧上的 __block
-> __forwarding
->堆上的 __block
,堆上的 __block
-> __forwarding
->堆上的 __block
。讀起來有點繞,借用《Objective-C高階程式設計》中的插圖:

__block 和 OC物件從棧上覆制到堆上?
上面講了 Block
和 __block
在從棧上覆制到堆上時的一些變化。為了解決 __block
和 OC物件
在 Block結構體
內的生命週期問題,新增了一下幾個方法:
- 在
__main_block_desc_0
中新加2個成員方法:copy
和dispose
,這是兩個函式指標,指向的分別就是__main_block_copy_0
和__main_block_dispose_0
。 - 在
Block
中使用OC物件
和__block
關鍵字時新增的2個方法:__main_block_copy_0
和__main_block_dispose_0
,這兩個方法用於在Block
被copy
到堆上時,管理__block
和OC物件
的生命週期。
Block:
static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } 複製程式碼
OC物件:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/); } 複製程式碼
__block:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { _Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } static void __main_block_dispose_0(struct __main_block_impl_0*src) { _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/); } 複製程式碼
捕獲 OC物件
和使用 __block
變數時在引數上會不同:
OC物件 | BLOCK_FIELD_IS_OBJECT |
---|---|
__block | BLOCK_FIELD_IS_BYREF |
_Block_object_assign
就相當於 retain
方法, _Block_object_dispose
就相當於 release
方法,但是我們在clang翻譯的C語言中並沒有發現 __main_block_copy_0
和 __main_block_dispose_0
的呼叫。只有在以下時機 copy
和 dispose
方法才會呼叫:
copy函式 | 棧上的Block複製到堆時 |
---|---|
dispose函式 | 堆上的Block被廢棄時(引用計數為0) |
什麼時候棧上的Block會複製到堆?
- 呼叫
Block
的copy
例項方法。 -
Block
作為函式返回值返回時。(autorelease
物件延長生命週期) - 將
Block
賦值給附有__strong
修飾符的id型別的類或Block
型別成員變數(賦值給Strong
修飾的Block
型別屬性時,編譯器會幫忙複製到堆)。 - 在方法名中含有
usingBlock
的Cocoa框架方法
或GCD
的api中傳遞Block
時。
Block tips
一、哪些情況下Block內self為nil時會引起崩潰?這個時候需要使用Weak-Strong-Dance。
-
使用
self.blockxxx()
時,使用clang
轉換成C時,可以看到Bblock的呼叫實際是呼叫
Block`內的函式指標與OC物件呼叫發訊息的形式不一樣。 -
其他業務場景,比如使用
self
的成員變數做NSAarry
或NSDictionary
做增加操作時。不要無腦使用,更加清晰的理解
Weak-Strong-Dance
,Block
內部strong
self
後Block
會繼續持有self
,有些場景並不需要。
解答
- 宣告成
Strong
與Copy
效果都一樣。在ARC環境下編譯會自動將作為屬性的Block
從棧Copy
到堆,這裡Apple建議繼續使用Copy
防止程式設計師忘記編譯器有Copy
動作。 - Block內部能截獲外部變數。
Block
結構體中會有建立一個成員變數與截獲的變數型別一直,這個值與截獲時的值一致,這是一個值傳遞,儲存的是一個瞬時值。 -
__block
關鍵字的實現是一個結構體,結構體中有個自己同類型的*_farwarding
指標,當Block在棧上,__block
也是在棧上時:*_farwarding
指向棧上的自己。當Block拷貝到堆,堆中建立的__block
的*_farwarding
指向自己,同時將棧上的*_farwarding
指向堆中__block
。 - 三種。棧上,堆上,全域性。
- 1 手動
copy
。2 作為返回值返回。3 將Block
賦值給__strong
修飾的id型別
或Block
型別成員變數。4 方面名中含有usingBlock
的cocoa框架方法
或GCD
。 - 使用
__weak
弱引用,或者手動斷開強引用。 -
Block
內的weakSelf
可能會出現nil
的情況,nil
可能會造成奔潰或是其他意外結果。所以在Block
內作用域內宣告一個Strong
型別的區域性變數,在作用域結束後會自動釋放不會造成迴圈引用。
程式設計題目答案,請參考Github上的repo: TestBlock 。
參考
- 《Objective-C高階程式設計》
- iOS-Source-Probe%2FObjective-C%2FRuntime%2F%25E6%25B5%2585%25E8%25B0%2588%2520block%25EF%25BC%25882%25EF%25BC%2589%2520-%2520%25E6%2588%25AA%25E8%258E%25B7%25E5%258F%2598%25E9%2587%258F%25E6%2596%25B9%25E5%25BC%258F.html" rel="nofollow,noindex">淺談 block - 截獲變數方式
- Blocks Programming Topics
- Working with Blocks
- fuckingblock