Block 初探

在介紹Block之前通過一個簡單的應用場景認識下Block

場景描述如下:TableView上面有多個CustomTableViewCell,cell上面顯示的是文字信息和一個詳情Button,點擊button后push到一個新的頁面。

對于這個簡單的需求: 點擊button以實現頁面跳轉。button添加在CustomTableViewCell上,若直接在cell中實現跳轉邏輯,就違背MVC的計模式。項目小還好,隨著功能的擴展,你會發現越寫越難寫;還有一種情況,就是這件事情做不到,只能委托給其他對象來做了,此時使用delegate委托是個很好的選擇。

下面先用delegate實現,代碼如下:
CustomTableViewCell 類中

@protocol CustomTableViewCellDelegate <NSObject>
- (void) pushToNewView ; //代理要實現的方法
@end

@interface CustomTableViewCell : UITableViewCell

@property(nonatomic,strong) UIButton *detailButton;
@property(nonatomic, weak) id<CustomCellDelegate> delegate;

@end

CustomTableViewCell 添加協議,并聲明要代理要實現的方法。

接下來在CustomTableViewCell.m中為button添加點擊事件,
button的 click 事件如下:

- (void)buttonClick:(UIButton *)button {
  if (_delegate && [_delegate respondsToSelector:@selector(pushToNewView)]) {
     [_delegate pushToNewView];
  }
}

跟著就是完善受到委托申請的類,這里是對應CustomTableViewCell所在的控制器,首先遵循CustomTableViewCellDelegate協議,然后要實現其中的pushToNewView方法,最重要的是設置CustomTableViewCell對象cell的delegate等于self。

@interface ViewController ()< CustomTableViewCellDelegate >
@property (nonatomic, strong) NSArray *textArray;
@end

實現 pushToNewView的方法,

- (void) pushToNewView {
    DetailViewController*detailVC = [[DetailViewController alloc] init];
    [self.navigationController pushViewController:detailVC animated:YES];
}

同時需要設置 CustomTableViewCell 對象cell的delegate,在 cellForRow中實現 cell.delegate = self即可。

此時self.delegate其實就是ViewController,cell對象委托了ViewController實現pushToNewVC方法。這個簡單的場景描述了使用代理的一種情況,就是CustomTableViewCell沒有能力實現pushViewController的功能,所以委托ViewController來實現。

------------------- 分割線 --------------------------

接著用 Block 實現上述功能
根據需求:定義一個無參無返的Block, 簡化代碼使用 typedef
typedef void(^ButtonCallback)(void);
同時需要在CustomTableViewCell類中添加block屬性
@property (nonatomic, copy) ButtonCallback buttonCallback;

在button 點擊事件中,調用剛剛聲明的 buttonCallback

- (void)buttonClick:(UIButton *)button {
  if(self.buttonCallback){
     self.buttonCallback();
  }
}

最后我們回到CustomTableViewCell所在的控制器中,去實現 buttonCallback
找到cellForRow的方法,通過cell點出buttonCallback的屬性,并實現

cell.buttonCallback = ^{
    DetailViewController*detailVC = [[DetailViewController alloc] init];
    [self.navigationController pushViewController:detailVC animated:YES];
};

兩種方式對比,Block 要比 delegate 精簡很多.

下面認識下 OC 中的 Block

1. 閉包 (了解)

在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。 ---- 維基百科

閉包就是一個函數,或者一個指向函數的指針,加上這個函數執行的非局部變量。
說的通俗一點,就是閉包允許一個函數訪問聲明該函數運行上下文中的變量,甚至可以訪問不同運行上文中的變量。

2.Block 基本語法及使用

Block實際上是OC語言對閉包的實現,是帶有自動變量值的匿名函數

2.1 Block的原型及定義

Block語法及常見類型

^ 返回值類型 參數列表 表達式

其中,返回值類型可省略

^ 參數列表 表達式

當沒有參數時,可省略參數列表

^表達式

Block基本語法
Block 常見類型

  1. 有參有返回值
