iOS 底层探究:Block

692 阅读7分钟

这是我参与8月更文挑战的第24天,活动详情查看:8月更文挑战

我们实际开发中,Block的使用率相当之高,我们在使用Block的时候,会遇到各种各样的问题,比如经典的循环应用,那么这些问题到底是怎么产生的,我们又如何去解决,这就需要我们对Block有深入的了解,才能更好的解决这些问题,今天我们就来深入分析一下Block。

1. Block的类型

1.1 种类

  • 全局Block,NSGlobalBlock,位于全局区,在Block内部不使用外部变量,或者只使用静态变量和全局变量。
  • 堆Block,NSMallocBlock,位于堆区,在Block内使用变量或者OC属性,并赋值给强引用或者Copy修饰的变量。
  • 栈Block,NSStackBlock,位于栈区,与堆Block一样,在Block内使用变量或者OC属性,但不能赋值给强引用或者Copy修饰的变量。

1.2 示例1

全局Block比较好区分,堆Block(MallocBlock)和栈Block(StackBlock)不易区分,下面我们来看这样一段代码

- (void)blockRetaincount{
    NSObject *objc = [NSObject new];
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    void(^strongBlock)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    strongBlock();
    
    void(^__weak weakBlock)(void) = ^{ // + 1
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    weakBlock();
    
    void(^mallocBlock)(void) = [weakBlock copy];
    mallocBlock();
}

请问这里的打印时多少? 我们运行一下,看下效果,如下图 image.png

为什么结果是这样呢?下面我们具体分析下

  • NSObject *objc = [Nsobject new];这里之后obj的引用计数为1
  • void(^strongBlock)(void) = ^{
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };这里为什么是3呢?因为strongBlock外部捕获了objc,进行了copy操作,引用计数+1,strongBlock是一个堆Block(mallocBlock),会进行强持有,从栈上拷贝到堆上,这个时候堆上的对象引用计数再次+1,就变成了3。
  • void(^__weak weakBlock)(void) = ^{ // + 1
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    }; 这是一个栈Block,因为weakBlock外部捕获了objc,进行了copy操作,引用计数+1,但是他是一个栈Block,不会从栈上拷贝到堆上,所以这里打印4。
  • void(^mallocBlock)(void) = [weakBlock copy];这里是对weakBlock进行一次copy操作,从栈上拷贝到堆上,这个时候堆上的对象引用计数+1,所以这里打印5。

1.3 示例2

我们再看一段代码,如下所示

- (void)blockCopy{
    int a = 0;
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    struct _RoBlock *blc = (__bridge struct _RoBlock *)weakBlock;
    id __strong strongBlock = weakBlock;
    blc->invoke = nil;
    void(^strongBlock1)(void) = strongBlock;
    strongBlock1();
}

这段代码运行结果如何呢?我们运行后,效果如下图

image.png 这里崩溃闪退了,我们分析下 blc -> invoke这里的invoke进行调用执行。 struct _RoBlock *blc = (__bridge struct _RoBlock *)weakBlock;这里把weakBlock进行强转。 id __strong strongBlock = [weakBlock copy];这里把weakBlock进行copy操作。 blc->invoke = nil;把blc的invoke置空。 void(^strongBlock1)(void) = strongBlock;这段代码只是强转一次,把strongBlock具有block的特性,可以调用执行。 说明weakBlock强转成blc时,他们还是指向同一块内存区域,blc的invoke为nil,那么weakBlock的invoke也是nil,所以这里调用的时候闪退。

如何解决呢?我们可以进行copy操作,id __strong strongBlock = weakBlock;改为 id __strong strongBlock = [weakBlock copy];我们再试下,如图

image.png 这样就正常了,一定要在blc->invoke = nil;之前进行copy操作。

1.4 示例3

下面再分析一段这样的代码,如下所示

- (void)blockDemo{
//    NSObject *a = [NSObject alloc];
    int a = 0;
    void(^__weak weakBlock)(void) = nil;
    {
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"strongBlock ---%d", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1");
    }
    weakBlock();
}

我们来看下执行效果:

image.png

我们分析下它的执行流程:

  • void(^__weak weakBlock)(void) = nil;这是一个栈Block
  • void(^__weak weakBlock)(void) = nil;
    {
    void(^__weak strongBlock)(void) = ^{
    NSLog(@"strongBlock ---%d", a);
    };
    weakBlock = strongBlock;
    NSLog(@"1");
    }
    weakBlock();
    }这里一个代码块,跟void(^__weak weakBlock)(void) = nil;无关
  • 在代码块中strongBlock是一个块Block
  • 在strongBlock给了weakBlock,weakBlock也是一个栈Block
  • 栈Block的生命周期是在blockDemo这个方法中,也就是说只有出了这个方法,这个栈Block的生命周期才会结束,所以这里运行是正确的。 我们改下代码,如下
