最近抽空把 iOS与OSX多线程和内存管理 这本书看完了,个人感觉这本书还是值得去看看的。
第一部分详细说明了内存管理,包括 ARC 下和 MRC 下的引用计数、__strong
、__weak
、__autorelease
的一些用法和使用过程中的问题;第二部分详细的介绍了Block
的原理,包括平时开发过程中所遇到的 Block
循环应用问题的原因;第三部分为 GCD API
的使用方式。
本文是对第二部分 Blocks
的整理,但是还是建议去看下书。
1、什么是 Blocks
Blocks 是 C 语言的扩充功能。用一句话来表示 Blocks 的扩充功能;带有自动变量(局部变量)值的匿名函数。
所谓匿名函数就是不带名称的函数。C语言的标准不允许存在这样的函数。一般声明和调用 C 函数如下:
/// 声明
int func(int count);
///调用
int result = func(10);
如果用下方函数指针代替直接调用函数,看起来似乎可以不用知道函数名也能调用,事实上使用函数指针也需要函数名称,如果不使用函数名称就不能获取该函数的地址,因为在程序中函数名即为指针地址。
int func(int count) {
return count + 1;
}
int (*funcptr)(int) = &func;
int result = *(funcptr)(10);
如果一些页面传值回调的代码大量使用上述方式,就会使代码变得非常冗余而且难以阅读,所以 iOS 4.0
引入了 Block ,其实和指针调用的写法差不多,对应上方。
int (^blk)(int) = ^(int count) { retrun count + 1; };
int result = blk(10);
由此,再看下方的 Block 的范式就不难明白了。
Block 可以隐藏多个参数,比如:可以隐藏返回值,如果表达式中存在 return 语句,Block 自己可以推断类型,如下图:
如果不使用参数,参数也可以省略,如下图:
2、理解 Block
1、带有自动变量值
通过上方 Blocks 的介绍说明了 "带有自动变量(局部变量)值的匿名函数" 中的 "匿名函数",而 "带有自动变量值" 是什么意思呢?我们看下方这个代码:
int main()
{
int a = 10;
void(^blk)(void) = ^{ printf("%d",a); };
a = 11;
blk();
}
上述代码中 print
的值是 10
并不是 11
, 因此 a
的值被保存(即被截获),从而在执行 block
时使用,这是就是自动变量值的截获。
既然 block
会截获 block
中所使用的值,那么我们开发中的 self
同样也会被截获,一旦被截获就会形成循环引用,这是我们共知的。所以留个小问题:self
是怎么被截获的?
2、__block 说明符
int main()
{
int a = 10;
void(^blk)(void) = ^{ a = 11; };
blk();
}
如果在 Block
中 修改 a
的值,编译器是会报错的,所以自动变量值截获是保存执行 Block
语法瞬间的值,保存后就不能改写改值。如果想改就需要将 Block
中被截获的值添加 __block
说明符。
int main()
{
// 这里添加了 __block
__block int a = 10;
void(^blk)(void) = ^{ a = 11; };
blk();
}
3、Block 本质探索
1、未截获自动变量值的 Block
先来一段代码,然后打开终端,在这个 源代码文件
目录下,输入 clang -rewrite-objc 源代码文件名
。
int main(int argc, const char * argv[]) {
void(^blk)(void) = ^{ printf("Hello, World!\n"); };
blk();
return 0;
}
Clang 后代码如下:
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;
__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 void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello, World!\n"); }
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[]) {
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
为了方便阅读和理解,去掉一些类型转换后代码如下:
int main(int argc, const char * argv[]) {
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 上方的代码等价于下方代码
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
(*blk->impl.FuncPtr)(blk);
return 0;
}
再看上述代码你就会发现,Block
的本质也就是C语言函数指针的调用。那么开始逐行分析:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
__main_block_impl_0
结构体的构造器有4个参数,初始化传入了2个:
-
isa
:表示当前结构体类型,分为__NSConcreteStackBlock
(栈),__NSConcreteMallocBlock
(堆),__NSConcreteGlobalBlock
(数据区); -
flags
:初始化默认给了0;flags
相关枚举如下:
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
-
__main_block_func_0
:这个函数是Block
代码块中的方法在底层经过编译器生成的函数,将方法实现放在函数体中; -
&__main_block_desc_0_DATA
:__main_block_desc_0_DATA
的结构体指针,用来表示Block
的大小。
另外 __main_block_func_0
函数将 struct __main_block_impl_0 *
类型的 __cself
作为参数传递,这个参数是为了如果 Block
中捕获了自动变量,就会使用 __cself
取值,下方截获自动变量值的 Block会说明。
struct __main_block_impl_0 *blk = &tmp;
将初始化的结构体取地址给 blk
结构体指针。
(*blk->impl.FuncPtr)(blk);
从 blk
结构体指针指向的值取出方法指针地址执行 Block
块中的方法。
2、截获自动变量值的 Block
稍微改动一下未截获自动变量值的 Block 的代码,让 Block
中使用自动变量 int a
,Clang
一下再看代码。
int main(int argc, const char * argv[]) {
int a = 10;
void(^blk)(void) = ^{ printf("Hello, World! = %d\n",a); };
blk();
return 0;
}
Clang
后的代码如下:
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;
/// 这里多了一个成员变量
int a;
/// 这里入参多了一个 a
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/// 这里使用了 __cself
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
/// 使用__cself 获取 a 的值
int a = __cself->a; // bound by copy
printf("Hello, World! = %d\n",a); }
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[]) {
int a = 10;
/// 构造函数将 a 的值传入
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
这段代码与前面的代码稍有差异,使用的 int a
自动变量被作为成员变量追加到了 __main_block_impl_0
结构体中,并且该结构体的构造方法多了 int a
的传参(没有在 Block 中使用的不会截获),被截获的自动变量,传递给结构体进行保存。
被截获的自动变量的值是不能在 Block
内修改的,比如将上方代码 void(^blk)(void) = ^{ printf("Hello, World! = %d\n",a); };
修改成 void(^blk)(void) = ^{ a = a + 1; };
编译器就会报错。
然而有一个情况比较特殊,用于 Block
回调后将回调对象添加到数组中,代码如下:
int main(int argc, const char * argv[]) {
NSMutableArray *a = [NSMutableArray array];
void(^blk)(void) = ^{
[a addObject:@1];
};
blk();
return 0;
}
虽然 NSMutableArray *a
被截获了,但是依然能对 a
数组进行添加数据操作,这是因为 Block
截获的只是 a
对象本身,不对可操作数据内部进行截获,也就是说:对于自动变量对象只要对象指针地址不发生改变,便能正常使用;对于自动变量为值类型的只要值不发生改变,便能正常使用。
由下方编译器报错示例可以推断:
int main(int argc, const char * argv[]) {
NSMutableArray *a = [NSMutableArray array];
void(^blk)(void) = ^{
/// 编译器报错
a = [NSMutableArray array];
};
blk();
return 0;
}
3、修改 Block 中变量值的方法
开发中经常遇到需要改动 Block
代码块中存在的值,如何实现呢?经过上述研究知道了以下两点:
Block
仅仅截获自动变量;Block
截获的自动变量的值不允许修改。
所以首先尝试不使用自动变量,改用全局变量、全局静态变量和静态变量实现。代码如下:
int global_var = 1;
static int static_global_var = 2;
int main(int argc, const char * argv[]) {
static int static_var = 3;
void(^blk)(void) = ^{
global_var *= 3;
static_global_var *= 3;
static_var *= 3;
};
blk();
return 0;
}
Builde
一下,编译正常,然后 Clang
一下看看和截获自动变量有什么区别:
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;
/// 这里变成了指针
int *static_var;
/// 仅仅入参了静态变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int flags=0) : static_var(_static_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
/// 静态变量从 __cself 取出来,全局变量和全局静态变量直接使用了
int *static_var = __cself->static_var; // bound by copy
global_var *= 3;
static_global_var *= 3;
(*static_var) *= 3;
}
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[]) {
static int static_var = 3;
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_var));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
分析后发现,全局变量和全局静态变量直接使用了,而静态变量被截获了,截获的是静态变量的地址,通过对地址指向的值改变从而达到改变值的目的。
看起来静态变量的这种被 Block
截获指针后改值的方法似乎也适用于声明一个自动变量的指针然后在 Block
中更改值。但是为什么我们没有这么做,转而使用 __block
说明符呢?
本菜鸡想了半天,想了一个办法展示这个错误,代码如下:
int main(int argc, const char * argv[]) {
blk_t blk = func();
blk();
return 0;
}
blk_t func() {
int *_var;
{
int a = 3;
_var = &a;
}
blk_t blk = ^{
*_var *= 3;
printf("static_var = %d\n",*_var);
};
return blk;
}
错误结果如下:
因为如果 int a
被释放掉了,那么 int *_var;
取值就会错误,所以自动变量不能像静态变量一样使用。
关于 __block
的分析,下一篇 Block 原来你是这样的(二)说明。
4、结语
到这里,应该对 Block
的内部有一些了解了吧,其实 Block
就是我们 OC
中的对象,所以 Block
的第一个参数为 void *isa
,下一篇 Block 原来你是这样的(二)对 __block
说明符修饰的变量进行分析,以及__NSConcreteStackBlock
,__NSConcreteMallocBlock
和__NSConcreteGlobalBlock
三种类型Block
的作用域分析。
有问题欢迎指出,共同学习,一起进步。