NSString(^testBlock)(int) = ^(int num){
      return [NSString stringWithFormat:"%d",num];
};
  1. 有參無返回值
void(^testBlock)(int) = ^(int num){
      NSLog(num = "%d",num);
};
  1. 無參無返回值
void(^testBlock)(void)= ^{
      NSLog(@"無參無返回值");
};
  1. 無參有返回值
int(^testBlock)(void) = ^{
      return 10;
};

下面我們簡單使用下Block:
先寫一個Block原型 (有參有返回值)
NSString *(^myBlock)(int)

下面看下Block的實現部分

myBlock = ^(int num){
    return [NSString stringWithFormat: @"Passed number: %d", num];
};

代碼中將一個函數體賦值給了myBlock變量,其接收一個名為num一個NSString對象。

Block最后的分號一定不能忘

至于調用
myBlock(3) 可以像調用其他函數一樣使用Block。

代碼簡化:
由于block數據類型的語法會降低整個代碼的閱讀性,所以常使用typedef來定義block類型。
typedef NSString(^MyBlock)(int);
上述重定義后構建了MyBlock新類型,這樣我們就可以在屬性聲明或方法中使用更加有語義的數據類型。

下圖很好的總結Block結構
Block.jpg
2.2 Block的使用 (代碼為例)
  1. 界面傳值
  2. 回調
3. Block對外部變量的截獲

3.1 局部變量
局部自動變量,在Block中可被讀取。Block定義時copy變量的值,在Block中作為常量使用,所以即使變量的值在Block外改變,也不影響他在Block中的值,Block此時對局部變量只是做了值傳遞的操作。

3.2 static 修飾的全局變量
因為全局變量或靜態變量在內存中的地址是固定的,Block在讀取該變量值的時候是直接從其所在內存地址讀出,獲取到的是最新值,而不是在定義時copy的常量。

3.3 對OC對象的截獲

     NSMutableArray *array = [NSMutableArray array];     
     void(^block)() = ^(){       
         NSObject *obj = [[NSObject alloc] init];    
         [array addObject:obj];         
     };   
     block();

上述代碼編譯通過,Block截獲的值為NSMutableArray類的對象,用C語言表述,就是用的NSMutableArray類的對象所用的結構體實例的指針,所以向該對象中添加元素操作屬于使用截獲變量的值,因此是沒有問題的。那么對該截獲的變量進行賦值

屏幕快照 2016-06-23 下午2.12.28.png

編譯未通過,提示缺少__block修飾符。

3.4 C語言數組

屏幕快照 2016-06-23 下午2.18.28.png

上圖代碼中, 在Block外部定義一個C語言字符串字面量數組, 在Block內部截獲自動變量的方法并沒有實現對C語言數組的截獲, 此時訪問數組元素text[2]會報錯. 此時使用指針可以解決.

 const char *text = "adsdczv";
 void(^block)() = ^(){
       NSLog(@"%c",text[2]);
 };
 block();

3.5 ______block 修飾的變量
某些場景下,我們需要在Block內部對外部變量進行修改。這時需要使用__block來修飾該變量實現在Block內部的修改,此時Block是復制其引用地址來實現訪問的。

關于______block 修飾符
從上面講解我們已經知道,Block內部能夠讀取外部局部變量的值。但如果我們需要在Block內部修改變量的值,則需要在Block外部給該變量添加一個__block修飾符。
__block另一個使用場景是,避免某些情況下Block使用中出現的循環引用的問題,此時可以給相應的對象加上一個__block來修飾。

為什么使用__block可以實現在Block內部修改外部變量的值?

這邊我們用一個Block代碼,并使用clang _rewrite_objc命令轉換成C++的代碼來說明__block是怎么實現內部變量的修改。
Block在main中實現

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger val = 10;
        void (^block)(void) = ^{
            NSLog(@"%ld", val);
        };
        block();
    }
    return 0;
}

轉碼后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct  __main_block_impl_0 *__cself) {
  int val = __cself->val;  // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__val_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_41daf1_mi_0, val);
}

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)};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int val = 10;
        void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val);
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