- (void)blockDemo{
//    NSObject *a = [NSObject alloc];
    int a = 0;
    void(^__weak weakBlock)(void) = nil;
    {
        void(^ strongBlock)(void) = ^{
            NSLog(@"strongBlock ---%d", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1");
    }
    weakBlock();
}

运行后

image.png 这里闪退了。 原因分析:Block默认是强引用的,这里是在堆区,它的生命周期是在代码块中,出了代码块,它就会被销毁,weakBlock = strongBlock;赋值后,weakBlock也是在堆区,所以这里调用weakBlock()就会闪退。 我们把weakBlock = strongBlovk;改成weakBlock = [strongBlock copy];同样会闪退,原因同上,因为也在堆区,出了代码块,销毁了。

2. Block循环引用的分析

2.1 为何会出现循环引用

首先我们看一下下面这张图

image.png 我们看到,A对于进程B持有,A会对B进行引用计数+1的操作,当B要想释放掉时,需要等待A发送release信号,如果B的引用计数为0的时候,dealloc就会被调用,只有当A在dealloc的时候会发送信号给B。

image.png 这张图描述了,A持有B,B也同时持有了A,这个时候就构成了无法释放的循环,因为B的释放依赖A的释放,A的释放又依赖B的释放,产生了循环引用。

2.2 示例

示例1

- (void)test1 {
    self.name = @"robert";
    self.block = ^(void) {
        NSLog(@"%@", self.name);
    };
    self.block();
}

- (void)dealloc{
    NSLog(@"dealloc走你");
}

运行以上代码,发现dealloc并没有调用,说明这里产生了循环引用。 再看下面这段代码 示例2

[UIView animateWithDuration:1 animations:^{
        NSLog(@"%@", self.name);
    }];

运行效果如图 image.png 这里可以看出调用了dealloc,所以没有产生循环引用,这是为什么?我们来分析下。 示例1中,self->block->self产生循环引用 __weak typedef(self) weakSelf = self;使用weak指针来解决 我们再改一下代码,如下:

- (void)test1 {
    __weak typeof(self)weakSelf = self;
  self.name = @"robert";
  self.block = ^(void) {
      dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)3*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
          NSLog(@"name=%@", weakSelf.name);
      });
  };
  self.block();
}

看一下运行结果 image.png name的值打印为空 分析:当我们返回上个页面时,self被释放掉了,weakSelf也被释放掉了,这个时候再调用就是nil。 我们可以通过__strong __typeof(weakSelf)strongSelf = self;来解决,代码如下

- (void)test1 {
    __weak typeof(self)weakSelf = self;
    self.name = @"robert";
    self.block = ^(void) {
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)3*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"name=%@", strongSelf.name);
        });
    };
    self.block();
}

结果如下

image.png 这里运行就正常了。 也就是通过weak-strong-dance的方法解决 strongSelf虽然是强引用,是个临时变量,它的生命周期是在block的范围内,会自动释放。 我们再来讲第二种方法,代码如下

 __block SecondViewController *vc = self;
    self.name = @"robert";
    self.block = ^(void) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)3*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            NSLog(@"name=%@", vc.name);
            vc = nil;
        });
    };
    self.block();
}

运行结果如下

image.png vc被self.block持有了,但是我们把vc置空,断开了循环。 self->blovk->vc->self,vc=nil断开了循环链。 对外部变量操作加上__block的才能修改。 当然我们还可以通过传参数的方式来解决。 我们再来看一段代码。

static SecondViewController *staticSelf_;
- (void)blockWeakStatic {
    __weak typeof(self) weakSelf = self;
    staticSelf_ = weakSelf;
}
- (void)dealloc{
    NSLog(@"dealloc走你");
}

分析结果:这里肯定会产生循环引用,weakSelf->self,staticSelf->weakSelf,staticSelf是个全局静态变量,不会自动释放,所以这里产生了循环引用。 这里的weakSelf其实就是Self,断点打印一下,如下图

image.png 这里可以看出,self、weakSlef、staticSelf是指向同一块内存区域,也就是说weakSelf与self是映射关系,他们三个又构成了循环链,所以就产生了循环引用。 我们接下来再看以下代码

- (void)block_weak_strong {
    __weak typeof(self) weakSelf = self;
    self.doWork = ^{
        __strong typeof(self) strongSelf = weakSelf;
        weakSelf.doStudent = ^{
            NSLog(@"111111111----- %@", strongSelf);
        };
       weakSelf.doStudent();
    };
   self.doWork();
}

请问这段代码会产生循环引用吗?答案是肯定的,我们先运行一下,看一下结果

image.png 我们分析下:

  • self.doWork对self持有
  • __strong typeof(self) strongSelf = weakSelf;
    weakSelf.doStudent = ^{
    NSLog(@"111111111----- %@", strongSelf);
    };
    weakSelf.doStudent();这里strongSelf对weakSelf持有,strongSelf虽然是临时变量,但是它的生命周期是在doWork这个block中,在strongSelf释放前,会执行上述代码,doStudent是会对strongSelf进行持有,捕获进去,对strongSelf引用计数+1操作,也就是说strongSelf的引用计数为2了,虽然在doWork这个block结束时会-1操作,但是还是无法释放这个strongSelf
  • 我们可以在NSLog(@"111111111----- %@", strongSelf);代码下面加入strongSelf=nil,丢按开循环来呢来解决。