之前的文章中介绍了关于block的定义和使用。这篇文章主要介绍跟block的内存管理的有关知识。重点介绍block在使用内部变量、外部变量和ARC、非ARC的情况下的使用,以及block引发的循环引用问题。

根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。(知道就行)

<1>NSGlobalBlock:类似函数,位于text段;

<2>NSStackBlock:位于栈内存,函数返回后Block将无效;

<3>NSMallocBlock:位于堆内存。

使用内部变量在ARC和非ARC条件下:

对于没有引用外部变量的Block,无论在ARC还是非ARC下,类型都是__NSGlobalBlock__,这种类型的block可以理解成一种全局的block,不需要考虑作用域问题。同时,对block进行Copy或者Retain操作也是无效的,

typedef int(^MyBlock)();
MyBlock func() {
    return ^{ return 123; };
}
// 在非ARC下执行如下代码:
- (void)test {
   MyBlock block = func();
   NSLog(@"%d", block());
   NSLog(@"%@", [block class]);
   MyBlock block2 = [block copy];
   //Copy操作对__NSGlobalBlock__类型无效
   NSLog(@"%d", block == block2);
}

输出:

123

__NSGlobalBlock__

1

可以看到,copy后的block和原来是同一个对象的。

使用外部变量在ARC条件下:

对于引用了外部变量的block,如果没有对他进行copy,它的作用域只会在声明它的函数栈内(类型是__NSStackBlock__),如果在ARC下直接返回此类block是正确的,因为在ARC下,系统会自动添加copy,如下:

typedef int(^MyBlock)();
MyBlock func() {
    int i = 123;
    return ^{ return i; };// 此处block使用了外部变量i
}
// 在非ARC下执行如下代码:
void test() {
    MyBlock block = func();
    NSLog(@"%d", block());
    NSLog(@"%@", [block class]);
}

输出:

123

__NSMallocBlock__

类型是__NSMallocBlock__,说明block已经被copy到了堆中了。

使用外部变量在非ARC条件下:

block内使用外部变量,在非ARC下编译会直接报错(Xcode提示Returningblock that lives on the local stack),因为此时block还在栈中,不能执行retain操作。

当然其实在非ARC下,也可以使上面的情况编译通过,如下

typedef int(^MyBlock)();
MyBlock func()
{
    int i = 123;
    //非ARC下不要这样!!!
    MyBlock ret = ^{ return i; };
    return ret;
}

我们把原来的返回值赋给一个变量,然后再返回这个变量,就可以编译通过了。不过虽然编译通过了,这个返回的block作用域仍是在函数栈中的,因此一旦函数运行完毕后再使用这个block很可能会引发BAD_ACCESS错误。

所以在非

ARC下,必须把block复制到堆中才可以在函数外使用,或使用外部变量,如下:

typedef int(^MyBlock)();
MyBlock func()
{
    //非ARC
    int i = 123;
    return [^{ return i; } copy];
}

block修改外部变量的值

block使用外部变量,并不对它修改的时候,一般来说,block 只是将变量值复制过来的。而当我们用__block标记的时候,被标记__block的变量事实上应该说是被移动到了堆上。表示在 block 中的修改对于 block 外也是有效地。(ARC和非ARC是一样的)

iOS 中 __block ios中block有几种_block

下面总结一下block被copy后,它所引用的变量被复制到了堆中的情况,如下代码(非ARC下):

//非ARC
void func()
{
    int a = 123;
    __block int b = 123;
    NSLog(@"%@", @"=== block copy前");
    NSLog(@"&a = %p, &b = %p", &a, &b);
    
    void(^block)() = ^{
        NSLog(@"%@", @"=== Block");
        NSLog(@"&a = %p, &b = %p", &a, &b);
        NSLog(@"a = %d, b = %d", a, b = 456);
    };
    block = [block copy];
    block();
    
    NSLog(@"%@", @"=== block copy后");
    NSLog(@"&a = %p, &b = %p", &a, &b);
    NSLog(@"a = %d, b = %d", a, b);
    
    [block release];
}

输出:

=== block copy前

