iOS之Block

一、Block的三种类型

1.全局Block(NSGlobalBlock)
2.栈Block(NSMallocBlock)
3.堆Block(NSStackBlock)

区别在于:如果没有引用局部变量,或者只引用了静态变量和全局变量,则为全局Block,如果内部有使用局部变量,如果有被强指针引用过,就是堆Block,如果没有则为栈Block。

二、Block的本质

我们通过对一段Block代码的clang运行得到关于Block的结构体,我们可以得知Block的一些本质:

 

1.Block是OC对象,因为他有isa指针
2.Block本身是在栈上的
3.Block具备捕获对象的能力,因为他可以捕获到obj对象
4.Block的本质是一个 Block_layout 类型的结构体
5.copy和dispose函数是用来对block内部的对象进行内存管理的,block拷⻉到堆上会调用copy函 数,在block从堆上释放的时候会调用dispose函数

三、Block的循环引用

Block循环引用最容易出现问题的地方就是self和block之间的循环引用导致双方都无法释放的情况。下面这个就是最简单的一种情况:

- (void)test {
    self.block = ^{
        self.name = @"LG";
    };
}

解决循环引用的办法:
3.1 使用_weak + _strong 协作

3.1.1 仅通过_weak解决循环引用(是可行的,但是在遇到某些情况会有问题)

- (void)test {
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        weakSelf.name = @"LG";
    };
}

3.1.2 通过_weak + _strong 协作解决

如果仅用_weak可能出现下面这种情况:如果controller还没有等到执行dispatch_after就被提前pop,等到3s dispatch 运行的时候name已经为nil了,这显然是不合理的。这种情况下我们就需要使用_weak + _strong 协作解决

-(void)test {
    self.name = @"lg";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name);
        });
    };
    self.block();
}
//解决方式:
-(void)method1 {
    self.name = @"lg";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%p",&strongSelf);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();
}

此时block对象里使用的是strongself这个临时变量,他的生命周期仅在block内部,在block内部,self的引用计数+1,出了作用域,引用计数又-1,这样既没有引起循环引用,又延长了self的生命周期,也可以取到正常的值。

3.2 __block

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

__block会在下面讲,使用这种方式也可以解决循环引用,但是block执行完一次,下一次执行之前要重新复制self,不然会出问题,block内部将局部变量设为nil,否则会循环引用,不推荐使用。

3.1.3 参数传递

-(void)method3 {
    self.name = @"lg";
    self.block = ^(LGViewController *vc){
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.block(self);
}

通过使用block的参数进行传递,同样可以解决循环引用的问题。

3.2 staic修饰self的情况,因为static不会自动释放,因此需要手动的把_staticSelf = nil

static MyViewController *_staticSelf;
-(void)test2 {
    __weak typeof(self) weakSelf = self; // self是MyViewController的实例对象
    _staticSelf = weakSelf;
}
// 产生循环引用

3.3 weakself->block2->strongself->weakself

- (void)test3 {
    __weak typeof(self) weakSelf = self;
    self.block1 = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%p", &strongSelf);
        weakSelf.block2 = ^{
            NSLog(@"%@", strongSelf);
        };
        weakSelf.block2();
    };
    
    self.block1();
}
// 产生循环引用

这里可以将block捕获的强引用改为弱引用就正常了。

四、Block对象中的引用计数(通过下面的代码深入了解)

- (void)blockDemo1{
    //1 3 4
    NSObject *objc = [NSObject new];
    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    //1
    //栈block的时候会自动引用计数+1 栈变成堆的时候会有一次copy 使得引用计数又+1
    void(^block1)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    block1();
    //3
    
    //栈block自动+1
    void(^__weak block2)(void) = ^{
        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
    };
    block2();
    //4
    
    //5 3从2来(2是4) 引用计数+1
    void(^block3)(void) = [block2 copy];
    block3();
    //5
    
    //4 4从1来(1是3) 引用计数+1
    void(^block4)(void) = [block1 copy];
    block4();
    //4

}

五、捕获变量

