block深究

一直使用block,总感觉用的云里雾里的,索性进行一次深入的研究,各位看官如果发现文章有什么理解不到位的情况,可以指出来,我们一起研究,如有转载,请注明出处。

block的实质

一.block是什么

  • block是带有自动变量(局部变量)值的匿名函数。

顾名思义,所谓匿名函数就是不带有名称的函数。那么带有自动变量是什么意思呢,是怎么来的呢,我的理解就是,截获来的,那么也就是说,block是能够截获自动变量的匿名函数。如何截获的,下面会进行说明。

1.先从一个最简单的不含有变量的block开始看:
int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        void (^simpleBlock)() = ^{
            NSLog(@"Hello,World!");
        };
        simpleBlock();
    }

    return 0;
}

使用clang进行反编译(clang -rewrite-objc XXX.m),可以得到C++的实现代码,在这里面,我们可以看到block内部的实现

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_77zrry7j3b570h0tq36z405c0000gn_T_BlockTest_985af6_mi_0);
}

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; 
        void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
    }

    return 0;
}

我们可以看到:

void (^simpleBlock)() = ^{
            NSLog(@"Hello,World!");
};

被转化成了:

void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

以上代码我们看到我们写的simpleBlock被转化成了指向__main_block_impl_0结构体的指针。下面我们来看看这个结构体:

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

我们可以看到这个结构体包含 __block_impl 类型的 impl,和 __main_block_desc_0 类型的Desc两个变量,且这两个类型都是结构体,以及一个构造方法__main_block_impl_0()。

我个人理解__block_impl 像是 block的一个基类,__main_block_desc_0就是block的描述。

下面就分析一下包含的这两个结构体是干嘛的。

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
  • isa 指向所属类的指针 也就是保存了block的类型
  • Flags 标识
  • Reserved为保留字段
  • FuncPtr指针 block执行时调用的函数指针 也就是block内的函数的实现

可以看出,它包含了isa指针(包含isa指针的皆为对象),也就是说block也是一个对象(runtime里面,对象和类都是用结构体表示)。

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)};
  • reserved为保留字段默认为0
  • Block_size为sizeof(struct __main_block_impl_0),用来表示block所占内存大小。因为没有持有变量,block大小为impl的大小加上Desc指针大小
  • __main_block_desc_0_DATA为main_block_desc_0的一个结构体实例,
    这个结构体,用来描述block的大小等信息。如果持有可修改的捕获变量时(即加block),会增加两个函数(copy和dispose)

下面我们再来看一下构造函数

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

构造函数里面初始化了imp和Desc,我们看到impl.isa为&_NSConcreteStackBlock 说明我们的simpleBlock是一个栈类型的block。标识Flags初始化默认为0。再来看看这个构造函数的调用:

void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

impl.FuncPtr赋值为__main_block_func_0,Desc赋值为&__main_block_desc_0_DATA

以上我们可以看出我们写的simpleBlock是一个对象,这个对象是结构体类型的,我们会给这个结构体里面的实例进行赋值,如描述,类型,标识,方法等。

2.含有变量的block又是什么样子呢?

我们写如下代码:

int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        int a = 10;
        void (^simpleBlock)() = ^{
            NSLog(@"a is %d",a);
        };
        a = 20;
        simpleBlock();
    }

    return 0;
}

我们运行后,会发现打印结果为:

a is 10

为什么后面我们改了a = 20没有起作用呢?

同样的,使用clang命令转化下上述代码

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_77zrry7j3b570h0tq36z405c0000gn_T_BlockTest_287a6e_mi_0,a);
 }

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 a = 10;
        void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
    }

    return 0;
}

我们说这个block持有了变量,那么这个变量在哪里呢?看一下代码:

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

我们会发现__main_block_impl_0结构体里面多了一个 int a; 也就是这个block持有了一个变量a,那么这个a和外面我们在main方法里面写的那个int a = 10的a有什么关系呢?我们继续看一下__main_block_impl_0的构造函数:

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

