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多线程和内存管理

推荐阅读更多精彩内容