高级编程之Blocks
本文主要写了为什么会这么设计,该怎么去想这个问题。可能没有其它文章那么详细的去写实现细节,但如果知道了为什么会这样设计,用法的东西应该很快就能通的。
为什么会有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[]的内容在栈区)
- 想修改变量的值,需要添加__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来动态申请内存的必须释放。
如果想彻底搞懂的话,请深入了解堆栈,还有ARM架构下汇编在栈的调用情况。否则,你只需知道栈不需要释放,堆上需要释放即可。 - 需要理解到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多线程和内存管理》