阿里、字节:一套高效的iOS面试题(七 - Block)

·  阅读 668

Block

回来看了一下,写的太乱了,而且不清楚,待重置

撸面试题中,文中内容基本上都是搬运自大佬博客及自我理解,可能有点乱,不喜勿喷!!!

原文题目来自:阿里、字节:一套高效的iOS面试题

1、block的内部实现,结构体是什么样的?

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    int autoA;
    int *autoStaticB;
    __Block_byref_auto__blockC_0 *auto__blockC;
    
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _autoA, int *_autoStaticB, __Block_byref_auto__blockC_0 *_auto__blockC, int flags = 0) : autoA(_autoA), autoStaticB(_autoStaticB), auto__blockC(_auto__blockC->__forwarding) {
        impl.isa = &_NSConcreateStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = des;
    }
}

struct __block_impl {
       
}

复制代码

2、block是类吗,有哪些类型?

不是类,但是是对象

  1. _NSConcreteGlobalBlock , 全局 Block
  2. _NSConcreteMallocBlock , 堆 Block
  3. _NSConcreteStackBlock , 栈 Block

3、一个int变量被 __block 修饰与否的区别?block的变量截获

被 __block 修饰时,自动生成一个封装这个变量的结构体。然后捕获这个结构体,内部操作为引用计数 +1

全局变量,直接使用访问

自由变量,将值拷贝到 Block 结构体中

局部静态变量,捕获其指针

__block 变量,通过 _Block_byref_assign_copy() 复制

Block 实例,通过 _Block_copy() 复制

OC 对象,引用计数 +1

__weak 对象,弱引用,直接拷贝指针

4、block在修改NSMutableArray,需不需要添加__block?

如果是修改 NSMutableArray 对象本身,需要;

如果只是修改其存储内容,不需要。

5、怎么进行内存管理的?

引用计数

6、block可以用strong修饰吗?

可以,编译器会自动拷贝

7、解决循环引用时为什么要用__strong、__weak修饰?

__weak 打破强引用

__strong 防止被访问变量被提前释放

8、 block发生copy时机?

  1. 手动 copy;
  2. 作为返回值;
  3. 被强引用 或者 copy 修饰;
  4. 系统 API 包含 usingBlock;

访问非全局变量时,或者作为返回值

9、Block访问对象类型的auto变量时,在ARC和MRC下有什么区别?

ARC 环境编译器会自动生成 copy/dispose helper 方法,调用这个方法将 retain 被访问的对象。

MRC 就需要自己来了。

搞事情~~~

1000 Block 到底是什么?

维基百科 中,有这样一段话:

A closure is a record storing a function together with an environment. 闭包,存储一个方法及其上下文环境的记录。

The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

这个上下文环境将这个方法的每一个自由变量(在本地使用,但在封闭环境内定义的变量)映射关联到这个闭包被创建时通过名称绑定的值或者引用

通俗一点,闭包就是一个能捕获上下文环境中变量(也就是自由变量)的方法。

block ,就是闭包(closure)在 Objective-C 中的实现。

可以使用 clang 将 OC 代码转换为 C++ 代码查看具体实现。

  • 在终端中输入代码 clang -rewrite-objc oc.m
  • 之后,在 oc.m 同一目录将生成一个 oc.cpp ,打开查看即可。

1001 定义与使用 Block

  1. 无参数无返回值
    void (^LyBlock_1)(void) = ^{
        NSLog(@"我是 “无参数无返回值” 的 block");  
    };
    LyBlock_1();
    复制代码
  2. 有参数无返回值
    void (^LyBlock_2)(int a) = ^(int a) { 
        NSLog(@"我是 “有参数无返回值” 的block,a = %d", a);
    };
    LyBlock_2(7);
    复制代码
  3. 有参数有返回值
    int (^LyBlock_3)(int a, int b) = ^(int a, int b) {
        NSLog(@"我是 “有参数有返回值” 的 block, a = %d, b = %d", a, b);
        return a + b;
    };
    LyBlock_13(7, 8);
    复制代码
  4. 无参数有返回值(很少用)
    int (^LyBlock_4)(void) = ^{
        NSLog(@"我是 “无参数有返回值” 的 block");
        return 7;
    };
    复制代码
  5. 使用 typedef 定义 block
    // 定义
    typedef int (^LySumBlock)(int a, int b);
    
    // 作为属性
    @property (nonatomic, copy) LySumBlock  sumBlock;
    
    // 使用
    self.block = ^int (int a, int b) {
        return a + b;
    }
    复制代码

1002 Block 的本质 ?

我们先创建一个 macOS - Command Line Tool 工程,并将 main.m 文件改写:

int main() {
    void (^block)(void) = ^{
        NSLog(@"block");
    };
    block();
    return 0;
}
复制代码

然后在终端中输入 clang -rewrite-objc main.m 将 main.m 转换为 C++ 代码,这时候在 main.m 同级目录下将生成一个 main.cpp。打开它,直接拉到最下边。

void (^block)(void) = ^{
    NSLog(@"block");  
};

// 被改写成

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
复制代码

