阅读 447

OC知识梳理:Block

1. 什么是Block?

Block是将函数及其执行上下文封装起来的对象

// Block源码

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    // 保存内部代码块执行的函数地址
    void *FuncPtr;
};

// Block底层的结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  // 构造函数(类似于OC的init方法),返回结构体对象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
     NSLog((NSString *)&__NSConstantStringImpl__var_folders_zz_1js09xx15fz7j7qyrylx4xm80000gn_T_main_3df4b0_mi_0);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        // 定义block变量
        void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
        // 执行block内部的代码
        block->FuncPtr(block);
    }
    return 0;
}
复制代码
  • Block底层数据结构是__main_block_impl_0类型,定义的block变量是指向__main_block_impl_0类型的对象类型指针。
  • __main_block_impl_0构造方法的需要两个参数:第一个参数:__main_block_func_0函数是对Block内部代码块的封装;第二个参数类型是__main_block_desc_0_DATA类型指针,内部主要对Block的信息封装,比如包含block的占用内部空间大小。
  • __main_block_impl_0构造方法中将封装block代码块的__main_block_func_0函数地址传入__main_block_impl_0对象的FuncPtr成员保存。
  • 当执行block,直接通过指针访问到FuncPtr成员的函数地址进行调用。

什么是Block调用? Block调用即是函数的调用。

2. 截获变量

  • 对于基本数据类型的局部变量截获其值。
  • 对于对象类型的局部变量连同所有权修饰符一起截获。
  • 以指针形式截获局部静态变量。
  • 不截获全局变量、静态全局变量。

3. __block修饰符

  • 一般情况下,对被截获变量进行赋值操作需添加__block修饰符。
  • 赋值和使用是两个场景(例如只是为可变数组类型的局部变量添加元素,就只是使用,并不需要__block修饰)。

由于截获变量的特征,对基本数据类型和对象类型的局部变量赋值时,需使用__block修饰;而对静态局部变量、全局变量和静态全局变量赋值时,并不需要使用__block修饰符。虽然可以通过定义static或者全局变量来实现在block内部修改变量,但是全局变量和static会修改变量的作用域,因此开发中一般使用__block。

1) __block是如何实现的?

__block之所以可以修改局部变量,是因为编译器将__block修饰的变量包装成了一个对象。 对象中包含:

  • __isa
  • __forwarding
  • __flags
  • __size
  • val // 值

2) __forwarding指向

  • 若block在栈上,且未做过copy操作,则__block对象其中的__forwarding指针指向自己。
  • 栈上的block复制到堆上后,则栈上的__block对象和堆上copy出的对象,其中的__forwarding指针都指向堆中的__block对象。

__forwarding的存在是为了不论在任何内存位置,都可以顺利的访问同一个__block变量。

4. Block内存管理

1) Block有哪几种类型?

block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。

  • _NSConcreteStackBlock:存储在栈区
  • _NSConcreteMallocBlock:存储在堆区
  • _NSConcreteGlobalBlock:存储在全局区的.data段

没有访问auto变量的block存储于数据段;访问了auto变量的block存储于栈区;对栈区的block做copy操作,其结果存储于堆区。

Block的Copy操作

Block类别Copy结果
_NSConcreteStackBlock
_NSConcreteMallocBlock增加引用计数
_NSConcreteGlobalBlock数据区什么也不做

2) 当block内部访问了对象类型的auto变量时,是否会强引用?

  • 如果block是在栈上,将不会对auto变量产生强引用。
  • 当block被拷贝到堆时:
    • 会调用block内部的copy函数;
    • copy函数内部会调用_Block_object_assign函数;
    • _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。
  • 当block从堆中移除时:
    • 会调用block内部的dispose函数;
    • dispose函数内部会调用_Block_object_dispose函数;
    • _Block_object_dispose函数会自动释放引用的auto变量(release)。
  • 如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象;
  • 如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用。

3) __block的内存管理

  • 当block在栈上时,并不会对__block变量产生强引用。
  • 当block被拷贝到堆时:
    • 会调用block内部的copy函数;
    • copy函数内部会调用_Block_object_assign函数;
    • _Block_object_assign函数会对__block变量形成强引用(retain)。
  • 当block从堆中移除时:
    • 会调用block内部的dispose函数;
    • dispose函数内部会调用_Block_object_dispose函数;
    • _Block_object_dispose函数会自动释放引用的__block变量(release)。

4) 在ARC环境下,编译器会在什么情况下自动将栈上的block复制到堆上?

  • block作为函数返回值时;
  • 将block赋值给__strong指针时;
  • block作为Cocoa API中方法名含有usingBlock的方法参数时;
  • block作为GCD API的方法参数时。

5. Block的循环引用问题

1) 为什么Block会产生循环引用?

如果Block是某一对象的成员变量,且内部使用了该对象的其他成员变量。此时Block会对当前对象进行截获变量,Block强引用当前对象,而当前对象又强引用Block,产生自循环引用。可以通过声明对象为__weak来消除自循环引用。

graph LR
A[Block] -->|持有| B[对象]
B -->|持有| A

如果Block使用__block变量,也会产生循环引用,此时Block强引用__block变量,__block变量强引用对象,对象强引用Block,需要手动断环来解决。在Block内部使用完__block变量后,手动将__block变量置为nil。这种方式有一个弊端,若Block一直得不到调用,循环引用是无法解除的。

graph LR
A[Block] -->|持有| B[__block变量]
B -->|持有| C[对象]
C -->|持有| A

2) ARC下如何解决block循环引用的问题?

__weak 不会产生强引用,指向的对象销毁时,会自动将指针置为nil。

Person *person = [[Person alloc] init];
__weak typeof(person) weakPerson = person;
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};
复制代码

__unsafe_unretained 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变。

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};
复制代码

__block 必须把引用对象置位nil,并且要调用该block。

__block Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", person.age);
    person = nil;
};
person.block();
复制代码

3) MRC下如何解决block循环引用的问题?

__unsafe_unretained

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};
复制代码

__block

__block Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", person.age);
};
复制代码
文章分类
iOS
文章标签