Block的本质(三)

532 阅读5分钟

Block内部引用对象

先看看下面代码执行的效果

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        {
            XWPerson *person = [[XWPerson alloc] init];
            person.age = 10;
            ^{
                NSLog(@"person -- %ld",(long)person.age);
            }();
        }
        
        NSLog(@"*******");
    }
    return 0;
}

会发现当 函数体内 大括号执行完毕后 XWPerson 即被释放,此时的block是栈类型的Block及__NSStackBlock__,存储在栈区的block即便引用了对象,也会跟随大括号一并释放

咱们修改一下以上代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        MyBlock myBlock;
        {
            XWPerson *person = [[XWPerson alloc] init];
            person.age = 10;
            myBlock = ^{
                NSLog(@"person -- %ld",(long)person.age);
            };
            myBlock();
            
        }
        
        NSLog(@"*******");
    }
    return 0;
}

此时发现执行到打印****时,person对象依然没有被释放,此时block已经对person对象进行了强引用.因为此时的block为强指针引用,类型为对block即__NSMallockBlock__.那么为什么堆block会对外部对象强引用呢? 来看看底层实现

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  XWPerson *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, XWPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

其中main_block_desc_0 定义为:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} 

较之前block引用基本成员类型时,main_block_desc_0多了两个参数分别为copy和dispose,并且传入的都是__main_block_impl_0(block)本身

当block执行copy操作的时候,执行的是

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

方法。最终调用的 _Block_object_assign 方法会对block引入的对象 person 进行引用计数操作,当所引入的对象使用 strong 修饰则使其引用计数加1,若使用weak修饰则引用计数不变。

当 block 执行完毕的时候会调用 dispose 方法,而dispose 在底层会调用

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

方法,将block内部引用的对象成员引用计数减1,如果此时外部对象使用strong 修饰,引用计数在copy加1后 此时再减1.依然会强引用外部对象,不会释放,如果使用weak修饰,此时因为自身已经被释放,所以不会再持有所引用外部对象,然而此时所引用外部对象是否会被释放取决于它的引用计数是否为 0。

block内部修改外部变量的值

我们知道,如果block 内部捕获的外部变量为 auto 类型,在block 内部生成的是该变量的值类型变量,无法通过block内部的值修改外部变量。 如果想在block内部修改外部变量的值有几种方法?

1. 外部变量使用 static 修饰

使用 static 修饰的变量block内部会直接获取到变量的内存地址,可以直接修改。这是咱们前面讲过的

2. 使用__block

若使用 static 变量修饰,该变量的生命周期就会无限延长,这不符合我们的设计思路,故我们可以使用 __block 来修饰外部变量,从而达到在block内部修改外部成员变量的目的。 那 __block 是如何实现此需求的呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        MyBlock block = ^{
            a = 20;
            NSLog(@"a --- %d",a);
        };
        block();
    }
    return 0;
}

上面是个很普通的例子,咱们来看看其内部是怎么实现的?

转化为 c++底层看看实现

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

我们可以看到通过__block修饰的外部变量被定义为__Block_byref_a_0对象,他们的声明为

  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};


此时在main函数内声明的__block类型的变量会以此方式初始化:

     __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

其中__forwarding保存的原变量a的内存地址,size为当前变量的内存大小,10保存原变量的值,因此,我们在block内部修改原变量时:

(a->__forwarding->a) = 20;

直接去原变量的地址进行更改,从而实现在block内部改变外部变量

__block和对象类型的auto变量的内存管理

对于block内部捕获的对象类型的auto变量和__block修饰的变量.

如果block在栈区,不会对他们进行内存管理,即不会强引用外部变量

如果block被赋值到堆区,则会调用内部copy函数对外部__block修饰的变量和对象类型的auto变量进行内存管理

当block从内存中移除时,同样也会调用dispose函数对所引用的外部变量进行释放。

循环引用

使用 block 很容易形成循环引用,如果一个类中定义的block内部引用了该类的外部属性,包括 类本身的 self, 均会导致 self 强引用 block,block 也强引用 self。导致self不会被释放。如下代码就会造成循环引用:

@interface GTPerson : NSObject
/**/
@property (nonatomic,copy) GTBlock block;
/**/
@property (nonatomic,assign) int age;
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        
        GTPerson *person = [GTPerson new];
        person.age = 10;
        person.block = ^{
            NSLog(@"----%d",person.age);
        };
        }
    }

产生循环引用的本质原因是,在block内部实现里,会将person 捕获到block内部,并且strong 强引用。

避免产生循环引用

1. (ARC 环境下) __weak : 弱引用对象,指向的对象销毁时,会自动将指针置为nil。因此一般通过__weak来解决问题。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        XWPerson *person1 = [[XWPerson alloc] init];
        person1.age = 18;
        __weak typeof(person1) weakPerson = person1;
        person1.personBlock = ^{
            NSLog(@"%ld",(long)weakPerson.age);
        };
        person1.personBlock();
    }
    return 0;
}

2. (ARC / MRC 环境下) __unsafe_unretained : 弱引用对象,指向的对象销毁时,不会自动将指针置为nil。再次引用该对象时可能会产生访问僵尸对象的错误,产生崩溃,故不建议使用!

__unsafe_unretained XWPerson *person1 = [[XWPerson alloc] init];


3. (ARC / MRC 环境下) __block : 使用__block 修饰对象. 在ARC环境下-前提是一定要调用此block,并且要在block内部将所引用的外部变量手动置nil。因为 MRC 环境下,引用__block 修饰的对象不会使其引用计数加1,所以不需要手动置nil,也不是必需要使用block。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block XWPerson *person1 = [[XWPerson alloc] init];
        person1.age = 18;
        person1.personBlock = ^{
            NSLog(@"%ld",(long)person1.age);
            person1 = nil;
        };
        person1.personBlock();
    }
    return 0;
}