断一下句:

(    (void (*)())    &    __main_block_impl_0(    (void *)__main_block_func_0, &__main_block_desc_0_DATA)    );
复制代码

block 被改写成一个名为 __main_block_impl_0 的构造方法了,这个构造方法传入了两个参数 (void *)__main_block_func_0&__main_block_desc_0_DATA 。然后往上翻一点。

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)};
复制代码

这不就是刚传递给 __main_block_impl_0 的第二个参数 &__main_block_desc_0 吗!继续往上。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main_4902a5_mi_0);
}
复制代码

这是刚传递给 __main_block_impl_0 的第一个参数 (void *)__main_block_func_0 !而且,从实现上看,这是我们在 block 内部写下的语句。 再往上一点,

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* 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;
  }
};
复制代码

原来:

  • __main_block_impl_0 是一个结构体;
  • impl : 类型为 struct __block_impl,是这个结构体具体实现的内容;
  • Desc : 类型为 struct __main_block_desc_0*,为这个结构体的描述;
  • __main_block_impl_0 : 一个构造方法,需要传入具体实现 fp,描述 desc,以及标记 flags (默认为 0)。

在网上翻一下,我们将找到:

struct __block_impl {
    void *isa;  // 指向所属类的指针,也就是block的类型
    int Flags;  // 标志变量,在实现block的内部操作时会用到
    int Reserved;   // 保留变量
    void *FuncPtr;  // block执行时调用的函数指针
};
复制代码

struct __block_impl 是有 isa 的。这也是个对象!!!(在 runtime 里,类与结构体都以结构体呈现)

全部结合起来看,定义 block 转换得来的这一句貌似就可以解释通了。

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
复制代码

构造了一个 struct __main_block_impl_0 实例,将 __main_block_func_0 作为具体实现的内容,__main_block_desc_0_DATA 作为描述。然后将这个 struct __main_block_impl_0 实例类型转换为 (void (*)()),接着赋值给 block

接着看 main 函数的下一句:

block()

// 被改写成

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

复制代码

也是做一下断句:

(    (void (*)(__block_impl *))    ((__block_impl *)block)->FuncPtr    )    ((__block_impl *)block);
复制代码

block 类型转换为 __block_impl *,然后取其 FuncPtr,也就是 block->FuncPtr (这个就是刚才实例化 struct __main_block_impl_0 时传入的 __main_block_func_0)。

然后将 block->FuncPtr 类型转换为 void (*)(__block_impl *)

接下来将 block 类型转换为 __block_impl *

最后调用 block->FuncPtr(block),这样看有点晦涩啊...

翻译一下:__main_block_func_0(block) 。这与 __main_block_func_0() 方法的定义基本符合

疑惑:将 block 转换类型的时候,两次都是转换成了 __block_impl * 了,而其实应该转成 __main_block_impl_0 * 不是吗?有没有大佬指点一下。。。不过这影响不大。

总结:Block 的本质就是一个结构体。Block 里的实现内容被改写成一个静态方法,然后在实例化 Block 结构体时传入并保存。使用 Block 时,拿出保存的静态方法调用即可。

1003 Block 如何捕获外界变量?

1003.1 捕获自动变量(其实就是 局部变量)

将 1002 的代码粘过来,建立 main_local.m:

int main() {

    int localA = 7;
    
    void(^block)(void) = ^{
        NSLog(@"block - %d", localA);
    };
    
    block();
    
    localA += 10;
    
    block();
    
    return 0;
}
复制代码

直接 clang -rewrite-objc main_local.m , 得到 main_local.cpp:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int localA;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int localA = __cself->localA; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main_local_d5ec0f_mi_0, localA);
    }

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 localA = 7;

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    localA += 10;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
复制代码

看一下与 1002 的不同:

  1. struct __main_block_impl_0 结构体的结构多了一个变量:localA ,同时它的构造函数也多了 localA ,这意味着这个结构体占用了更多的空间;
  2. __main_block_func_0 方法的实现先拿到了 __main_block_impl_0.a ,然后再执行操作;
  3. 根据这一行 int localA = __cself->localA; // bound by copy ,localA 通过拷贝与 struct __main_block_impl_0 绑定,意思是 localA 是拷贝过来的。那么得出结论:在外部改变 localA 不会影响到 block 内部。

证明一下 第三点不同:

同时,在 block 内部无法修改 localA ,这会直接引起编译器报错。

1003.2 捕获外部变量

修改 main_local.m 的代码,得到 main_global.m(添加一个外部的全局变量 globalB):

int globalB = 8;

int main() {

    int localA = 7;
    
    void(^block)(void) = ^{
        NSLog(@"block - %d - %d", localA, globalB);
    };
    
    block();
    
    localA += 10;
    globalB += 10;
    
    block();
    
    return 0;
}
复制代码

这次先上结果:

与 1003.1 对比一下:

  • 在 block 外修改 globalB 影响到了 block 内部;

盘它, clang -rewrite-objc main_global.m ,得到 main_global.cpp:

