iOS Block

Block的分类

Block有三种类型:全局Block,堆区Block,栈区Block

全局Block

当Block没有引用到局部变量时或者Block里面使用的是全局变量,静态变量时为全局Block

    int a = 10;
    void (^block)(void) = ^{
         NSLog(@"hello world");
    };

    NSLog(@"block:%@", block);

输出结果:block:<__NSGlobalBlock__: 0x104e580f8>
    static int a1 = 20;
    void (^block)(void) = ^{
        NSLog(@"hello - %d",a1);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSGlobalBlock__: 0x100cec0f8>

堆区Block

当Block里有引用到局部变量时为堆区block

    int a = 10;
    void (^block)(void) = ^{
        NSLog(@"hello - %d",a);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSMallocBlock__: 0x281065950>
    __block int a = 10;
    void (^block)(void) = ^{
        NSLog(@"hello - %d",a);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSMallocBlock__: 0x281aad5c0>

栈区Block

在Block名前面加个__weak就是栈区block

    __block int a = 10;
    static int a1 = 20;
    void (^__weak block)(void) = ^{
        NSLog(@"hello - %d",a);
        NSLog(@"hello - %d",a1);
    };

    NSLog(@"block:%@", block);
输出结果:block:<__NSStackBlock__: 0x16f7ccfb0>

堆Block和栈Block的区别

接下来看看这两种block有什么区别呢?
先看个示例:

    __block int a = 10;
    __block int b = 20;
    NSLog(@"a:%p---b:%p", &a, &b);
    void (^__weak block)(void) = ^{
        NSLog(@"hello - %d---%p",a, &a);
        a++;
    };
    void (^block1)(void) = ^{
        NSLog(@"hello - %d---%p",b, &b);
        b++;
    };
    block();
    block1();
    NSLog(@"block:%@---block1:%@", block, block1);
    NSLog(@"a:%d---b:%d", a, b);
    NSLog(@"a:%p---b:%p", &a, &b);
输出结果:
a:0x16bb7cfe8---b:0x16bb7cfc8
hello - 10---0x16bb7cfe8
hello - 20---0x283e0b1b8
block:<__NSStackBlock__: 0x16bb7cf70>---block1:<__NSMallocBlock__: 0x28307d9b0>
a:11---b:21
a:0x16bb7cfe8---b:0x283e0b1b8

通过结果我们看到,首先block的地址是在栈区,而block1的地址是在堆区,而栈block引用的变量a的地址并没有变化,而堆block1引用的变量b的地址也相应变成了堆区0x283e0b1b8,并且后面使用的b的地址都是堆区上的。
总结:栈block存放在栈区,对局部变量引用只拷贝局部变量的地址,而堆block存放在堆区,并且直接将局部变量拷贝了一份到堆空间。
接下来我们再来看个示例:

NSObject *objc = [NSObject new];
    NSLog(@"%@---%ld",objc, CFGetRetainCount((__bridge CFTypeRef)(objc)));// 1
    // block 底层源码
    // 捕获 + 1
    // 堆区block
    // 栈 - 内存 -> 堆  + 1
    void(^strongBlock)(void) = ^{ // 1 - block -> objc 捕获 + 1 = 2
        NSLog(@"%@---%ld",objc, CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    strongBlock();

    void(^__weak weakBlock)(void) = ^{ // + 1
        NSLog(@"%@---%ld",objc, CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    weakBlock();
    
    void(^mallocBlock)(void) = [weakBlock copy];
    mallocBlock();
输出结果:
---1
---3
---4
---5

奇怪为什么堆区block里面的对象引用计数加2呢?而后面的mallocBlock只加1呢?
首先objcstrongBlock里面必然会拷贝一份到堆区,所以会加1,但是他是从当前函数的栈区拷贝吗?并不是,对于堆区Block一开始编译时是栈block这时候objc对象地址拷贝了一份引用计数加1,后面从栈block变成堆block,又拷贝了一份引用计数又加1,所以这时候是3
weakBlock栈block仅拷贝了一份,所以引用计数加1,这时候是4
mallocBlockweakblock拷贝了一份,所以引用计数再加1,这时候是5
相当于strongBlock = weakblock + void(^mallocBlock)(void) = [weakBlock copy];

再来看个示例:

    NSObject *a = [NSObject alloc];
    NSLog(@"1---%@--%p", a, &a);
    void(^__weak weakBlock)(void) = nil;
    {
        // 栈区
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"2---%@--%p", a, &a);
        };
        weakBlock = strongBlock;
        strongBlock();
        NSLog(@"3 - %@ - %@",weakBlock,strongBlock);
    }
    weakBlock();
    NSLog(@"4---%@--%p", a, &a);
输出结果:
1-----0x16bcf4fd8
2-----0x16bcf4fc0
3 - <__NSStackBlock__: 0x16bcf4fa0> - <__NSStackBlock__: 0x16bcf4fa0>
2---(null)--0x16bcf4fc0
4-----0x16bcf4fd8

当前是栈区strongBlock的赋值给外面的栈区weakBlock,因为都是存放在栈空间的,只有当前函数结束才会被销毁,随意这边weakBlock调用并不会有什么问题。如果换成堆区block就不一样了
注意:这边的a对象weakBlock()调用时是nil,通过上面打印可以看出a对象在进入到strongblock里,&a拷贝了一份,拷贝的这一份地址指向的跟外面一样,但是当strongblock出了{},尽管strongblock对象不再了,但是其指向的内存空间还在,销毁之前给了外面的weakBlock,同理a也一样,对象(此时a指向的内容)不在了,但是内存空间却还在。

NSObject *a = [NSObject alloc];
//    NSLog(@"1---%@--%p", a, &a);
    void(^__weak weakBlock)(void) = nil;
    {
        // 堆区
        void(^strongBlock)(void) = ^{
            NSLog(@"2---%@--%p", a, &a);
        };
        weakBlock = strongBlock;
        strongBlock();
        NSLog(@"3 - %@ - %@",weakBlock,strongBlock);
    }
    weakBlock();
//    NSLog(@"4---%@--%p", a, &a);
输出结果:
2-----0x281e7f6e0
3 - <__NSMallocBlock__: 0x281e7f6c0> - <__NSMallocBlock__: 0x281e7f6c0>
调用weakBlock时崩溃

为什么呢?因为在{}里面的堆区strongBlock出了大括号就会被销毁,此时你去调用这个block就会崩溃
注意:这边weakBlock为什么也是__NSMallocBlock__,其实weakBlock相当于是指针,此时指向的是一个堆上的内存所以是__NSMallocBlock__

Block的循环引用

内存泄漏一个主要原因就是block的循环引用。那么如何解决循环引用呢?

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循环引用
    self.name = @"hongfa";
    self.block = ^{
        NSLog(@"%@", self.name);
    }
    
    self.block();
}

这边vc 引用了block ,block引用了vc,最后面造成了循环引用,那么如何解决呢?
方案一:通过weak来解决

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循环引用
    self.name = @"hongfa";
    __weak typeof(self) weakself = self;
    self.block = ^{
        NSLog(@"%@", weakself.name);
    };
    
    self.block();
}
直接使用weakself来代替self,weakself是弱引用,这样不会导致引用计数+1。
这边也有一个问题如下:
    self.name = @"hongfa";
    __weak typeof(self) weakself = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakself.name);
        });
    };
    
    self.block();

