iOS中Block底层窥探

对于一名有着几年经验的iOS开发来说block的使用一定不会陌生,对于协议代理来说这个使用起来更加方便快捷,比如我们常见的AFNetworking、Masonry等几个大框架都有使用block,但在实际开发中我们又会遇到各种问题,block造成的循环引用继而导致内存泄露,是什么原因造成这些问题?通过下面对Block底层的窥探我们就可以知道了。

一、常用的几种block

1.无参无返回值
void (^block)(void) = ^{
          NSLog(@"无参无返回值");
};
2.有参无返回值
void (^block)(int a) = ^(int a){
          NSLog(@"有参无返回值");
};
3.无参有返回值
int (^block)(void) =  ^ int(void){
          NSLog(@"有参无返回值");
          return 1;
     };
4.有参有返回值
int (^block)(int a) = ^ int (int a){
          NSLog(@"有参有返回值");
          return a;
};
5.最常用的

使用typedef 来定义一个block,例:typedef void (^block)(void);
以上就是我们最常用的几种block,上面写的都是最简单的,相信大家都见过并且都能熟练使用,下面将逐步深入

二、实际创建block的类型

如果我们有打印我们创建的block就会发现block对象的isa指针指向的类型有三种
全局块:NSConcreteGlobalBlock,相当于单例,在全局内存中。
栈块:NSConcreteStackBlock,在栈中,超出作用域就会被马上销毁
堆块:NSConcreteMallocBlock,在堆中,是带引用计数的对象,需要自己进行管理

1. NSConcreteGlobalBlock
void (^block)(int a) = ^(int a){
          NSLog(@"我是NSGloabBlock--%d",a);
     };
NSLog(@"%@",block);//__isa:__NSGlobalBlock__
打印出来的isa指针指向的类对象是__NSGlobalBlock__类型
可以看到内存地址是<__NSGlobalBlock__: 0x103fd9140>  是0x1开头
2. NSConcreteStackBlock
NSLog(@"%@",^(int a){
          NSLog(@"我是NSStackBlock--%d",b);
     });//__NSStackBlock__
内存地址是<__NSStackBlock__: 0x7ffeebc26918>是0x7开头
3. NSConcreteMallocBlock
     int b = 10;
     void (^block1)(int a) = ^(int a){
          NSLog(@"我是NSMallocBlock--%d",b);
     };
     NSLog(@"%@",block1);//__isa:__NSMallocBlock__
内存地址是<__NSMallocBlock__: 0x600001ae4e10>是0x6开头

一般来说0x1开头的地址是在全局内存区,0x6开头的在堆区,0x7开头的在栈区。

三、引用外部变量的block分析

1.没有使用__block修饰的外部变量
     int a = 10;
     void (^block)(void) = ^{
          a++;
          NSLog(@"block内---%d",a);
     };
     block();
     NSLog(@"block外---%d",a);

如这个例子很明显这是一个堆block,我们在block外面定义了一个int类型的变量a,在block里面对a进行'++'运算,然后在外面打印a,此时a的值会是多少呢?运行这段代码发现block内a的值是11,block外的值仍旧是10,说明block内部对变量a 进行操作没有影响外部a的值,这是什么原因呢?这就需要查看一下这段代码的底层代码是怎么实现的。

//block结构体的定义   (其实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;//指向对应的block类对象
    impl.Flags = flags;
    impl.FuncPtr = fp;//存储block包函数  当调用block其实是在调用这个函数
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy   值传递  所以不会出现 代码块中变量值改变 导致外面使用值也发生改变
          a++;
}

//这是上面那段oc代码的底层实现
      int a = 10;
     void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
//     简化下面的代码
/*
    int a = 10;
   void (*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);
//     函数调用
    block->FuncPtr(block);
*/
未使用__block修饰.png