從展開代碼可以發現,Block被轉成了一個struct __main_block_impl_0類型的結構體實例,并且該結構體成員中包含局部變量val。當執行Block時,通過該實例找到Block執行部分void __main_block_func_0,并把該結構體實例傳入到void __main_block_func_0方法中。
void __main_block_func_0方法中第一個參數聲明如下
struct __main_block_impl_0 *__cself
*注意:這里的__cself就類似于OC中的self, * 而它指向結構體的指針。
此時我們就可以通過__cself->val 訪問該局部變量。

那么問題來了,為什么此時不對變量val進行修改?

因為main函數中的局部變量val和函數__main_block_func_0不在同一個作用域中,調用過程中只是進行了值傳遞。
當然,在上面代碼中,我們可以通過指針來實現局部變量的修改。不過這是由于在調用__main_block_func_0時,main函數棧還沒展開完成,變量val還在棧中。
但是在很多情況下,Block是作為參數傳遞以供后續回調執行的。通常在這些情況下,Block被執行時,定義時所在的函數棧已經被展開,局部變量已經不在棧中了,再用指針訪問會產生野指針錯誤。
所以,這類情況下對于auto類型的局部變量,不允許Block進行修改是合理的。

__block是如何實現變量修改的

此時使用更新后的代碼

添加 __block修飾符后

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSInteger val = 0;
        void (^block)(void) = ^{
            val = 1;
        };
        block();
        NSLog(@"val = %ld", val);
    }
    return 0;
}

使用_rewrite_objc展開

struct __Block_byref_val_0 {
   void *__isa;
   __Block_byref_val_0 *__forwarding;
   int __flags;
   int __size;
   NSInteger val;
};

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(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
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;

        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&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*/);}

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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};

        void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2h_70k4gzp53qn7ytk0cdjr9kk80000gn_T_main_7eb9e7_mi_0,(val.__forwarding->val));

    }
    return 0;
}

這次轉碼后似乎比剛才多了些東西,仔細看下,
一個是__Block_byref_val_0的結構體以及兩個方法static void __main_block_copy_0static void __main_block_dispose_0; 后面的兩個方法先暫且不關注(后面會涉及)。

其實結構體__Block_byref_val_0產生的實例就是我們使用__block修飾過的變量。

struct __Block_byref_val_0 {
   void *__isa;
   __Block_byref_val_0 *__forwarding;
   int __flags;
   int __size;
   NSInteger val;
};

從該結構體聲明可以看出,這個結構體中包含了該實例本身的引用 __forwarding。

  • 我們從上述被轉化的代碼中可以看出 Block 本身也一樣被轉換成了 __main_block_impl_0 結構體實例,該實例持有__Block_byref_val_0結構體實例的指針。

我們再看一下Block實現和調用部分代碼被轉化后的結果:

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;
        }

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
  • 不難發現從__cself找到__Block_byref_val_0結構體實例,然后通過該實例的__forwarding訪問成員變量val。成員變量val是該實例自身持有的變量,指向的是原來的局部變量。

詳情參見下圖:

__block.jpg

至此,已經展示了__block變量在Block中查找和修改的過程,那么:

  • 當Block作為回調執行時,局部變量val已經出棧了,這個時候代碼為什么還能正常工作呢?
  • 我們為什么通過成員變量__forwarding而不是直接去訪問結構體中我們需要修改的變量呢?
這邊需要引入下一個概念: 存儲域

Objective-C中Block的存儲域

我們在上述轉換過的代碼中可以發現 __main_block_impl_0結構體構造函數中,isa指針指向的是 _NSConcreteStackBlock; 而Block還有另外兩個與之相似的類:

  • _NSConcreteGlobalBlock //全局的靜態block 不會訪問任何外部變量
  • _NSConcreteMallocBlock //保存在堆區的,引用計數為0時會被銷毀。
  • _NSConcreteStackBlock //保存在棧區,出棧后被銷毀