如果当我们延迟使用weakself的话,这时候的weakself可能已经被销毁了,这时候就需要用到__strong typeof(weakself) strongself = weakself;

    self.name = @"hongfa";
    __weak typeof(self) weakself = self;
    self.block = ^{
        __strong typeof(weakself) strongself = weakself;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongself.name);
        });
    };
    
    self.block();

__strong可以让对象暂时在存活一段时间,用完就会销毁,这样也不会带来内存泄漏。
以上就是通过__weak__strong来解决block的循环引用。
方案二:通过临时变量来解决

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

定义一个跟self一样类型的vc = self,然后block引用vc,用完之后再把vc=nil,这样引用链:self -> block -> vc -> self这样也可以解决循环引用问题

方案三:通过参数将self传进去,传参的话,参数是在栈区,函数运行好栈区销毁参数也就跟着销毁,所以也可以解决循环引用问题

   // 通讯 参数 block 通知
    self.hfblock = ^(ViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.hfblock(self);

以上就是目前掌握的三种解决循环引用的方案。

block的本质

接下来看看block通过xcrun后究竟是什么?

int main(int argc, char * argv[]) {
    int a = 9;
    __block int b = 10;
    NSObject *objc = [NSObject alloc];
    void(^Block)(void) = ^{
        NSLog(@"a:%d", a);
        NSLog(@"b:%d", b);
        NSLog(@"objc:%@", objc);
    };
    return 0;
}

xcrun -sdk iphonesimulator clang -rewrite-objc main.m
int main(int argc, char * argv[]) {
    int a = 9;
    __attribute__((__blocks__(byref))) __Block_byref_b_0 b = {(void*)0,(__Block_byref_b_0 *)&b, 0, sizeof(__Block_byref_b_0), 10};

    NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));

    void(*Block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, objc, (__Block_byref_b_0 *)&b, 570425344));
    return 0;
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  NSObject *objc;
  __Block_byref_b_0 *b; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, NSObject *_objc, __Block_byref_b_0 *_b, int flags=0) : a(_a), objc(_objc), b(_b->__forwarding) { // 构造函数
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __Block_byref_b_0 {
  void *__isa;
__Block_byref_b_0 *__forwarding;
 int __flags;
 int __size;
 int b;
};

xcrun后看到的__block b,底层是变成了结构体b,里面保存了b的值10,整个block也变成了__main_block_impl_0结构体对象,把a, b, objc作为参数传进去。而在block结构体里定义了三个成员变量来保存a,b,objc
通过上面的例子我们就很清楚block的底层结构以及他是如何对引用局部变量

你可能感兴趣的:(iOS Block)