由于block的动态捕获变量的特性,当使用外部变量时,block会自动在其内部创建一个与外部变量一样的变量,并将外部变量赋值给它,当我们调用block的时候内部对变量a进行'++'操作的时候,其实是对block内部复制的变量a进行操作,由于是值传递,所以内部变量的更改不会影响外部变量的修改,因此上面提到的问题是不是很容易就解答了。

2.使用__block修饰的外部变量
     __block int a = 10;
     void (^block)(void) = ^{
          a++;
          NSLog(@"block内---%d",a);
     };
     block();
     NSLog(@"block外---%d",a);

同样是上面的代码我们对外部变量a用__block修饰会出现什么结果?运行代码我们可以看到其内外的结果是一样的都是11,这是什么原因呢?接下来我们看看它的底层代码是怎么实现的

//使用__block修饰的变量在底层回转换成下面结构体类型
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;//存储block变量
 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) {  //将栈中的__block变量复制了一份放到了堆中,并且将栈中forwarding指针从__block变量转移到堆中的__block对象
    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; // 找到堆中的__block对象a
          a->__forwarding->a++;//找到forwarding 指向的__block对象中外部变量a的值并进行操作
     }
//上述OC代码的底层实现  栈中的__block类型变量   并且forwarding指针指向自己本身
     __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {
          (void*)0,
          (__Block_byref_a_0 *)&a,
          0,
          sizeof(__Block_byref_a_0),
          10
     };
     void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
//简化代码
//__block修饰的a
     /*
      __Block_byref_a_0 a = {
          0,
          &a,
          0,
          sizeof(__Block_byref_a_0),
          10
     };
     void (*block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);
     block->FuncPtr(block);
      */
__block修饰的变量.png

注:
1)在MRC下访问外部变量会默认存在栈中 (当使用是需要通过 copy方法手动将变量copy到堆中)
2)在ARC下访问外部变量默认存在堆中(实际是先存在栈中,ARC下是自动copy到堆中),自动释放

当时用__block修饰的时候会将变量转换成__Block_byref_a_0修饰的变量,同时在block结构体里面也会定义__Block_byref_a_0结构体类型的变量a,并且给a = _a->__forwarding,在内部操作a++的时候其实是操作a->_forwarding->a++,如上图在ARC环境下block在使用外部变量时会copy一份放到堆中,并且将栈中的forwarding指针指向堆中__block变量,这样做目的是无论在外面访问还是在内部访问,都是访问同一个__block变量,这样就能解释上面的问题block内部改变值外部也会跟着改变。

我们还可以打印__block修饰的变量a的地址,这样也能证明上图展示的
     __block int a = 10;
     // block具有外部变量捕获特性和copy 会把变量从栈区复制到堆区
     //通过__block修饰 的变量会进行指针传递
     //没有用__block修饰 的变量会进行值传递
     NSLog(@"使用前---%p",&a);//0x7ffee6ff0988
     void (^block)(void) = ^{
          NSLog(@"内部使用---%p",&a);//0x600003830218
     };
     NSLog(@"调用前---%p",&a);//0x600003830218
     block();

这是我执行的代码每个地方打印地址的结果,很明显,当使用__block修饰的时候会在栈中定义一个__Block_byref_a_0类型的变量地址是0x7ffee6ff0988,声明block的时候由于它的特性会将变量从栈区copy到堆区,在调用前打印a的地址是0x600003830218,很明显已经copy到堆区了,在block内部打印a的地址也是0x600003830218,也说明了他们访问的是同一个__block变量。

疑问:问什么基本数据类型、NSString等在block内部使用时需要用__block 修饰否则会报Variable is not assignable (missing __block type specifier)这个错误,但对于自己创建的对象却不需要?