int globalB = 8;


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int localA;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int localA = __cself->localA; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main_global_8f2f2a_mi_0, localA, globalB);
    }

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 localA = 7;

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    localA += 10;
    globalB += 10;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
复制代码

仔细看一下 main_global.cpp :

struct __main_block_impl_0 这个结构体 与 1002 中的 结构体并且有任何区别,依然只有 localA 这个变量,而 globalB 还是外部的全局变量。

结论 : block 在与变量建立联系的时候,自由变量通过 值拷贝 建立绑定关联,而外面的全局变量则是通过 引用 建立绑定关联。

(此时,返回 1000 Block 到底是什么? 看一下那段英文 the value or reference to which the name was bound ,貌似就能理解了)

同时,我们尝试新建一个 main_global_modify.m(main 函数复制自 main_global.m),不过在 return 之前添加一个 blockModify 来修改一下 globalB :

int globalB = 8;

int main() {

    int localA = 7;
    
    void(^block)(void) = ^{
        NSLog(@"block - %d - %d", localA, globalB);
    };
    
    block();
    
    localA += 10;
    globalB += 10;
    
    block();
    
    void (^blockModify)(void) = ^{
        globalB += 100;
    };
    blockModify();
    
    block();
    
    return 0;
}
复制代码

运行一下:

这里就不贴 main_global_modify.cpp 的内容了,其实从 main_global.cpp 就可以想到,globalB 就是一个全局变量,它一直存在,block 与 blockModify 都只是建立了 globalB 的引用的绑定关联而已。

1003.3 捕获静态变量

新建 main_static.m,复制 main_global.m 中的内容,并新增一个局部静态变量和一个全局静态变量:

int globalB = 8;
static int globalStaticB = 10;

int main() {

    int localA = 7;
    static int localStaticB = 9;

    void(^block)(void) = ^{
        NSLog(@"block - %d - %d - %d - %d", localA, globalB, localStaticB, globalStaticB);
    };

    block();

    localA += 10;
    globalB += 10;
    localStaticB += 10;
    globalStaticB += 10;

    block();

    return 0;
}
复制代码

运行结果:

从运行结果可以看到:修改局部静态变量与全局静态变量都会影响到 block 内部的。

难道只要是静态变量,都是一样的吗?

盘它,clang -rewrite-objc main_static.m ,将 main_static.cpp 与 main_local.cpp 和 main_glocal.cpp (不过这次只看重点):

struct __main_block_impl_0 {
  // ...
  int *localStaticB;    // 这里是指针
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int *_localStaticB /*这也是指针*/, int flags=0) : localA(_localA), localStaticB(_localStaticB) {
    // ...
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int localA = __cself->localA; // bound by copy
  int *localStaticB = __cself->localStaticB; // bound by copy   // 这里是指针

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main_static_c79fdc_mi_0, localA, globalB, (*localStaticB) /* 这里是指针 */, globalStaticB /* 这里只引用 */);
    }

int main() {
    // ...
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA, &localStaticB /*这里是指针*/));
    // ...
}
复制代码

从 main_static.cpp 得出:

  1. 局部静态变量 localStaticB 确实是通过 来绑定关联的,但是建立关联的是 指针(int *)
  2. 全局静态变量 globalStaticB 与全局非静态变量 globalB 处理方式相同,建立引用关联(毕竟都是全局变量,一直存在);

1003.4 捕获 __block 修饰的变量

建立 main___block.m ,复制 main_local.m 内容,新增一个被 __block 修饰的 local__blockB 变量:

int main() {
    
    int localA = 7;
    __block int local__blockB = 8;
    
    void(^block)(void) = ^{
        NSLog(@"block - %d - %d", localA, local__blockB);
    };
    
    block();
    
    localA += 10;
    local__blockB += 10;
    
    block();
    
    return 0;
}
复制代码

自由变量 localA 并没有改变(还是 7),但是被 __block 修饰的 local__blockB 改变了(8 += 10 >>> 18)。

盘它, clang -rewrite-objc main___block.m ,查看 main___block.cpp :

struct __Block_byref_local__blockB_0 {
  void *__isa;
__Block_byref_local__blockB_0 *__forwarding;
 int __flags;
 int __size;
 int local__blockB;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int localA;
  __Block_byref_local__blockB_0 *local__blockB; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, __Block_byref_local__blockB_0 *_local__blockB, int flags=0) : localA(_localA), local__blockB(_local__blockB->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_local__blockB_0 *local__blockB = __cself->local__blockB; // bound by ref
  int localA = __cself->localA; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main___block_71facf_mi_0, localA, (local__blockB->__forwarding->local__blockB));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->local__blockB, (void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);}

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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main() {

    int localA = 7;
    __attribute__((__blocks__(byref))) __Block_byref_local__blockB_0 local__blockB = {(void*)0,(__Block_byref_local__blockB_0 *)&local__blockB, 0, sizeof(__Block_byref_local__blockB_0), 8};

    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA, (__Block_byref_local__blockB_0 *)&local__blockB, 570425344));

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    localA += 10;
    (local__blockB.__forwarding->local__blockB) += 10;

    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    return 0;
}
复制代码

