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或產生更大的工作量。