我们看到这个构造函数后面跟了一个: 其实这个C++的语法,构造函数后面加:相当于赋值,

冒号后面跟的是赋值,这种写法是C++的特性。

A( int aa, int bb ):a(aa),b(bb){
}
相当于

A( int aa, int bb ){
a=aa;
b=bb;
}

也就是上面的代码等价于:

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

我们再来看看__main_block_impl_0的引用:

int a = 10;
void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

我们可以看到参数传入值为10,仅仅是一个值传递,里面a和外面的a没有什么关系,所以后续更改a = 20,并不能影响block内部的变量值。所以外面改变,依然打印:

a is 10

总得来说,所谓"截获自动变量值"意味着在执行block语法的时候,block语法表达式所使用的自动变量值被保存到block的结构体实例(也就是block自身)中。

我们都知道,当我们在block方法里面,改变a的值的时候,编译器会报错,也就是写以下代码的时候,

void (^simpleBlock)() = ^{

 a = 30;
 NSLog(@"a is %d",a);

 }; 

编译器会报错:

为什么这里会报错呢,为什么这个时候不能给a进行赋值呢?

因为main函数中的局部变量a和函数__main_block_func_0不在同一个作用域中,调用过程中只是进行了值传递。在不同的作用域的时候,改变值的方法就是通过指针传递,所以,在上面代码中,我们可以通过指针来实现局部变量的修改。如下:

int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        int *pointer1;
        int a = 10;
        pointer1=&a;         //把变量a的地址付给pointer1
        void (^simpleBlock)() = ^{
            *pointer1 = 30;
        };
        simpleBlock();
        NSLog(@"a is %d",a);

    }

    return 0;
}

这样就会打印:

a is 30

这样就可以改了,那么我们是不是每个都可以通过指针来改变局部变量呢?显然不是的,这个例子是由于在调用__main_block_func_0时,main函数栈还没展开完成,变量a还在栈中。但是在很多情况下,block是作为参数传递以供后续回调执行的。通常在这些情况下,block被执行时,定义时所在的函数栈已经被展开,局部变量已经不在栈中了(block此时在哪里?),再用指针访问就……所以,对于auto类型的局部变量,不允许block进行修改是合理的。

3.如果我对a加__block修饰,为什么就行改变打印值了呢?

__block修饰符是什么呢? __block修饰符类似于static,auto,register等说明符,他们是用于指定讲变量值设置到哪个存储域中,例如auto表示作为自动变量存储在栈种,static表示作为静态变量存储在数据区中。那么__block是存储到哪里呢?

我们写以下代码:

int main(int argc, const char * argv[])
{
    @autoreleasepool
    {
        __block int a = 10;
        void (^simpleBlock)() = ^{
            NSLog(@"a is %d",a);
        };
        a = 20;
        simpleBlock();
    }

    return 0;
}

我们查看打印结果,如下

a is 20

我们发现这次我们修改a = 20后,竟然响应了block里面的打印内容,也就是说__block的修饰起了某些作用,到底起了什么作用呢,我们使用clang进行反编译后,得到以下代码:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
   NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_77zrry7j3b570h0tq36z405c0000gn_T_BlockTest_43d9d1_mi_0,(a->__forwarding->a));
 }

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

        void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        (a.__forwarding->a) = 20;
        ((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
    }

    return 0;
}

看到以上代码,我们发现加了__block修饰后,产生了一些之前没有的代码,如新增了一个结构体__Block_byref_a_0 代码如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
  • __isa, 指向变量Class
  • __forwarding,指向自己的指针,当从栈copy到堆时,指向堆上的block
  • __flags,当block被copy时,标识被捕获的对象,该执行的操作
  • __size,结构体大小
  • i,持有的变量

注意到这个结构体中包含了该实例本身的引用 __forwarding。

还增加了两个实例:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

这两个实例是干嘛的,copy和dispose分别有什么作用呢?从表面意思看是执行了copy操作,那么是把什么东西copy了呢,又是从哪里copy到哪里呢?



我们看一个代码例子:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self testBlockFunc];
}