被 __block 修饰的自由变量 local__blockB 竟然变成了一个结构体 struct __Block_byref_local__blockB_0

在实例化 struct __Block_byref_local__blockB_0 时,

// struct __Block_byref_local__blockB_0 实例化 local__blockB 
{
    __isa = (void *)0,
    __forwarding = (____Block_byref_local__blockB_0 *)&local__blockB,
    __flags = 0,
    __size = sizeof(__Block_byref_local__blockB_0),
    local__blockB = 8   // 这就是我们赋值给 local__blockB 的值 8
}
复制代码

在实例化 struct __main_block_impl_0 时,传入的是 (__Block_byref_local__blockB_0 *)&local__blockB ,很明显,这是指针拷贝。并且在 struct __main_block_impl_0 的构造函数中,赋值给 local__blockB 的是 _local__blockB->__forwarding

而且,struct __main_block_desc_0 较之前多了两个方法 copy()dispose()copy() 对应的是 __main_block_copy_0()dispose() 对应的是 __main_block_dispose_0()。暂时点到为止,等下再研究。

在执行 block 时,__main_block_func_0() 方法中,先从 struct __main_block_impl_0 拿出 __Block_byref_local__blockB_0 *local__blockB。然后,在读取值得时候,竟然绕了一个大弯:local__blockB->__forwarding->local__blockB。。。

同时,在修改 locak__blockB 的时候,也是绕了一个大弯 (local__blockB.__forwarding->local__blockB) += 10;

这到底是是怎么回事,为什么要这么麻烦,这就牵扯到 下一节 Block 的 copy 操作 了。

不过,先总结一下 block 对变量的捕获情况:

变量类型 是否捕获到 block 内部 访问方式
自由变量 值拷贝
静态变量 指针拷贝
全局变量 / 直接使用

1004 Block 的类型

为了研究 block 的 copy 操作,我们先要搞清楚 block 到底存储在栈上还是堆上???

这是 C/C++/Objctive-C 编译之后程序占用内存的分布结构(搬运自 iOS Block 详解):

常用级别的 Block 分为三类:

  1. _NSConcreteGlobalBlock : 全局 block,存储在全局内存中,相当于单例;
  2. _NSConcreteMallocBlock : 堆 block,存储在堆内存中,是一个带有引用计数的对象,需要自行管理器内存;
  3. _NSConcreteStackBlock : 栈 block,存储在栈内存中,超出作用域立马销毁。

这三种 block 各自的存储区域(搬运自 iOS Block 详解):

当然,还有三种系统级别( _NSConcreteAutoBlock_NSConcreteFinalizingBlock_NSConcreteWeakBlockVariable

简单来说,存储在哪片内存区域就是哪个类型的 block:

  1. 存储在全局区的 block 就是全局 block;
  2. 存储在堆区的 block 就是堆 block;
  3. 存储在栈区的 block 就是栈block。

如何判断 Block 存储在哪里

通过是否创建 Block 变量和是否访问变量,来研究一下:

情况一 不创建 Block 变量,不访问变量

结果:__NSGlobalBlock__

情况二 创建 Block 变量,不访问变量

结果:__NSGlobalBlock__

情况三 不创建 Block 变量,访问自由变量

结果:__NSStackBlock__

情况四 创建 Block 变量,访问自由变量

结果:__NSMallocBlock__

情况五 不创建 Block 变量,访问全局变量

结果:__NSGlobalBlock__

情况六 创建 Block 变量,访问全局变量

结果:__NSGlobalBlock__

总结一下:

是否创建 Block 变量 访问变量类型 Block 类型 备注
/ __NSGlobalBlock__
/ __NSGlobalBlock__
自由变量 __NSStackBlock__
自由变量 __NSMallocBlock__ __NSStackBlock__ 执行了 copy 操作
全局变量 __NSGlobalBlock__
全局变量 __NSGlobalBlock__

结论:

  1. 访问全局变量跟没有访问变量是相同的。原因:全局变量时直接引用,不存在捕获操作;
  2. 是否创建 Block 变量仅在访问自由变量时对 Block 类型有影响。原因:如果不创建 Block 变量,那么就不存在 copy 操作,使用完直接就释放掉了。

1005 Block 的 copy 操作

针对上一节的第二点结论,举一个例子:

typedef int (^TheMultiplyBlock)(int times);

TheMultiplyBlock buyMultiplyBlock(int base) {
    return ^(int times) {
        return base * times; 
    };
}
复制代码

此时,这个 Block 存储在栈中。在调用 buyMultiplyBlock() 方法返回时,该 Block 的作用域就结束了,Block 也会被丢弃。但是这就说不通了。所以在 ARC 环境下,编译器会自动完成 __NSStackBlock__ 的拷贝工作。至于 MRC 环境,就需要开发者调用 copy 方法手动拷贝了。

需要拷贝 Block 的时候,只需调用 [block copy] 即可。

  • 在 ARC 环境下,无需手动调用,编译器会自动完成拷贝工作。但是,我们也可以手动调用,而且不需要担心起内存问题。

    这样也没有问题 : theBlock = [[[[block copy] copy] copy] copy];

  • 在 MRC 环境下,需要手动调用 copy 方法,并且需要我们自己管理其释放工作(引用计数)。

    [block copy]; [block release];

Block 的拷贝操作执行的是 Block 的实例方法 copy 。不同类型的 Block 使用 kopy 的效果也不同:

Block 类型 Block 源的存储区域 拷贝效果
_NSConcreteGlobalBlock 全局区 什么也不做
_NSConcreteMallocBlock 堆区 引用计数 + 1
_NSConcreteStackBlock 栈区 从栈拷贝到堆

Block_copy()

研究一下 Apple 开源代码 BlocksRuntime/Block.h 吧:

BLOCK_EXPORT void *_Block_copy(const void *aBlock);

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
复制代码

可以看到 Block_copy() 完全是一个宏 #define 。其参数为 void * ,并且它最终实现为 _Block_copy()

继续深入,在 Apple 开源代码 BlocksRuntime/runtime.c 里:

void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}
复制代码