上述示例代碼中,Block是被設為_NSConcreteStackBlock,在棧上生成。當我們把Block作為全局變量使用時,對應生成的Block將被設為_NSConcreteGlobalBlock

void (^block)(void) = ^{NSLog(@"This is a Global Block");};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        block();
    }
    return 0;
}

該代碼轉碼c++后,Block結構體的isa指針初始化時如下:
impl.isa = &_NSConcreteGlobalBlock;

_NSConcreteMallocBlock何時被使用

分配在全局變量上的Block,在變量作用域外也可以通過指針安全的訪問。
但分配在棧上的Block,如果它所屬的變量作用域結束,該Block就被廢棄。同樣,__block變量也分配在棧上,當超過該變量的作用域時,該__block變量也會被廢棄。
此時,就需要使用 _NSConcreteMallocBlock,OC中提供了將Block和__block變量從棧上復制到堆上的方法,將分配到棧上的Block復制到堆上,這樣當棧上的Block超過它原本作用域時,堆上的Block還可以繼續存在。
復制到堆上的Block,它的結構體成員變量isa將變為:
impl.isa = &_NSConcreteMallocBlock;

_block變量中結構體成員__forwarding就在此時保證了從棧上復制到堆上能夠正確訪問__block變量。在這種情況下,只要棧上的_block變量的成員變量__forwarding指向堆上的實例,我們就能夠正確訪問。

我們一般可以使用copy方法手動將 Block 或者 __block變量從棧復制到堆上。比如我們把Block做為類的屬性訪問時,我們一般把該屬性設為copy。有些情況下我們可以不用手動復制,比如Cocoa框架中使用含有usingBlock方法名的方法時,或者GCD的API中傳遞Block時。

當一個Block從棧復制到堆中,與之相關的__block變量也會被復制到堆中。此時堆中的Block持有相應堆上的__block變量,當堆上的__block變量沒有持有者,才會被釋放。

  • 而在棧上的__block變量被復制到堆上之后,會將成員變量__forwarding的值替換為堆上的__block變量的地址。這個時候我們可以通過以下代碼訪問:
    val.__forwarding->val
    如下圖:
__block變量和循環引用問題

__block修飾符可以指定任意類型的局部變量。此時還記這兩個方法嗎?

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src {
    _Block_object_assign((void*)&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_object_assign函數持有該變量(相當于retain);
當堆上的Block被廢棄時,會調用_Block_object_dispose函數釋放該變量(相當于release)。

由上文描述可知,我們可以使用下述代碼解除Block循環引用的問題:

__block id tmp = self;
void(^block)(void) = ^{
    tmp = nil;
};

block();

通過執行block方法,nil被賦值到_block變量tmp中。這個時候_block變量對 self 的強引用失效,從而避免循環引用的問題。

*總結:
通過__block變量可以控制對象的生命周期,在不能使用__weak修飾符的環境中,我們可以避免使用__unsafe_unretained修飾符。
在執行Block時可動態地決定是否將nil或者其它對象賦值給__block變量。
但是這種方法有一個明顯的缺點就是,我們必須去執行Block才能夠解除循環引用問題,否則就會出現問題。

4. 比較 ______weak 和 __strong

這邊用AFN中的一段代碼

__weak __typeof(self)weakSelf = self;
AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
      __strong __typeof(weakSelf)strongSelf = weakSelf;

      strongSelf.networkReachabilityStatus = status;
     if (strongSelf.networkReachabilityStatusBlock) {
            strongSelf.networkReachabilityStatusBlock(status);
      }
 };
1. __weak

我們在使用Block時,有時候會用到self,而Block內部對self默認都是強引用。在ARC下,編譯器將Block從棧區拷貝到堆區,Block會強引用和持有self,而self 也會強引用和持有Block,于是就造成了循環引用。

此時就需要使用__weak,在修飾變量時,修飾符修飾變量 self,讓 block 不強引用 self,從而破除循環。

    __weak typeof(self) weakSelf = self;
        self.passValueBlock = ^(NSString *string){      
            dispatch_async(dispatch_get_main_queue(), ^{    
                weakSelf.pointView.startLabel.text = string;        
            });        
        };

弱引用不會影響對象釋放,當一個對象被釋放是,所有指向它的弱引用會被置空,也避免出現野指針。

2. __strong

上面提到,__weak 很好的解決retain Cycle,但還是會存在一些隱患。不知道self什么時候被釋放,為了保證在Block內部不會被釋放,所以使用__strong修飾。

看下一段測試代碼
ViewController添加屬性
@property (nonatomic, strong) ViewController *vc;
viewDidLoad

    ViewController *vc = [[ViewController alloc] init];
    self.vc = vc;
    __weak ViewController * weakVC = self.vc;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSInteger count = 0;  
        while (count < 4) {   
            count++;
            NSLog(@"%@",weakVC);     
            sleep(1);
        }    
    });
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.vc = nil;
    });

