Block

block的本质是什么?是 oc 对象(包含isa指针), 是函数及其运行环境、上下文的封装。 常用于

使用

  1. 作为变量

    int (^sum) (int, int); // 定义一个 Block 变量 sum
    // 给 Block 变量赋值
    // 一般 返回值省略:sum = ^(int a,int b)…
    sum = ^int (int a,int b){
    return a+b;
    }; // 赋值语句最后有 分号
    int a = sum(10,20); // 调用 Block 变量
    
  2. 作为属性

    // 1. 给  Calculate 类型 sum变量 赋值「下定义」
    typedef int (^Calculate)(int, int); // calculate就是类型名
    Calculate sum = ^(int a,int b){ 
        return a+b;
    };
    int a = sum(10,20); // 调用 sum变量
       
    // 2. 作为对象的属性声明,copy 后 block 会转移到堆中和对象一起
    @property (nonatomic, copy) Calculate sum;    // 使用   typedef
    @property (nonatomic, copy) int (^sum)(int, int); // 不使用 typedef
       
    // 声明,类外
    self.sum = ^(int a,int b){
        return a+b;
    };
    // 调用,类内
    int a = self.sum(10,20);
    
  3. 作为方法参数

    // ---- 无参数传递的 Block ---------------------------
    // 实现
    - (CGFloat)testTimeConsume:(void(^)())middleBlock {
        // 执行前记录下当前的时间
        CFTimeInterval startTime = CACurrentMediaTime();
        middleBlock();
        // 执行后记录下当前的时间
        CFTimeInterval endTime = CACurrentMediaTime();
        return endTime - startTime;
       
    }
       
    // 调用
    [self testTimeConsume:^{
           // 放入 block 中的代码 
       
    }];
       
    // ---- 有参数传递的 Block ---------------------------
    // 实现
    - (CGFloat)testTimeConsume:(void(^)(NSString * name))middleBlock {
        // 执行前记录下当前的时间
        CFTimeInterval startTime = CACurrentMediaTime();
        NSString *name = @"有参数";
        middleBlock(name);
        // 执行后记录下当前的时间
        CFTimeInterval endTime = CACurrentMediaTime();
        return endTime - startTime;
    }
       
    // 调用
    [self testTimeConsume:^(NSString *name) {
       // 放入 block 中的代码,可以使用参数 name
       // 参数 name 是实现代码中传入的,在调用时只能使用,不能传值    
       
    }];
    
  4. 用于回调

Block 实现

搞清楚Block实现原理,只要搞清楚Block的函数调用、和变量捕获的逻辑,我们可以通过clang将oc文件转译成cpp文件来查看block的c语言实现:

新建文件 main.m:

int main()
{
    int age=10;
    void (^Block)(void) = ^{
        printf("age:%d",age);
    };
    age = 20;
    Block();
}

使用命令:clang -rewrite-objc main.m 生成main.cpp,在main函数里面可以看到:

int main()
{
    int age=10;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
    age = 20;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

进一步可以查看__main_block_func_0等结构体的定义:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
        printf("age:%d",age);
    }
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

从上面可以看到block赋值,其实是用 main_block_func_0(函数指针)、 main_block_desc_0_DATA(描述说明)、age(外部变量)生成一个__main_block_impl_0结构的实例blk,这个blk即是我们在oc文件里面定义的block的对象blk。

block的调用,实际上则是调用blk的FuncPtr函数,其入参为blk本身。

以上为一个最简单的block的实现源码解析,基于这个简单的block可以总结如下:

  1. block调用实际上是函数指针的调用
  2. block的普通传值,实际上作为成员变量存储在block里面

__block

先来几个关于block捕获变量知识点:

Q:为什么block对auto和static变量捕获有差异?

auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可

Q:block对全局变量的捕获方式是?

block不需要对全局变量捕获,都是直接采用取值的

Q:为什么局部变量需要捕获?

考虑作用域的问题,需要跨函数访问,就需要捕获

Q:block的变量捕获(capture)

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

Q:block里访问self是否会捕获?

会,self是当调用block函数的参数,参数是局部变量,self指向调用者

Q:block里访问成员变量是否会捕获?

会,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量

下面依然通过源码转换方式,探究下__block的实现原理,使用如下源码:

int main()
{
    __block int age=10;
    void (^blk)(void) = ^{
        printf("age:%d",age);
    };
    age = 20;
    blk();
}

转换后可以看到对应几个代码块:

int main()
{
    __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
    (age.__forwarding->age) = 20;
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

可以看到,block的调用依然没有变化,而变量age则从之前的简单传值,变成了一个__Block_byref_age_0变量传入(传址),block的初始化也有了变化,先展开 Block_byref_age_0:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

这里可以看到,Block_byref_age_0实际同时保存了age的值(int age)和地址(__forwarding,也就是&age),而对age的赋值,则变成

(age.__forwarding->age) = 20;

所以该赋值同时能改变block捕获的age的值了。

以上。