盘它(简化一下):

/* 拷贝 Block,或者增加 Block 的引用计数。若需要拷贝,调用拷贝协助方法(如果存在) */
static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;

    ///Junes 1、若不存在源 Block ,则返回 NULL
    if (!arg) return NULL;
    
    ///Junes 2、将源 Block 指针转换为 (struct Block_layout *)
    aBlock = (struct Block_layout *)arg;
    
    ///Junes 3、若源 Block 的 flags 包含 BLOCK_IS_GC,则其为堆块。 \
    /// 此时增加其引用计数,并返回这个源 Block
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    
     ///Junes 4、源 Block 是全局块,直接返回源 Block(全局 Block 就是一个单例)
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    ///Junes 5、源 Block 是一个栈 Block,执行拷贝操作。首先申请相同大小的内存
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;
    
    ///Junes 6、使用 memmove 方法将栈区里的源 Block 逐位复制到刚申请的堆区 Block 内存中。这样做是为了保证完全复制所有元数据。
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    
    ///Junes 7、更新 result 的 flags。
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed    ///Junes 确保引用计数为 0。注释表示没这个必要,可能因为此时引用计数早已为 0。但是为了防止 bug 被保留下来。
    result->flags |= BLOCK_NEEDS_FREE | 1;  ///Junes 为 result 的 flags 添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1。表明这是一个堆 Block(一旦引用计数降为 0,则其内存将被回收)
    
    ///Junes 8、将 result 的 isa 指向 _NSConcreteMallocBlock。这意味着 result 是一个堆 Block。
    result->isa = _NSConcreteMallocBlock;
    
    ///Junes 9、如果 result 存在拷贝协助方法,调用它。
    /// 如果 block 捕获对象,编译器将会生成这个协助方法。
    /// 这个协助方法将会 retain 被捕获的对象。
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); 
    }
    
    return result;
}
复制代码

注释其实已经写得蛮清楚了。但也可以总结一下:

  1. 如果源 Block 不存在,返回 NULL;
  2. 如果源 Block 是 _NSConcreteMallocBlock,增加其引用计数,然后返回源 Block;
  3. 如果源 Block 是 _NSConcreteGlobalBlock,直接返回源 Block;
  4. 如果源 Block 是 _NSConcreteStackBlock:

    (1) 申请相同大小的内存;

    (2) 拷贝源 Block 的所有元数据到刚申请的内存;

    (3) 更新 result 的 flags,确保其引用计数为 0;

    (4) 更新 result 的 flags,添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1;

    (5) 将 result 的 isa 指向 _NSConcreteMallocBlock。标明 result 是一个堆 Block;

    (6) 如果 result 捕获了对象,调用编译器生成的拷贝协助方法 retain 被捕获的对象。

解释一下为什么去掉了 GC 相关的代码。在 runtime.c 中研究:可以发现 GC 是一种与 objc-auto 完全不同的模式。也就是说,这是一种不会用到的模式(当然,你也可以从 ObjC runtime 或者 CoreFoundation 中调用方法 _Block_use_GC 来从 objc-auto 切换到 GC 模式):

如何拷贝被捕获的变量

在自由变量作用域结束之后还能访问,这证明我们访问的这个自由变量肯定不是栈里原本的那个变量了。

只有一种解释,这个变量被拷贝到堆区了。、

前边的实例其实已经明白了普通类型的 自由变量,静态变量,全局变量(不需要拷贝,直接访问)的 拷贝操作了。那么 __block 修饰的变量、对象类型如何拷贝呢?

回到刚才的 暂时点到为止,等下再研究

