總述
以下環境都在ARC環境下,常規設定,使用XCode10測試。
這篇文章會解決以下幾個問題:
-
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
是如何實現資料同步的?
這就利用__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高階程式設計》
- 淺談 block - 截獲變數方式
- Blocks Programming Topics
- Working with Blocks
- fuckingblock