實現dealloc方法

- (void)dealloc { 
    NSLog(@"%@",[self class]);
}

看輸出結果:

2016-06-20 15:12:27.797 __strongTest[14823:1753981] <ViewController: 0x7fdbd2724c20>
2016-06-20 15:12:28.802 __strongTest[14823:1753981] <ViewController: 0x7fdbd2724c20>
2016-06-20 15:12:29.797 __strongTest[14823:1753934] ViewController
2016-06-20 15:12:29.804 __strongTest[14823:1753981] (null)
2016-06-20 15:12:30.808 __strongTest[14823:1753981] (null)

可以看出:Block內部的對self.vc是弱引用。當2s后,self.vc在外部被釋放,則Block內部對self.vc的持有也失效。

現在在Block內部對self.vc進行強引用,Block內部代碼調整為:

   __strong ViewController *strongVC = weakVC;
   NSInteger count = 0;
   while (count < 4) {
       count++;
       NSLog(@"%@",strongVC);
       sleep(1);
   }

再看輸出結果:

2016-06-20 15:22:38.423 __strongTest[14839:1762881] <ViewController: 0x7fd632d1f690>
2016-06-20 15:22:39.424 __strongTest[14839:1762881] <ViewController: 0x7fd632d1f690>
2016-06-20 15:22:40.429 __strongTest[14839:1762881] <ViewController: 0x7fd632d1f690>
2016-06-20 15:22:41.430 __strongTest[14839:1762881] <ViewController: 0x7fd632d1f690>
2016-06-20 15:22:42.431 __strongTest[14839:1762835] ViewController

Block內部對對象采用strong修飾后,既使原持有對象在block外部已經被釋放,但Block內部扔能持有,于是執行完Block后,該對象才被dealloc。

總結:weakSelf是為了Block不持有self,避免循環引用,而再聲明一個strongSelf是因為一旦進入Block執行,就不允許self在這個執行過程中釋放。Block執行完后這個strongSelf會自動釋放,沒有循環引用問題。

最后,使用Block時的注意事項

1.Block內部不能直接修改局部變量
Block內部可以訪問外部的變量, 默認是將其拷貝到其數據結構中來實現訪問的, 屬性是只讀的. Block內部不能修改外面的局部變量.
如果要修改需要對要修改的局部變量用__block 修飾, 這樣局部變量就可以在Block內部修改了,Block是復制其引用地址來實現訪問的

2.當Block里面的出現self,造成的循環引用
循環引用就是當self 擁有一個Block的時候,在Block中又調用self的方法。形成了你中有我,我中有你,造成誰都無法將誰釋放。從而發生內存泄漏。
解決方法:
__weak typeof (self) weakSelf = self;
定義一個weakSelf變量并加上__weak修飾符,在Block代碼塊中,所有需要self的地方都用weakSelf來替代。這樣就不會增加引用計數,所以Block持有self對象也就不會造成循環引用,從而避免內存泄漏。

參考

Objective-C中的Block
閉包(Closures)
Objective-C中Block的存儲域
______block & ______weak & __strong
Objective-C 高級編程: iOS和OS X多線程和內存管理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容