__block 变量的 copy ,也就是 __main_block_copy_0

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
    _Block_object_assign((void*)&dst->local__blockB, (void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

现在 Apple 开源 BlocksRuntime/Block_private.h 了解一下这些枚举:

///Junes _Block_object_assign() 与 _Block_object_dispose() 中参数的值
enum {
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...  ///Junes OC 对象类型,如前边这几种
    BLOCK_FIELD_IS_BLOCK    =  7,  ///Junes 一个 Block 变量
    BLOCK_FIELD_IS_BYREF    =  8,  ///Junes 栈区中被 __block 修饰变量后产生的结构体
    BLOCK_FIELD_IS_WEAK     = 16,  ///Junes __weak 修饰的变量,只在 Block_byref 管理内部对象时使用,即 __block __weak id;
    BLOCK_BYREF_CALLER      = 128, ///Junes 在处理 Block_byref 内部对象内存时加上的额外标记,配合上边的几个枚举值一起使用
};

enum {
    ///Junes 以上所有枚举值的结合
    BLOCK_ALL_COPY_DISPOSE_FLAGS = 
        BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_BYREF |
        BLOCK_FIELD_IS_WEAK | BLOCK_BYREF_CALLER
};
复制代码

Apple 开源 BlocksRuntime/runtime.c ,可以找到 _Block_object_assign

/*
 * Block 对象由栈区拷贝至堆区时,如果 Block 持有对象,则执行
*/
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
    ///Junes 1、使用结构体内部的 copy / dispose 辅助方法手动管理,内部无需再次 retain, 直接指针赋值即可。
    if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {
        ///Junes 弱引用
        if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) {
            *destAddr = object; // _Block_assign_weak(object, destAddr);
        }
        ///Junes 强引用
        else {
            *destAddr = object; // _Block_assign((void *)object, destAddr);
        }
    }
    ///Junes 2、拷贝目标是由 __block 修饰变量而生成的二结构体
    else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {
        // 如果持有弱引用或需要一个特殊的 isa 时,flags 会表明这些
        _Block_byref_assign_copy(destAddr, object, flags);
    }
    ///Junes 3、拷贝目标是一个 Block。此 if 分支必须先于之后的 BLOCK_FIELD_IS_OBJECT(所有 Block 都是对象)
    else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK) {
        ///Junes 从栈区将 Block 捕获的自由变量至堆区
        *destAddr = _Block_copy(object); // _Block_assign(_Block_copy_internal(object, flags), destAddr);
    }
    ///Junes 4、拷贝目标是一个对象。此 if 分支必须晚于 之前的 BLOCK_FIELD_IS_BLOCK
    else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) {
        ///Junes 引用计数 +1
        _Block_retain_object(object);
        ///Junes 将原来的对象的指针也指向这个拷贝得来的新对象
        *destAddr = object; //_Block_assign((void *)object, destAddr);
    }
}
复制代码

_Block_assign_weak : 直接指针赋值

static void (*_Block_assign_weak)(const void *dest, void *ptr) = _Block_assign_weak_default;

static void _Block_assign_weak_default(const void *ptr, void *dest) {
    *(void **)dest = (void *)ptr;
}
复制代码

_Block_assign : 直接指针赋值

static void (*_Block_assign)(void *value, void **destptr) = _Block_assign_default;

static void _Block_assign_default(void *value, void **destptr) {
    *destptr = value;
}
复制代码

_Block_byref_assign_copy

static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2;

/*
 * 当拷贝目标为 __block 修饰变量而生成的结构体时,则执行
*/
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;
        
    ///Junes __block 变量结构体还在栈区,拷贝它
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        ///Junes 判断这是否为一个弱引用
        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
        ///Junes 申请相同大小的空间
        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
        ///Junes 将新结构体标记为堆,并将引用计数置为 2。一份给调用者,一份给栈。
        copy->flags = src->flags | _Byref_flag_initial_value;
        ///Juens 将栈区结构体与新结构体的 __forwarding 都之上堆区中的新结构体
        copy->forwarding = copy; 
        src->forwarding = copy; 
        ///Junes 赋值 size
        copy->size = src->size;
        
        ///Junes 如果是弱引用,isa 指向 _NSConcreteWeakBlockVariable。标记为 Block 的弱引用
        if (isWeak) {
            copy->isa = &_NSConcreteWeakBlockVariable;  
        }
        
        ///Junes 如果存在 copy_dispose 内存管理方法,执行
        if (src->flags & BLOCK_HAS_COPY_DISPOSE) {
            ///Junes 将新结构体的内存管理方法指针指向全区源结构体的相应方法
            copy->byref_keep = src->byref_keep;
            copy->byref_destroy = src->byref_destroy;
            ///Junes 调用源结构体的 byref_keep 方法(也就是 _Block_object_assign),管理被捕获的对象内存。不过会加上 BLOCK_BYREF_CALLER 标记
            (*src->byref_keep)(copy, src);
        }
        else {
            ///Junes 仅适用于普通变量(非对象),全字节拷贝 byref_keep
            _Block_memmove(
                (void *)&copy->byref_keep,
                (void *)&src->byref_keep,
                src->size - sizeof(struct Block_byref_header));
        }
    }
    ///Junes 这个结构体已经在堆区,引用计数 +1
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // assign byref data block pointer into new Block
    ///Junes 将源结构体指针也指向堆区的这个新结构体
    *destp = src->forwarding;  // _Block_assign(src->forwarding, (void **)destp);
}
复制代码

_Block_memmove : 全字节拷贝

