Block的内存管理

iOS开发Block内存示例

Posted by Jannon on August 1, 2018

Block内存管理

Block根据在内存的位置可以分为全局的Block,栈上的Block,还有堆上的Block:

  • __NSConcretStackBlock
  • __NSConcretGlobalBlock
  • __NSConcretMallocBlock

ARC一般只有全局的Block和堆上的Block,栈上的Block在ARC下一般都会默认拷贝到堆上。 以下情况除外:

  • Block作为方法或函数的参数传入时,Block还是在栈上的;
- (void)viewDidLoad {
    [super viewDidLoad];

    __block NSString *str = @"I am Jan!";

    NSLog(@"Before the block string value is '%@' and address is %p",str,str);
    [self testWithBlock:^{
        str = @"My name is Jack!";
    }];
    NSLog(@"After the block string value is '%@' and address is %p",str,str);
}

- (void)testWithBlock:(void(^)())bl{
    bl();
    NSLog(@"Block invoked %@",bl);

}

输出结果如下:

Test[43208:1418384] Before the block string value is 'I am Jan!' and address is 0x100dd80e0
Test[43208:1418384] Block invoked <__NSStackBlock__: 0x7fff5ee27a68>
Test[43208:1418384] After the block string value is 'My name is Jack!' and address is 0x100dd8120

如果将block当作参数传入容器类的初始化函数中,这个block也会拷贝到了堆上,即该block的是__NSMallocBlock

NSMutableArray *arr = [[NSMutableArray alloc]initWithObjects:^{str = @"My name is Jack!";}, nil];
NSLog(@"blk is %@",[arr objectAtIndex:0]);

输出结果如下:

Test[43476:1427221] blk is <__NSMallocBlock__: 0x608000047170>

Block循环引用

1.内存
程序中栈内存是由系统负责管理申请和释放,堆内存是由程序负责申请和释放。不同语言内存管理是不一样的,objective-c内存管理是用引用计数来管理对象内存的。使用alloc/new/copy/mutableCopy方法返回的对象引用计数都会加1,在MRC下需要调用release方法释放这些方法返回的对象,在ARC下由ARC负责管理释放的。

NSMutableArray *array = [[NSMutableArray alloc]initWithObjects:@"a", nil];
    NSMutableArray *arr1 = array;
    NSMutableArray *arr2 = array;
    NSLog(@"%p,%p,%p",&array,&arr1,&arr2);
    NSLog(@"%p,%p,%p",array,arr1,arr2);
    NSLog(@"%@,%@,%@",array,arr1,arr2);

该段代码输出如下:

Test[17150:2347822] 0x7fff525dfac8,0x7fff525dfac0,0x7fff525dfab8
Test[17150:2347822] 0x608000057c70,0x608000057c70,0x608000057c70
Test[17150:2347822] (
    a
),(
    a
),(
    a
)

可见,array,arr1和arr2都是在栈上分配的内存,栈上的内存地址是由高地址向低地址,所以这三个变量的地址0x7fff525dfac8,0x7fff525dfac0,0x7fff525dfab8都是由高向低扩展。由于array指针指向了alloc方法申请的对象,然后赋值给了arr1和arr2,所以他们三个变量指向的地址都是0x608000057c70,在ARC下,该对象由ARC负责管理释放。

2.block循环引用
使用block一般都会遇到循环引用问题导致内存泄漏。如果block声明为对象obj的copy或者strong类型property变量,该block里面又使用到了obj对象,那就会造成循环引用内存泄漏问题。因为设置obj的block属性的时候,由于block里面使用到了obj对象,block会捕捉obj对象,并对obj进行retain操作,而block又是obj的copy或者strong(ARC下都一样)属性,即obj拥有block,block拥有obj导致循环引用,无法释放。
如:

typedef void (^proBlock)(void);

@interface SecViewController : UIViewController

@property (copy,nonatomic) proBlock block;

- (void)secTestWithBlock:(void(^)())blk;
@end

在SecViewController的viewdidload方法中执行该block:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.block();
  }

当前界面中通过按钮跳转到第二个界面,代码如下:

UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    secVC.block = ^{
        [secVC secTestWithBlock:^{
            NSLog(@"Print within block");
        }];

    };
    [self presentViewController:secVC animated:YES completion:nil];

该代码就会造成block循环引用问题。编译器会给出⚠️capturing ‘secVC’ strongly in this block is likely to lead to a retain cycle.运行并从SecViewController返回当前界面的时候,控制台输出如下:

Test[17486:2361465] Print within block

可见SecViewController的

- (void)dealloc {
    NSLog(@"Dealloc SecVC");
}

方法并没有执行,这是因为循环引用问题导致了内存无法释放。但是可以通过__weak声明变量来打破循环,代码修改如下:

UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    __weak __typeof(secVC) weakSecVC = secVC;
    secVC.block = ^{
        [weakSecVC secTestWithBlock:^{
            NSLog(@"Print within block");
        }];
    };
    [self presentViewController:secVC animated:YES completion:nil];