&a = 0x7fff5fbff8bc, &b = 0x7fff5fbff8b0

=== Block

&a = 0x100201048, &b = 0x100201068

a = 123, b = 456

=== block copy后

&a = 0x7fff5fbff8bc, &b = 0x100201068

a = 123, b = 456

可以看到,在block执行中,他所引用的变量a和b都被复制到了堆上。因此,当block执行后,函数栈内访问被__block标记的b的时候,b的地址会变成堆中的地址。而变量a,仍会指向函数栈内原有的变量a的空间。

循环引用

只要对象没有强指针指向它,它就会被释放,最后被销毁。

前面说过,使用block不管在ARC还是非ARC ,都必须使用copy将block复制到堆中。但block在copy时都会对block内部用到的对象进行强引用(ARC)或者retainCount增1(非ARC)。在ARC与非ARC环境下对block使用不当都会引起循环引用问题。

一般表现为:某个类将block作为自己的属性变量,然后该类在block的方法体里面又使用了该类本身。如下图:


iOS 中 __block ios中block有几种_循环引用_02


block的这种循环引用会被编译器捕捉到并及时提醒。

网上大部分帖子都表述为"block里面引用了self导致循环引用",但事实真的是如此吗?其实这种说法是不严谨的,不一定要显式地出现"self"字眼才会引起循环引用。我们改一下代码,不通过属性self去访问变量,而是通过实例变量_去访问,如下:


iOS 中 __block ios中block有几种___weak_03


很明显了:

即使在你的block代码中没有显式地出现"self",也会出现循环引用!只要你在block里用到了self所拥有的东西!就有可能引发循环引用。

循环引用使得两者都无法释放,导致内存泄露。解决思想:可以将其中一个声明为弱指针,这样就可以避免循环引用问题。

ARC

__weak typeof(self)  weakSelf=self; // 其实 __weak someClass *weakSelf = self也是OK的!!!

__block typeof(self)  blockSelf=self; // 其实 __block someClass*blockSelf = self也是OK的!!!

非ARC

__block typeof(self)  blockSelf=self; // 其实 __block someClass *blockSelf = self也是OK的!!!

__block和__weak修饰符的区别其实是挺明显的:

1.__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。

2.__weak只能在ARC模式下使用,也只能修饰对象(如:NSString),不能修饰基本数据类型(如:int)。

3.__block对象可以在block中被重新赋值,__weak不可以。

PS:__unsafe_unretained修饰符可以被视为iOSSDK 4.3以前版本的__weak的替代品,不过不会被自动置空为nil。所以尽可能不要使用这个修饰符。

看下面的例子(ARC):

定义一个HXTool工具类,如下:

#import <Foundation/Foundation.h>
typedef void (^blockOperation)();
@interface HXTool : NSObject
@property (nonatomic, copy) blockOperation operation;// 执行打印操作
@end

在控制器view中,ViewController.m

#import "ViewController.h"
#import "HXTool.h"

@interface ViewController ()
@property (nonatomic, strong) HXTool *tool;
@property (nonatomic, assign) int age;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    HXTool *tool = [[HXTool alloc] init];
    self.tool = tool;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.tool.operation = ^(){
        self.age ++;
        NSLog(@"age = %d", self.age);
    };
    self.tool.operation();
}
@end

上面写法对应ViewController和HXTool的关系:


iOS 中 __block ios中block有几种_block_04

每个对象都有强指针引用着,谁也释放不了,导致循环引用。

将self声明为__blockViewController *vc = self;或者 __weak ViewController *vc = self;则关系变成下图:

iOS 中 __block ios中block有几种_iOS 中 __block_05

(上面2张图中实线代表强引用,虚线代表弱引用)

由于self被声明为弱引用,block不会再对它进行强制引用,当不在使用self的时候,由于没有强引用引用它,所有它可以被释放掉,这样就不会循环引用。

 

retain cycle问题的根源在于Block和obj可能会互相强引用,互相retain对方,这样就导致了retaincycle,最后这个Block和obj就变成了孤岛,谁也释放不了谁。

解决:将其中一个声明为弱指针即可。