- (void)testBlockFunc
{
    __block int a = 50;
    void (^simpleBlock)() = ^{
            a = 70;
        };
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        simpleBlock();
        NSLog(@"a is %d",a);
    });
}

我们写一个controller,在viewDidLoad里面定义了一个int a = 50;和一个simpleBlock,我们都知道这个a和 simpleBlock都存储在栈上,我们进入这个controller后,5秒之后执行一个simpleBlock,修改这个变量a的值为70.但是我们在不到5s的时间内就离开这个界面,对应的就是存储在栈上的这个变量a和simpleBlock已经被销毁了,到了5s的时候,这个simpleBlock执行,我们看一下结果:

a is 70

发现虽然已经离开了这个界面,栈上的变量a和simpleBlock已经被销毁了,但是simpleBlock依然执行且依然改了a的值,为什么呢。原来,Blocks提供了将block和__block变量从栈上复制到堆上的功能,将配置在栈上的block复制到堆上,这样即使block语法记述的变量作用域结束,堆上的block还可以继续存在。当block被copy到堆中时,__main_block_impl_0的拷贝辅助函数__main_block_copy_0会将__Block_byref_a_0拷贝至堆中,所以即使局部变量所在栈被销毁,block依然能对堆中的局部变量进行操作



我们来继续之前的话题,来看看是如何copy的。我们看看__main_block_copy_0和__main_block_dispose_0这两个实例:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

我们看到__main_block_copy_0里面用到了_Block_object_assign函数

_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

我们再想_Block_object_assign这个函数到底是如何工作的呢,

我们先看一下main函数里面加入__block后,a的变化:

 __block int a = 10

变成了:

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

上述代码看出加入__block的修饰后,竟然把简单变量int a转变成了一个对象,这个对象是一个结构体__Block_byref_a_0类型的。
我们继续查看_Block_object_assign的源代码:

void _Block_object_assign(void *destAddr, const void *object, const int flags) {
    ...
    else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {
        // copying a __block reference from the stack Block to the heap
        // flags will indicate if it holds a __weak reference and needs a special isa
        _Block_byref_assign_copy(destAddr, object, flags);
    }
    ...
}
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;

    ...
    else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        //printf("making copy\n");
    // src points to stack
        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
        // if its weak ask for an object (only matters under GC)
        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
        copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
        copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            copy->byref_keep = src->byref_keep;
            copy->byref_destroy = src->byref_destroy;
            (*src->byref_keep)(copy, src);
        }
        ...
    }
    ...
}

这里出现了Block_byref的类型,我理解的Block_byref就是__Block_byref_a_0的基类。
看到有这样一段代码:

 copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
 src->forwarding = copy;  // patch stack to point to heap copy

堆里的Block_byref变量copy的成员变量forwarding指向了它自己,栈里的Block_byref变量src的成员变量forwarding指向了堆里的copy,这就保证了操作的值始终是堆中的拷贝,而不是栈中的值。

我们看一下_Block_object_dispose

_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

当堆上的block被废弃时,会使用_Block_object_dispose函数释放该变量(相当于release)。

综上可以得出结论:

当block被copy到堆中时,__main_block_impl_0的拷贝辅助函数 __main_block_copy_0会将__Block_byref_a_0拷贝至堆中,所以即使局部变量所在堆被销毁,block依然能对堆中的局部变量进行操作。其中__Block_byref_a_0成员指针__forwarding用来指向它在堆中的拷贝,

simpleBlock由:

 void (^simpleBlock)() = ^{
            NSLog(@"a is %d",a);
        }
 a = 20;

变成了:

 void (*simpleBlock)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
 (a.__forwarding->a) = 20;

之前说__forwarding指向了堆里的变量,也就是堆里的变量a赋值为20,所以block打印出来的a就是修改后的值。


从以上例子看出,block是一个匿名函数,本质上block也是一个OC对象,结构体类型的对象,它可以截获自动变量,如果自动变量加了__block修饰符,可以将自动变量从栈拷贝到堆里面,从而保证了超出作用域的时候,block的执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值