static void (*_Block_memmove)(void *dest, void *src, unsigned long size) = _Block_memmove_default;

static void _Block_memmove_default(void *dst, void *src, unsigned long size) {
    memmove(dst, src, (size_t)size);
}
复制代码

总结一下:

对象类型 拷贝方式 备注
操作对象内存 BLOCK_BYREF_CALLER 直接指针拷贝 *dest = object 有着就以这个工作
__weak BLOCK_FIELD_IS_WEAK 直接指针拷贝 *dest = object 有着就以这个工作
Block 实例 BLOCK_FIELD_IS_BLOCK 通过 _Block_copy() 复制 所有 Block 都是对象,一定先判定这个
OC 对象 BLOCK_FIELD_IS_OBJECT 引用计数 +1
__block 结构体 BLOCK_FIELD_ISBYREF 通过 _Block_byref_assign_copy() 复制该结构体

Block_release()

做戏做全套。拷贝一次,引用计数增加了。当引用计数降为 0 的时候,得释放了。

还是在 Apple 开源代码 BlocksRuntime/Block.h 吧:

#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
复制代码

继续研究:

static void (*_Block_deallocator)(const void *) = (void (*)(const void *))free;


void _Block_release(void *arg) {
    ///Junes 1、将 Block 从 void * 转换为 (struct Block_layout *)。 
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;
    
    ///Junes 2、将引用计数 -1
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
    
    ///Junes 3、如果引用计数大于 0,不做处理
    if (newCount > 0) return;
    
    ///Junes 4、如果引用计数为 0,但是它是一个堆 Block(其 flags 包括 BLOCK_NEEDS_FREE),此时这个 Block 应该被释放。
    ///Junes 第一步,调用与拷贝协助方法(copy helper)相反的方法 dispose helper,release 其 retain 的对象。
    ///Junes 最后,调用 _Block_deallocator 释放这个 Block。_Block_deallocator 其实就是 free。
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        free(aBlock); // _Block_deallocator(aBlock);
    }
    
    ///Junes 5、如果是 _NSConcreteGlobalBlock,不做任何操作
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }
    
    ///Junes 6、如果到了这里,那么就是在释放栈 Block 的时候发生了某些神奇的事情。打印一条 log 来警告开发者。但事实上,不会到这里的。
    else {
        printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
    }
}
复制代码

例行总结一下:

1、引用计数 -1; 2、若引用计数扔大于 0,不做任何操作;若等于 0,则执行判断 Block 类型按需执行操作;

Block 类型 执行操作 备注
_NSConcreteGlobalBlock 不做操作 /
_NSConcreteMallocBlock free(aBlock) 若存在 copy_dispose 方法,执行 dispose 辅助方法释放其捕获的对象
_NSConcreteStackBlock / 不会到这里的

如何释放被捕获的变量

既然 Block 都释放了,那么其捕获的变量也得释放啊。

就是这个了 :