int b = 20;
static int c = 30;
int main(int argc, const char * argv[]) {
    
    static int a = 10;
    __block NSObject *objc = [NSObject new];
    NSLog(@"%@",objc);
    void(^__weak block)(void) = ^{
        a ++;
        b ++;
        c ++;
        NSLog(@"%d %d %d %@",a,b,c,objc);//11 21 31
    };
    NSLog(@"%@",block);
    block();
    return NSApplicationMain(argc, argv);
}
1.在block内部可以修改静态变量和全局变量的值,不可以改变局部变量的值(因为不能修改的是栈中局部的值,因为没有forwarding指针链接)
2.1 全局变量不会捕获可以直接使用
2.2 静态变量捕获的是地址
2.3 局部变量捕获的是值
3.要想修改局部变量的值,需要在局部变量之前加上__block

六、__block

6.1 __block的底层结构(重点关注forwarding指针)

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

用__block修饰的变量在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有一 个 __forwarding 的结构体指针,指向结构体本身,这个 __forwarding 就是用来操作Block内部的代码的

block创建的时候是在栈上的,在将栈block拷⻉到堆上的时候,同时也会将block中捕获的对象拷⻉到堆上,然后就会将栈上的__block修饰对象的__forwarding指针指向堆上的拷⻉之后的对象(原来forward ing指针指向的是自己)。 这样的话我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的 __forwarding指针将堆和栈的对象链接起来。因此就可以达到修改的目的。

__block与Block相似,如果在栈区就会深拷贝一份,forward指针都指向堆区,如果是在堆区,则引用计数+1

6.2 __block不可以修饰静态变量和全局变量

6.3 __block的copy是通过_Block_byref_copy来进行拷贝的

// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
//    被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
//    原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
    // arg 强转为 Block_byref * 类型
    struct Block_byref *src = (struct Block_byref *)arg;

    // 引用计数等于 0
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        // 为新的 byref 在堆中分配内存
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        // 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
        // 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
        // one for caller 很好理解,那 one for stack 是为什么呢?
        // 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        // 堆上 byref 的 forwarding 指向自己
        copy->forwarding = copy; // patch heap copy to point to itself
        // 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
        src->forwarding = copy;  // patch stack to point to heap copy
        // 拷贝 size
        copy->size = src->size;

        // 如果 src 有 copy/dispose helper
        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            // 取得 src 和 copy 的 Block_byref_2
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            // copy 的 copy/dispose helper 也与 src 保持一致
            // 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            // 如果 src 有扩展布局,也拷贝扩展布局
            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                // 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
                copy3->layout = src3->layout;
            }
            // 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            // 如果 src 没有 copy/dispose helper
            // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    // src 已经在堆上,就只将引用计数加 1
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

七、block的copy

7.1调用时机

1.有强指针第一次指向的时候,会调用一次copy
2.[block copy]
1.堆[block copy],只进行引用计数+1,相当于浅拷贝
2.如果是全局block,[block copy]直接返回,相当于不拷贝
3.如果是栈block,[block copy]是重新开辟新的内存并创建,并且isa指向_NSConcreteMallocBlock,设置类型为堆block,然后返回
在ARC下, 对于block使用copy与strong其实都一样, 
因为block的retain就是用copy来实现 的, 
所以在ARC下 block使用 copy 和 strong 都可以

八、通过两道题来熟悉一下不同类型的block

//会崩溃;
- (void)blockDemo1{
    int a = 1;
    //栈block
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"-----%d", a);
    };
    struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
    void(^strongBlock)(void) = weakBlock;//【weakBlock copy】就不会崩溃了
    //将栈block拷贝到堆上 是深拷贝
    //weakBlock、blc、strongBlock指向的是同一块内存空间
    blc->invoke = nil;
    //这里将函数指针置为nil,导致其无法实现
    strongBlock();//这里的调用就崩溃了
}
//正常运行
- (void)blockDemo2{
    
    int a = 1;
    //栈block
    void(^__weak block1)(void) = nil;
    {
        //栈block
        void(^__weak block2)(void) = ^{
            NSLog(@"%d",a);//打印5
        };
        block1 = block2;//【block2 copy】就会崩溃,因为此时的block2在堆上,他的释放和作用域有关,就会崩溃,相当于此时的block指向了nil导致崩溃
    }//block2在作用域之外也没有被释放
    //栈上的变量出了作用域之后不会被释放,而是在函数的作用域释放的,也就是说栈上的释放仅和函数的生命周期有关(blockdemo2)
    block1();
}
posted on 2022-06-16 17:16  suanningmeng98  阅读(189)  评论(0编辑  收藏  举报