从底层了解面试题-Block篇

147 阅读5分钟

Block内部结构

block的数据结构定义 image.png

代码如下:

struct Block_descriptor {    
    unsigned long int reserved;     
    unsigned long int size;     
    void (*copy)(void *dst, void *src);     
    void (*dispose)(void *); 
};  

struct Block_layout {     
    void *isa;     
    int flags;     
    int reserved;     
    void (*invoke)(void *, ...);     
    struct Block_descriptor *descriptor;     
};

通过上面的结构, 可以看出一个 block 实例的构成实际上有6个部分:

  1. isa 指针: 所有对象都有该指针,用于实现对象相关的功能。
  2. flags: 附加标识位, 在 copydispose 等情况下可以用到。
  3. reserved:保留变量。
  4. invoke: 函数指针,指向 block 的实现代码, 也可以说是函数调用地址。
  5. descriptor: 表示该 block 的附加描述信息,主要是 size,以及 copydispose 函数的指针。这两个辅助函数在拷贝及丢弃块对象时运行, 其中会执行一些操作, 比方说, 前者要保留捕获的对象,而后者则将之释放。
  6. variables: 捕获的变量,block 能够访问它外部的局部变量,就是因为将这些变量复制到了结构体中。

面试题

2.block 是类吗?有哪些类型?

block 算是类,因为 blockisa 指针 有三种类型

  1. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

3.一个 int 变量被 __block 修饰与否的区别?

先看代码

__block int a = 20;
int b = 20;

void (^printBlock)(void) = ^{
    a -= 20;
    NSLog(@"block内部 a = %d",a);
    NSLog(@"block内部 b = %d",b);
};

printBlock();

a += 20;
b += 20;

NSLog(@"block外部 a = %d",a);
NSLog(@"block外部 b = %d",b);

printBlock();


block内部 a = 0
block内部 b = 20
block外部 a = 20
block外部 b = 40
block内部 a = 0
block内部 b = 20

通过 __block 修饰 int ablock 结构体中对这个变量的引用是指针拷贝,它会作为 block 结构体构造参数传入到结构体中且复制这个变量的指针饮用,从而达到可以修改变量的作用。

int b 没有被 __block 修饰,block 内部对 b 是值拷贝,所以在 block 外部修改 b 不影响内部 b 的变化。

4.block 的变量截获?

先看代码

int main() {
    id arr = [NSMutableArray new];
    void (^subBlock)(id) = ^(id obj){
        [arr addObject:obj];
        NSLog(@"arr count = %ld",[arr count]);
    };

    subBlock([NSObject new]);
    subBlock([NSObject new]);
    subBlock([NSObject new]);
}
   
   
arr count = 1
arr count = 2
arr count = 3

把上面代码翻译成C(在命令行中输入clang -rewrite-objc block1.c即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 blockc 语言实现,将 block1.cpp 中一些无关的代码去掉,将关键代码引用如下:)

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

struct __main_block_impl_0 {
    struct __block_impl impl;// 成员变量
    struct __main_block_desc_0* Desc;// desc结构体声明
    __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;
    }
};

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()
{
    (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
    return 0;
}

OC中,C结构体里不能含有被__strong修饰的变量,因为编译器不知道应该合适初始化和废弃C结构体,但是OC运行时库能够准确把握block从栈复制到堆,以及堆上的block被废弃的时机,在实现上是通过 __TestClass__testMethods_block_copy_0 函数和 __TestClass__testMethods_block_dispose_0 函数进行的

static void __TestClass__testMethods_block_copy_0 (struct __TestClass__testMethods_block_imp_0 *dst,struct __TestClass__testMethods_block_imp_0 *src) {
    _Block_object_assign((void *)&dst->array,(void *)src->array,3)
}

static void __TestClass__testMethods_block_dispose_0 (struct __TestClass__testMethods_block_imp_0 *src) {
    _Block_object_dispose((void *)src->array,3);
}
  • _Block_object_assign 相当于 retain 操作,将对象赋值在对象类型的结构体成员变量中
  • _Block_object_dispose 相当于 release 操作

什么时候栈上的 block 会被复制到堆?

  • 调用 blockcopy 函数时
  • block 作为函数返回值返回时
  • block 赋值给附有 __strong 修饰符 id 类型的类或者 block 类型成员变量时
  • 方法中含有 usingBlockCocoa 框架方法时
  • GCDAPI 中传递 block

什么时候 block 被废弃呢?

堆上的 block 被释放后,谁都不再持有 block 时调用 dispose 函数

以上就是变量被 block 捕获的内容

5.block 在修改 NSMutableArray 时,需不需要添加 __block ?

修改数组的存储内容不需要添加 __block 修饰 修改数组对象的本身则需要添加 __block 修饰

6.block 怎么进行内存管理?

  1. 全局 block_NSConcreteGlobalBlock 的结构体实例设置在程序的数据存储区,所以可以在程序的任意位置通过指针访问,产生条件:

    1. 记述全局变量的地方有 block 语法时
    2. block 不截获的自动变量 两个条件只要满足一个就可以产生全局 block
  2. block_NSConcreteStackBlock 在生成 block 以后,如果这个 block 不是全局 block,那它就是栈 block,生命周期在其所属的变量作用域内;如果 block 变量和 __block 变量复制到了堆上以后,则不再受到变量作用域结束的影响了,因为它变成了堆 block

  3. block_NSConcreteMallocBlock 将栈 block 复制到堆以后,block 结构体的 isa 成员变量变成 _NSConcreteMallocBlock

7.block 可以用 strong 修饰吗?

ARC 中可以,因为在ARC环境中的 block 只能在堆内存或全局内存中,因此不涉及到从栈拷贝到堆的操作

MRC 中不行,因为要有拷贝的过程,如果执行 copy 用 strong 的话会 crash, strong 是 ARC 中引入的关键字,如果使用 retain 相当于忽视了 block 的 copy 过程。

8.解决循环引用为什么要用 __stong,__weak 修饰?

首先因为 block 捕获变量时,结构体构造时传入了 self,造成默认的引用关系

__weak 修饰的变量,不会出现引用计数+1,也就不会造成 block 强持有外部变量,这样也就不会出现循环引用的问题了。

但是,我们的 block 内部执行的代码中,有可能是一个异步操作,或者延迟操作,此时引用的外部变量可能会变成 nil,导致意想不到的问题,而我们在 block 内部通过 __strong 修饰这个变量时,block 会在执行过程中强持有这个变量,此时这个变量也就不会出现 nil 的情况,当 block 执行完成后,这个变量也就会随之释放了。

9.block 访问对象类型的 auto 变量时,在 ARCMRC 下有什么区别?

ARC 下会对这个对象强引用,MRC 下不会