BLOCK介紹及常見問題
- 2019 年 10 月 28 日
- 筆記
前言
這段時間小編在整理開發程式碼問題時發現開發同學在使用block時經常出現一些BUG,其中還有一些隱藏的很深的問題,這裡小編就為大家介紹一下block的原理,簡單用法和常見問題。

Block概要
Block:帶有自動變數的匿名函數。
匿名函數:沒有函數名的函數,一對{}包裹的內容是匿名函數的作用域。
Block表達式語法:^ 返回值類型 (參數列表) {表達式}

返回類型為空:

參數列表為空:

聲明Block類型變數語法:返回值類型 (^變數名)(參數列表) = Block表達式
聲明一個變數名為blk的Block:

Block實現原理
Block實際上是作為極普通的C語言源碼來處理的:含有Block語法的源碼首先被轉換成C語言編譯器能處理的源碼,再作為普通的C源程式碼進行編譯。首先我們先寫一個簡單的block。

利用終端編譯生成C++程式碼:
clang -rewrite-objc main.m
static void __main_block_func_0( struct __main_block_impl_0 *__cself) { int count = __cself->count; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0, count); }
這是一個函數的實現,對應Block中{}內的內容,這些內容被當做了C語言函數來處理,函數參數中的__cself相當於Objective-C中的self。
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; //描述Block大小、版本等資訊 int count; //構造函數函數 __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _count, int flags=0) : count(_count) { impl.isa = &_NSConcreteStackBlock; //在函數棧上聲明,則為_NSConcreteStackBlock impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
__main_block_impl_0即為main()函數棧上的Block結構體,其中的__block_impl結構體聲明如下:
struct __block_impl { void *isa;//指明對象的Class int Flags; int Reserved; void *FuncPtr; };
__block_impl結構體,即為Block的結構體,可理解為Block的類結構。
再看下main()函數翻譯的內容:
int main() { int count = 10; void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); }
由此,可以看出,Block也是Objective-C中的對象。
Block常見問題
問題1. Block對外部變數進行修改,最後沒有生效導致出現BUG
首先,為什麼Block不能修改外部自動變數?
自動變數存於棧中,在當前方法執行完,會釋放掉。一般來說,在 block 中用的變數值是被複制過來的,自動變數是值類型複製,新開闢棧空間,所以對於新複製變數的修改並不會影響這個變數的真實值(也稱只讀拷貝)。大多情況下,block是作為參數傳遞以供後續回調執行的。所以在你想要在block中修改此自動變數時,變數可能已被釋放,所以不允許block進行修改是合理的。 對於 static 變數,全局變數,在 block中是有讀寫許可權的,因為此變數存於全局數據區(非棧區),不會隨時釋放掉,也不會新開闢記憶體創建變數, block 拷貝的是指向這些變數的指針,可以修改原變數。
那麼怎麼讓自動變數不被釋放,還能被修改呢?
__block修飾符把所修飾的變數包裝成一個
結構體對象
,即可完美解決。Block既可以強引用此結構體對象,使之不會自動釋放,也可以拷貝到指向該結構體對象的指針,通過指針修改結構體的變數,也就是__block所修飾的自動變數。
問題2. Block在使用過程中出現循環引用
在測試過程中,我們經常遇到記憶體泄漏問題,這裡提到的循環引用就是引起記憶體泄漏的元兇之一,而且Block的循環引用很難被開發同學察覺,因此也需要我們重點注意。
舉例說明:
//DemoObj.m @interface DemoObj () @property (nonatomic, strong) NSMutableArray *myBlocks; @end #pragma mark 將程式碼改為調用self的方法 -(NSMutableArray *)myBlocks { if (_myBlocks == nil) { _myBlocks = [NSMutableArray array]; } return _myBlocks; } - (instancetype)init { self = [super init]; if (self) { int(^sum)(int, int) = ^(int x, int y) { return [self sum:x y:y]; }; [self.myBlocks addObject:sum]; } return self; } -(int)sum:(int) x y:(int)y { return x + y; } #pragma mark 對象被釋放時自動調用 - (void)dealloc { NSLog(@"DemoObj被釋放"); }
大家閱讀完上述程式碼,請問創建的對象可以被正常銷毀嗎?
答案是否定的,產生問題的原因就是
int(^sum)(int, int) = ^(int x, int y) { return [self sum:x y:y]; };
此時sum的block對self強引用,在加上self對myBlocks強引用:
@property (nonatomic, strong) NSMutableArray *myBlocks;
以及sum block被添加到數組時,會被數組強引用:
[self.myBlocks addObject:sum];
這三個引用之間形成了循環引用,如下圖:

那我們如何解除循環引用呢?
1. 在block程式碼中不要引用self以及其他局部變數
int(^sum)(int, int) = ^(int x, int y) { return x + y; };
2. 使用__weak關鍵字,可以將局部變數聲明為弱引用
- (instancetype)init { self = [super init]; if (self) { __weak DemoObj *weakSelf = self; int(^sum)(int, int) = ^(int x, int y) { return [weakSelf sum:x y:y]; }; [self.myBlocks addObject:sum]; } return self; }
結語
在開發程式碼中,Block的使用特別的頻繁,因此我們在做程式碼分析時也要重點關注其程式碼中的常見問題,以免將這類問題遺漏到測試末期,造成產品delay或產生更大的工作量。