static void __main_block_dispose_0(struct __main_block_impl_0*src) 
{
    _Block_object_dispose((void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

再次进入 Apple 开源 BlocksRuntime/runtime.c

void _Block_object_dispose(const void *object, const int flags) {
    ///Junes 如果需要释放的是由 __block 修饰变量生成的结构体
    if (flags & BLOCK_FIELD_IS_BYREF)  {
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
    }
    ///Junes 如果需要释放的是 Block 
    else if ((flags & (BLOCK_FIELD_IS_BLOCK|BLOCK_BYREF_CALLER)) == BLOCK_FIELD_IS_BLOCK) {
        _Block_destroy(object);
    }
    ///Junes 如果需要释放的是对象
    else if ((flags & (BLOCK_FIELD_IS_WEAK|BLOCK_FIELD_IS_BLOCK|BLOCK_BYREF_CALLER)) == BLOCK_FIELD_IS_OBJECT) {
        _Block_release_object(object);
    }
}
复制代码

逐个查看:

如果是 __block 结构体,执行 _Block_byref_release()

static void _Block_byref_release(const void *arg) {
    ///Junes 1、将需要释放的 __block 结构体强转为 (struct Block_byref *)
    struct Block_byref *shared_struct = (struct Block_byref *)arg;
    int refcount;

    ///Junes 根据 __forwarding 获取真实需要释放的实例
    shared_struct = shared_struct->forwarding;
    
    ///Junes 为了在 GC 下支持 C++ 析构器,通过 isa 指针将代码重定向到 byref_destory() 方法来执行终结操作
    if ((shared_struct->flags & BLOCK_NEEDS_FREE) == 0) {
        return; // stack or GC or global
    }
    
    ///Junes 获取引用计数
    refcount = shared_struct->flags & BLOCK_REFCOUNT_MASK;
    
    ///Junes 如果引用计数为 0,打印 Log 警告开发人员
    if (refcount <= 0) {
        printf("_Block_byref_release: Block byref data structure at %p underflowed\n", arg);
    }
    ///Junes 引用计数 -1,如果此时引用计数为 0,释放它
    else if ((latching_decr_int(&shared_struct->flags) & BLOCK_REFCOUNT_MASK) == 0) {
        ///Junes 如果该 __block 结构体存在 copy_dispose 辅助方法,执行 byref_destroy() 方法管理其捕获的变量内存
        if (shared_struct->flags & BLOCK_HAS_COPY_DISPOSE) {
            //printf("calling out to helper\n");
            (*shared_struct->byref_destroy)(shared_struct);
        }
        
        ///Junes 释放这个 __block 结构体
        free(shared_struct); // _Block_deallocator((struct Block_layout *)shared_struct);
    }
}
复制代码

如果是 Block,执行 _Block_destroy

static void _Block_destroy(const void *arg) {
    struct Block_layout *aBlock;
    if (!arg) return;
    ///Junes 强转 arg 为 struct Block_layout *
    aBlock = (struct Block_layout *)arg;
    ///Junes 如果在 GC 环境下,不做任何操作
    if (aBlock->flags & BLOCK_IS_GC) {
        // assert(aBlock->Block_flags & BLOCK_HAS_CTOR);
        return; // ignore, we are being called because of a DTOR
    }
    ///Junes 执行 _Block_release() 方法销毁这个 Block
    _Block_release(aBlock);
}
复制代码

如果是 OC 对象,执行 _Block_release_object

看看这里,也就是执行 [object release]

void _Block_use_RR( void (*retain)(const void *),
                    void (*release)(const void *)) {
    _Block_retain_object = retain;
    _Block_release_object = release;
}
复制代码

例行总结:

对象类型
__block 结构体 引用计数 -1,若为 0,释放它 在释放之前,若存在 copy_dispose 辅助方法,执行 dispose() 来管理其捕获的变量内存
Block 对象 执行 _Block_release() 方法来释放 /
OC 对象 引用计数 -1,[object release] /

1006 __block 与 __forwarding

刚刚是不是跑偏了~~~

终于到重头戏了,想想还有点小激动~~~

如果一个 Block 捕获了自由变量,那么这个 Block 就会被编译器 copy 到堆区。从一个 _NSConcreteStackBlock 变为一个 _NSConcreteMallocBlock。

被 __block 标记的自由变量 local__blockB,Block 并不是简单的值拷贝,而是拷贝了 local__blockB 这个结构体(自动被重写成 sturct __Block_byref_local__blockB_0 的一个实例,而 __Block_byref_local__blockB_0.local__blockB 承载之前的自由变量 a)的指针。

是想一下,这个自由变量 local__blockB 在其作用域结束之后就不存在了,Block 就无法访问了,但是 Block 又需要访问 local__blockB ,这是一个悖论。所以编译器在拷贝 Block 的时候,也会将这个生成的结构体(__Block_byref_local__blockB_0)也拷贝到堆区。

那么问题就来了,此时访问 local__blockB 到底是要访问栈上的 local__blockB 还是堆上的 local__blockB 呢?于是, __forwading 登场了。

借用一下 大佬的图

通过 __forwading ,无论在 Block 中还是在 Block 外访问 __block 变量,也无论该变量在栈中还是在堆中,都能准确地访问到同一个变量。

所以才会存在绕一大圈的访问方法:

(local__blockB.__forwarding->local__blockB) += 10;
复制代码

1007 Block 容易造成的问题 - 内存泄漏

在拷贝 Block 的时候,我们提到,也会将其捕获的对象一并拷贝,尤其对于使用极为频繁的 OC 对象而言(id、NSObject、Block、__attribute))((NSObject))),其拷贝手段为 引用计数 +1。拷贝的时候确实 +1 了,但如果在其该释放的时候没有 -1,那么就会造成内存泄漏。

比如这个例子,一个类将 Block 作为自己的属性,然后这个 Block 内部又使用了该类本身 self:

self.handsomeBlock = ^{
    [self doSomething];
};
复制代码

在这个例子中,self 强引用了 handsomeBlock,而 handsomeBlock 内部又通过强引用捕获了 self。这就造成了循环引用。在该释放的时候,其引用计数都无法 -1,导致都无法释放,从而造成内存泄漏。

解决办法:

1、MRC 环境,使用 __block__unsafe_unretained

__block typeof(self) blockSelf = self;
self.handsomeBlock = ^{
    [blockSelf domomething];
};
复制代码
__unsafe_unretained id weakSelf = self;
self.handsomeBLock = ^{
    [weakSelf dosomething];
}
复制代码

1、ARC 环境,使用 __weak

__weak typeof(self) weakSelf = self;
self.handsomeBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf domomething];
};
复制代码

至于为什么使用 __strong ,这是为了防止 __weak 变量被提前释放,不过 self 不存在提前释放这种问题,但如果不是 self ,那可就不一定了。

参考链接

Closure - 维基百科

iOS Block 详解

iOS爱上底层-Block实现与原理

A look inside blocks: Episode 1

A look inside blocks: Episode 2

A look inside blocks: Episode 3 (Block_copy)

Apple 开源 BlocksRuntime/Block.h

Apple 开源 BlocksRuntime/runtime.c

Apple 开源 BlocksRuntime/Block_private.h

关于Block再啰嗦几句

LLVM Download Page

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改