控制台输出结果如下:

Test[17558:2364346] Print within block
Test[17558:2364346] Dealloc SecVC

由于block的执行时机并不确定,因为block捕获的变量是weak修饰的,当block执行的时候,有可能weakSecVC已经被释放了,所以会导致向nil对象发送消息,即block执行的时候[weakSecVC secTestWithBlock:]这个方法中weakSecVC已经被外面释放掉了。
如代码修改如下:

UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
 SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
 __weak __typeof(secVC) weakSecVC = secVC;
 secVC.block = ^{
     double delayInSeconds = 2.0;
     dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
     dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
         [weakSecVC secTestWithBlock:^{
             NSLog(@"Print within block");
         }];
     });
 };
 [self presentViewController:secVC animated:YES completion:nil];

控制台输出结果如下:

Test[17723:2370401] Dealloc SecVC

将block中的[weakSecVC secTestWithBlock:]这个方法延迟2秒执行,并立刻返回第一个页面,这个时候会发现dealloc方法在返回的时候就会执行了,而[weakSecVC secTestWithBlock:]这个方法并不会被执行,因为weakSecVC已经被释放掉了。
接下来将代码修改如下:

UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    __weak __typeof(secVC) weakSecVC = secVC;
    secVC.block = ^{
        double delayInSeconds = 2.0;
        SecViewController *strongSecVC = weakSecVC;
        dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
        dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
            [strongSecVC secTestWithBlock:^{
                NSLog(@"Print within block");
            }];
        });
    };
    [self presentViewController:secVC animated:YES completion:nil];

控制台输出结果如下:

Test[17654:2368051] Print within block
Test[17654:2368051] Dealloc SecVC

block代码中将捕获的weak变量weakSecVC进行了强引用,这个时候SecViewController在返回的时候并不会被释放,只有等block执行完成返回的时候才进行释放。注意的是block在声明赋值的时候(拷贝到堆上),不管block执行还是没执行,block就会对block使用的变量进行了捕获,如果变量是强引用的,block也就强引用了该变量,如果该变量也强引用了block,那就会导致循环引用问题。但是如果在block里面声明了一个变量强引用捕获的weak变量,那么该强引用只有在block执行的时候才会发生,并且在block执行完成返回的时候会进行释放,即该强引用只会在block执行期间生效,block执行完成就无效了,所以并不会导致循环引用。
在实际项目中由于项目的复杂性,在block中这句代码SecViewController *strongSecVC = weakSecVC;中,strongSecVC也有可能是nil的,所以代码应该优化成以下代码会比较好:

UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
    __weak __typeof(secVC) weakSecVC = secVC;
    secVC.block = ^{
        SecViewController *strongSecVC = weakSecVC;
        if (strongSecVC) {
          double delayInSeconds = 2.0;
          dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
          dispatch_after(delayTime, dispatch_get_main_queue(), ^(void){
              [strongSecVC secTestWithBlock:^{
                  NSLog(@"Print within block");
              }];
          });
        } else {
          //block capture object is nil;
        }

    };
    [self presentViewController:secVC animated:YES completion:nil];

其他情况说明:

  • 在Block的外面将weak变量赋值给strong变量,block里面使用strong变量一样会导致循环引用问题,如:
    UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
     SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
     __weak __typeof(secVC) weakSecVC = secVC;
     SecViewController *strSecVC = weakSecVC;
     secVC.block = ^{
         [strSecVC secTestWithBlock:^{
             NSLog(@"Print within block");
         }];
     };
     [self presentViewController:secVC animated:YES completion:nil];
    

    因为经过以上赋值之后,strSecVC,weakSecVC和secVC变量都是指向了同一个在堆上申请的SecViewController对象地址,而strSecVC对该申请返回对象进行了强引用,所以导致循环引用。

  • block里面通过赋值nil解决循环引用
    如果将之前代码改成这样:
    UIStoryboard *storyBoard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
      SecViewController *secVC = [storyBoard instantiateViewControllerWithIdentifier:@"SecondViewController"];
      __weak __typeof(secVC) weakSecVC = secVC;
      __block SecViewController *strSecVC = weakSecVC;
      secVC.block = ^{
          [strSecVC secTestWithBlock:^{
              NSLog(@"Print within block");
          }];
          strSecVC = nil;
      };
      [self presentViewController:secVC animated:YES completion:nil];
    

    输出结果如下:

    Test[18929:2406918] Print within block
    Test[18929:2406918] Dealloc SecVC
    

    可见并没有发生循环引用,因为在block执行的时候,将strSecVC设置了nil,但是如果将block执行的代码注释掉,block不执行,就会发生循环引用问题,因为‘strSecVC = nil;’得不到执行,像之前说的那样不管block执行还是没执行,block就会对block使用的变量进行了捕获,如果变量是强引用的,block也就强引用了该变量,如果该变量也强引用了block,那就会导致循环引用问题。