本文主要写了为什么会这么设计, 该怎么去想这个问题. 可能没有其它文章那么详细的去写实现细节, 但如果知道了为什么会这样设计, 用法的东西应该很快就能通的.
为什么会有 Block?
首先要理解的是闭包, 闭包是引用了自由变量的函数. 当然, 这样的话, 就有挺多问题需要考虑的:
如果是基础类型数据, 并且里面没有修改, 那么最简单的就是值拷贝了.
如果在函数里面, 需要修改外部指针变量, 这时候就需要传递地址才可以修改指针变量了.
如果在里面, 引用了对象, 这里就比较复杂一些了. 我们需要在方法里也能跑, 正常的话, 会想到增加引用计数, 这样就能保活往下跑了. 然后为什么会出现循环引用, 内存泄漏的情况呢? 是不是循环引用就会内存泄漏呢? 其实不是这样的, 如果有一方能解开这个环也是可以的. 其实如果 block 保存在栈上的话, 只要出了作用域之后会被释放. 所以这种情况基本上是不存在内存泄漏问题. 但是我们为什么要拷贝到堆上呢? 就要想一下刚才那个问题, 出了作用域就会被释放, 那么如果我在另外一个方法, 才对这个 block 执行呢? 这样的话, 就只能拷贝到堆上.
大概说一下, 三种 block 的情况. 第一种: 仅引用了全局变量或者 static 静态变量的, 就是存在 data 区域的_NSConcreteGlobalBlock, 这种不需要我们自己释放了, 生命周期和应用一样. 第二种: 除了第一种说的, 初始化的时候, 都是_NSConcreteStackBlock. 第三种: 注意我们是无法生成_NSConcreteMallocBlock 的, 只有在_NSConcreteStackBlock 调用__Block_copy 时才会被 copy 到堆上, 生成_NSConcreteMallocBlock.
想想为什么会是上面这样的结果, 我们都知道在方法里面定义的变量, 出了作用域就销毁. 就很符合上面第二种类型. 至于第三种类型, 其实就是希望这个 block 在其它地方也能跑, 如果对堆栈比较熟悉的话, 就会想用堆.
其实这三种情况, 什么时候是怎么样的, 大概就清晰了.
Block 的内部结构
其实在 iOS 中, 对象归根到底, 其实也就是 c 数据和 c 方法组成. 那么最简单的区别, 就是数据和方法了. 那么最容易想到的就是存储引用数据和实现方法了. 由于我们需要从栈拷贝到堆, 所以还需要知道 block 的大小. 由于 Block 里面可能会引用对象, 那么还需要考虑内存管理.
Block 的拷贝:
_NSConcreteGlobalBlock
直接返回不做处理,
_NSConcreteStackBlock
会从栈拷贝到堆,
_NSConcreteMallocBlock
对这种类型再拷贝, 引用计数加一.
Block 中变量的拷贝方法
_Block_object_assign
:1. 对象的话, 会调用_Block_retain_object 方法会被赋值为 retain 操作(需要注意的是在 ARC 环境是是什么都不做的, 因为 ARC 环境下有更成熟的内存管理)2. 引用对象是 block 则进行递归对 Block 进行 copy.3. 如果是__block 变量的话, 如果是 id 或者 block 类型, 会进行简单赋值.
由于我们知道, 循环引用会有可能会导致内存泄漏. 为了避免循环引用的问题,__block 修饰的变量在 MRC 中可以避免 retain 操作, 这是因为该变量会被打包成 Block_byref 类型的结构体. 所以该变量在被引用的就可以避免调用_Block_retain_object, 而是调用了另外一个方法
__Block_byref_id_object_copy
, 仅做了赋值操作.
Block 记法
Block 语法该怎么记?
可以记为 ^ 后面带有 c 语言的方法,^ void (int a){}
Block 类型变量怎么记?
函数指针的 * 改 ^. 看看函数指针
- int func(int count){
- return count +1;
- }
- int (*funcptr)(int) = &func;
所以变量申明赋值就是这样的
int (^blk)(int) = ^(int count){return count + 1};
必须搞清楚的一些东西
char *a 与 char a[] 的区别 (char *a 的内容是在常量区, char a[] 的内容在栈区) http://www.cnblogs.com/kaituorensheng/archive/2012/10/23/2736069.html
想修改变量的值, 需要添加__block.Block 不能截获 c 语言的数组, 但是可以截获 char * 指针. 编译器会将 OC 方法转为 C/C++ 方法来处理.
- [self SendImage:fileName];
- // 上面转换如下
- void (*action)(id, SEL, NSString*) = (void (*)(id, SEL, NSString*))objc_msgSend;
- action(self, @selector(SendImage:), fileName);
在 c++ 里面, 结构体也有构造函数和构造函数
- #include <stdio.h>
- struct ClassBook{
- int age;
- int number;
- int lala;
- ~ClassBook(){
- printf("被自动被释放了");
- };
- ClassBook(int _age,int _number):age(_age),number(_number)
- {
- };
- };
- void hello(){
- ClassBook book = {23,11};
- ClassBook *temp = &book;
- int *test = (int *)temp;
- for (int i=0; i<3; i++) {
- printf("第 %d 个数是:%d\n",i,*test);
- test++;
- };
- printf("%d\n",sizeof(ClassBook));
- printf("%d\n",temp->age);
- }
- int main(){
- hello();
- return 0;
- }
运行结果
第 0 个数是: 23
第 1 个数是: 11
第 2 个数是: 0
12
23
被自动被释放了
根据上面结果, 我们需要知道结构体和指针的访问方式, 静态创建结构体, 除了分配在栈上的结构体, 除了作用域就会被释放.
堆上的内容需要我们释放, 也就是 clloc 或者 new 来动态申请内存的必须释放.
如果想彻底搞懂的话, 请深入了解堆栈 http://www.cnblogs.com/edisonchou/p/4669098.html , 还有 ARM 架构下汇编在栈的调用情况 http://www.cnblogs.com/csutanyu/p/3575297.html . 否则, 你只需知道栈不需要释放, 堆上需要释放即可.
需要理解到 Block 本质也是一个对象, 对象的本质是一个指向堆上的地址的结构体指针. alloc 一个对象, 在 OC 里面, 最终是调用了 calloc 方法生成, 就是分配了一个堆上的地址.
堆和栈必须要有比较深刻的理解.
搞清楚全局区和堆区,(指针变量)是所指向的地址, 就是保存的地址,(& 指针变量)是指针变量所在的地址,(* 指针变量)是所指向的地址的内容. 这些东西必须记牢. 根据下面理解一下.
- #include <stdio.h>
- #include <stdlib.h>
- int *pGlobal;// 指针 pGlobal 在全局区(静态区)
- void demo1(){
- printf("全局指针现在的位置 %p\n",&pGlobal);// 全局区
- printf("全局指针存储的内容 %p\n",pGlobal);// 全局变量都会初始化为 0
- pGlobal = (int *)malloc(sizeof(int)*20);// 把分配一个堆区的内容的地址赋值给全局区的 pGlobal 的内容
- printf("全局指针现在的位置 %p\n",&pGlobal);// 当然还是全局区, 编译时就已经决定了
- printf("全局指针存储的内容 %p 这个 pGlobal 存储的地址其实现在是在堆区的地址了 \ n",pGlobal);//pGlobal 保存了一个堆区的地址
- printf("---------------------------------------------------------\n");
- };
- int a = 3;// 变量是全局区 (静态区) 内容: 常量区
- void demo2(){
- printf("%p 常量区 \ n",a);// 常量区
- printf("%p 全局区 \ n",&a);// 全局区
- int *p = &a;//3 所在的地址
- a = 5;
- printf("%p 既然是 3 的地址, 那么肯定就是全局区啦 \ n",p);// 全局区
- printf("%p p 是局部变量, 那么现在存放位置肯定就是栈区啦 \ n",&p);// 全局区
- printf("---------------------------------------------------------\n");
- }
- void demo3(){
- int temp = 3;// 变量和内容都是在栈区
- int temp2 = 44444;// 变量和内容都是在栈区
- printf("%p \n",&temp);// 栈区
- printf("%p 栈区 \ n",&temp2);// 栈区
- }
- int main(){
- // 搞清楚全局区和堆区,(指针变量)是所指向的地址, 就是保存的地址,(& 指针变量)是指针变量所在的地址,(* 指针变量)是所指向的地址的内容. 这些东西必须记牢.
- demo1();
- demo2();
- demo3();
- return 0;
- }
运行结果
全局指针现在的位置 0x100001030
全局指针存储的内容 0x0
全局指针现在的位置 0x100001030
全局指针存储的内容 0x102196bc0 这个 pGlobal 存储的地址其实现在是在堆区的地址了
---------------------------------------------------------
0x3 常量区
0x100001028 全局区
0x100001028 既然是 3 的地址, 那么肯定就是全局区啦
0x7ffeefbff668 p 是局部变量, 那么现在存放位置肯定就是栈区啦
- ---------------------------------------------------------
- 0x7ffeefbff66c
0x7ffeefbff668 栈区
Program ended with exit code: 0
clang 转把 OC 代码转写成 c/c++ 代码
把 OC 代码转写成 c/c++ 代码, 执行
clang -rewrite-objc block1.c
, 得到 block 的结构体. 附上: 可能用到指令
clang -rewrite-objc -fobjc-arc -Wno-deprecated-declarations block1.c
- //block1.c 代码
- #include <stdio.h>
- int main(){
- int localNumber = 4;
- char *format = (char *)"%d";
- void (^blk)(void) = ^{
- printf(format,localNumber);
- };
- blk();
- return 0;
- }
其实如果熟悉 c 语言和 c++ 的话, 阅读下面这些东西, 应该是无障碍的, 当然至少得搞清楚结构体和构造方法. 如果不清楚, 建议先看完 c 和 c++ 的基础, 后面也不会讲解太多这部分基础.
- 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;
- char *format;
- int localNumber;
- __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_format, int _localNumber, int flags=0) : format(_format), localNumber(_localNumber) {
- impl.isa = &_NSConcreteStackBlock;
- impl.Flags = flags;
- impl.FuncPtr = fp;
- Desc = desc;
- }
- };
- static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
- char *format = __cself->format; // bound by copy
- int localNumber = __cself->localNumber; // bound by copy
- printf(format,localNumber);
- }
- 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 localNumber = 4;
- char *format = (char *)"%d";
- void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, format, localNumber));
- ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
- return 0;
- }
Block 结构体的话, 应该没有太多可以说的. 就是把变量, 大小, 实际调用函数, isa 所属类, 还有一些标识往里塞就是了. 方法的调用就是这个结构体里面实际调用的方法, 参数就是 Block 结构体本身.
不过看到 isa, 应该第一反应就是其实 Block 本质也是对象. 不清楚的话, 看看 iOS Runtime ---- 元类.
- struct __block_impl {
- void *isa;
- };
我们先来理解一下传值和方法的调用差异
没有__block 修饰的变量传值, 以及修改情况(也就是 Block 被调用情况).
- // 假设 Block 要截获的是如下的数据, 并且下面都是局部变量
- int localNumber = 4;
- char *format = (char *)"%d";
上面这种数据最直接的值传递, 为什么只能打印却无法修改? 其实 Block 截获这些数据的时候, 都是值传递. 也就是说, 你把 4 传递了给 Block 里面的变量, 那么你对变量修改的话, 就应该是不被允许的, 也无法修改到原来的变量(因为你只能修改到 Block 保存的变量啊). 第二种有点特殊, 其实这个是字符串的地址的传递, 其实可以想象一下, 你把内存的地址赋值给了 Block 的指针. 你能修改原来的指针吗? 不行. 所以也是不被允许的.
对于全局变量和静态全局变量, 静态局部变量来说, 却有所不同. 对于全局变量和静态全局变量来说, 在源代码转换后, 仍然为全局变量和静态全局变量. 而静态局部变量, 则变成 int 指针. 传递的时候, 也是传递静态局部变量的地址.
static int static_value = 3;
实际传值为 & static_value, 而方法调用赋值方式为(*static_value )=6;
下面开始是__block 修饰的变量
- __block int val = 10;
- // 编译后, 变换为下面
- struct __Block_byref_val_0 {
- void *__isa;
- __Block_byref_val_0 *__forwarding;
- int __flags;
- int __size;
- int val;
- };
- // 实际上变成了结构体实例
- __Block_byref_val_0 val = {0,&val, 0, sizeof(__Block_byref_val_0), 10};
而赋值到 Block 结构体又是怎么样的呢?
- __Block_byref_val_0 *val = __cself->val; // bound by ref
- (val->__forwarding->val) = 5;
其实 val->__forwarding->val 就是原自动变量, 并提供了__Block_byref_val_0 的一对内存管理方法. 为什么__Block_byref_val_0 会有__forwarding 并指向自己? 其实是为了从栈上复制到堆上的时候, 让栈上的__forwarding 指向堆上的, 这样就可以访问同一个变量.
为什么我们需要从栈拷贝到堆?
最简单的理解就是, 我们想出了作用域也能访问
对 Block 进行 copy 的所有情况:
如果是全局 Block, 那么直接返回
如果是堆 Block, 那么引用计数增加
如果是栈 Block, 那么从栈复制到堆, 这个情况是需要调用 copy 的
为什么我们需要进行额外的内存管理?
其实我们主要也是考虑从栈复制到堆的情况. 当然除了拷贝 Block, 捕获的变量需要考虑吗? 需要的, 因为我们拷贝到堆, 就是为了出了作用域还能用. 如果我们对于捕获的变量也只是值拷贝的话. 出了作用域, 就会发生悬垂指针了. 所以我们也需要把__block 的栈变量也拷贝到堆. 所以我们需要一个拷贝变量的方法. 也就是说捕获的变量为 id 类型变量和__block 修饰的变量, 则需要调用 Block_descriptor_2->copy()方法(其实就是内存管理的方法, id 对象的话, 为了出了作用域还能访问, 你至少得 retain 操作对吧? 如果是__block 对象, 也是为了出了作用域还能访问, 那么你就得把__block 修饰的结构体拷贝到堆上, 那么肯定需要管理啦), 该方法会调用到_Block_object_assign 方法(管理捕获变量的内存).
如果调用_Block_object_assign 的结构体是个 BLOCK_FIELD_IS_BYREF(也就是被__block 的变量并且捕捉的对象是个 id 类型)的类型, 那么除了会从栈上拷贝到堆上, 还会调用
__Block_byref_obj2_1
上的
__Block_byref_id_object_copy
方法. 其实上面一部分的原理是一样的, 就不再重复了, 所以你需要再做一次的内存管理, 最终调用的还是_Block_object_assign 方法.
除了前面讲的, 我们最常见的捕获自由变量的情况分析:
id 类型, 不带__block
在 MRC 下, 由于 Block 保存的也是 id, 也就是存储的是一个地址, 你修改了 Block 里面的变量的地址, 当然影响不了原来的 id. 但是可以根据这个地址找到对应的对象, 从而进行一些操作. 简单来说, 就是不能修改对象, 但是如果你这个对象是个 NSArray 类型, 但是可以做添加元素的操作. 有悬垂指针的尴尬.
在 ARC 下, 和上面的情况差不多, 但是执行期间对象不会突然变为悬垂指针, 因为 ARC 下默认就是__strong 的修饰符.
id,NSString * 类型, 带__block
这种情况, 为什么就可以修改了呢? 这是因为被__block 修饰的变量, 其实是个存有 id 类型的结构体. 而我们访问的数据, 也是这个结构体里面的 id 对象, 其实我们所捕获的自由变量. 那么如果你是把这样结构体的 id 存储的地址修改了, 那么就是相当于修改了外部的.
也就是说, 如果你在 block 再去访问这个 id 对象的时候, 实际上就是通过结构体里面的 id 对象, 编译器会自动转换的.
内存管理, 看看能不能考虑一下.
int,int * 类型, 不带__block
其实都是简单的值传递, 只是指针变量传的是地址, 而普通变量传的是数值. 这种只是简单的值传递. Block 的结构体会保存着他们的值, 但是修改结构体里面的数据, 并不会影响到外面的数据.
int,int * 类型, 带__block.
其实这种的变量, 外面包了一层结构体, 外部访问也是通过这个结构体访问(编译器会自动处理). 所以我们对结构体的修改(值传递), 也会同时影响到外面.
为什么有些需要拷贝到堆, 有的不需要?
这个时候, 我们知道需要管理捕获变量的内存, 并且是 Block 从栈拷贝到堆的时候, 还有 Block 从堆上释放的时候. 当然, 只有捕获的变量为 id, NSObject, attribute((NSObject)), block, ... 类型变量和__block 修饰的变量才需要这个复制. 为什么呢? 其实原理都一样, 堆上的当然需要管理啦. 那为什么普通的指针类型
char *test= "nihao";
和普通变量 int a = 3; 不需要内存管理? 其实只能说明他们都不需要修改, 如果需要修改, 那么还是一样, 要加__block 才行.
核心方法_Block_object_assign 用来确定被捕获的变量怎样进行 copy.
BLOCK_FIELD_IS_OBJECT
(就是 3)说明捕捉的变量是这样的
NSObject *obj1 = [NSObject new];
, 那么在 MRC 下就会引用计数增加, 也就是 retain 操作. 但是需要注意的是
_Block_retain_object
在 ARC 下, 实际上只是个空操作, 因为 ARC 有自己更完善的一套内存管理机制.
当变量由__block 修饰时, 该变量会被打包成 Block_byref 类型, flags 会被标记为 BLOCK_FIELD_IS_BYREF, 就是 8. 从栈拷贝到堆, 并且调用内存管理方法, 这个是对 Block 结构体上保存的变量进行内存管理.
那么必然的, 如果是__block 结构的话, 也是需要对__block 变量结构体里面的对象或者 Block 进行管理,
__Block_byref_id_object_copy
, 也就是
BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT
和
BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK
的情况, 就是简单赋值就好了.
让我们分析一下为什么简单赋值就好
这时__block 变量结构体已经从栈拷贝到堆上了, 这个必须先理解的. 当我们访问原变量的时候, 是怎么访问的? 还记得吗?
obj2->__forwarding->obj2 = 新对象地址
这样解决修改原变量的问题了吗? 解决了, 所以目的也已经达到了. 出了作用域能访问吗? 不能, 因为__block 还避免了 MRC 下被持有, ARC 有自己的一套内存管理机制, 所以默认情况下, 还是会持有原变量, 所以 ARC 下却可以继续访问. 其实这个地方, 必须得有取舍, 在 ARC 下, 如果是__strong 修饰的原变量, 那么原变量将被持有, 那么意味着, 可能原变量, 永远不会被释放. 但是如果没有对原变量进行持有的话, 那么意味的, 需要进行持有, 并且在不再需要原变量的时候需要进行释放.
__block 修饰的变量, 如果是 id 或者 block 类型, 那么变量就会转换成下面这样的结构体.
- struct __Block_byref_obj2_1 {
- void *__isa;
- __Block_byref_obj2_1 *__forwarding;
- int __flags;
- int __size;
- void (*__Block_byref_id_object_copy)(void*, void*);
- void (*__Block_byref_id_object_dispose)(void*);
- NSObject *__strong obj2;
- };
block 类型, 决定了怎么样进行 copy, 可能最终只是简单赋值.
- // Values for Block_layout->flags to describe block objects
- enum {
- BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
- BLOCK_NEEDS_FREE = (1 << 24), // runtime
- BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
- BLOCK_IS_GLOBAL = (1 << 28), // compiler
- };
block 类型, 默认为 0
- // flags/_flags 类型
- enum {
- /* See function implementation for a more complete description of these fields and combinations */
- // 是一个对象
- BLOCK_FIELD_IS_OBJECT = 3, /* id, NSObject, attribute((NSObject)), block, ... */
- // 是一个 block
- BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */
- // 被__block 修饰的变量
- BLOCK_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */
- // 被__weak 修饰的变量, 只能被辅助 copy 函数使用
- BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */
- // block 辅助函数调用(告诉内部实现不要进行 retain 或者 copy)
- BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */
- };
block 辅助函数调用的情况, 其实就是__Block_byref_id_object_copy,131 就是 BLOCK_FIELD_IS_OBJECT| BLOCK_BYREF_CALLER.
- static void __Block_byref_id_object_copy_131(void *dst, void *src) {
- _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
- }
以下使用 Block 不用手动调用 copy 操作:
将 Block 作为函数返回值时
将 Block 赋值给__strong 修改的局部变量, 或者标记为 strong 和 copy 的属性时
向 Cocoa 框架含有 usingBlock 的方法或者 GCD 的 API 传递 Block 参数时
参考资料:
Objective-C 高级编程 iOS 与 OS X 多线程和内存管理
char *a 与 char a[] 的区别 (char *a 的内容是在常量区, char a[] 的内容在栈区) http://www.cnblogs.com/kaituorensheng/archive/2012/10/23/2736069.html
栈空间与堆空间 http://www.cnblogs.com/edisonchou/p/4669098.html
漫谈 Block
来源: http://www.jianshu.com/p/c159acc728fb