类似下面这段代码

      int a = 10;// $4 = 0x00007ffee0e6e97c
     NSString * str = @"123";//(__NSCFConstantString *) $0 = 0x000000010ed91218 @"dasd"
     NSMutableString * mutableStr = [NSMutableString stringWithString:@"1"];//(__NSCFString *) $1 = 0x0000600002533420
     CYPerson * person = [CYPerson new];//(CYPerson *) $2 = 0x000060000297c930
     person.name = @"张三";
     void (^block)(void) = ^{
          a++;
          str = @"456";
          [mutableStr appendString:@"2"];
          person.name = @"李四";
     };
     block();

根据打印出来的地址可以看到基本数据类型定义的变量在栈中他们是值传递,NSString类型的变量放在全局静态区,可以看做是常量,再里面使用也是值传递跟基本类型一样因此需要用__block修饰,但对象是存在堆区,变量中存的是堆中的地址,copy的话也是copy的地址,相当于也是访问同一个存储空间,所以不需要用__block修饰。

3.总结

block具有外部变量捕获特性和copy 会把变量从栈区复制到堆区,通过__block修饰 的变量会进行指针传递,没有用__block修饰 的变量会进行值传递(对于会进行值传递的变量)。

四、block的循环引用

1.循环引用怎么造成的
     self.myBlock = ^(id) {
          [self doSomething];
     };

如上代码self->myBlock->forwarding->self,这样变成了一个循环,相互持有,无论谁要释放都依赖于另一个的释放,所以就造成了循环引用。

2.如何解决循环引用

2.1 使用__weak进行修饰(ARC)

__weak typeof(self) weakSelf = self;
     self.myBlock = ^(id a) {
          [weakSelf doSomething];
     };

使用__weak修饰会出现一个问题就是,当外面的self被释放掉后,block里面调用该对象就会是空的

场景:A控制器 push B控制器,然后又从B控制器pop 到A控制器,B控制器被释放掉了,查看代码块中打印结果
控制器B代码
__weak typeof(self) weakSelf = self;
     self.person = [CYPerson new];
     self.person.name = @"张三";
     self.myBlock = ^(id a) {
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.person.name);
        });
     };
     self.myBlock(@"");

当pop出去后B对象被释放掉了,最后打印结果是空的,通过__weak可以解决循环引用,通过结果是由于我们在block块内部没有对外部变量进行强引用导致取到的结果是空的,所以为了让这段代码更完美,我们需要在代码块内部对其再进行强引用即可,这么做和直接用self有什么区别,为什么不会有循环引用:外部的weakSelf是为了打破环,从而使得没有循环引用,而内部的strongSelf仅仅是个局部变量,存在栈中,会在block执行结束后回收,不会再造成循环引用。使用__strong进行修饰代码如下:

控制器B代码
__weak typeof(self) weakSelf = self;
     self.person = [CYPerson new];
     self.person.name = @"张三";
     self.myBlock = ^(id a) {
          __strong typeof(weakSelf) strongSelf = weakSelf;
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.person.name);
        });
     };
     self.myBlock(@"");

2.2__block修饰(MRC)

__block typeof(self) blockSelf = self;
     self.myBlock = ^(id a) {
          [blockSelf doSomething];
     };

2.3直接传入当前引用对象

self.myBlock = ^(id  vc) {
          [vc doSomething];
     };
     self.myBlock(self);

五、总结

相信通过上面的分析应该对block有了更深的认识了吧,block分几种类型、block的底层实现、循环引用怎么造成的、如何解决循环引用等等,以前我也只知道怎么使用block,怎么解决循环引用,但为什么这样做却一点也不知道,其实这些东西当我们了解了底层后在使用中就更能得心应手了,例如 [UIView animateWithDuration:1 animations:^{}];当我们在内部使用当前对象的时候,会不会造成循环引用呢? 通过上面的分析很容易就得到答案,答案是肯定不会了,当前对象没有持有UIView对象,仅仅是block中持有当前对象而已,不会造成循环引用,诸如此类还有很多,因此还是应该多了解一些底层原理,这样对于我们以后写代码还是很有帮助的,继续努力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。