三年前,开始接触使用block的时候,觉得block的语法很怪异,也不理解block的原理,只是觉得block比代理更高级,会用block的人就牛逼。那时候看唐巧大神的博客,跟着他的博客学习。
时光荏苒,现在在看唐巧大神的博客,已经很少看到他在更新技术相关的文章了,基本上都是在更新一些看过的书籍之类的,很明显他转管理了,应该还很成功。唐巧好多事情,都是走在了绝大多数程序员的前面,很有前瞻性。
再看看巧大的block文章,当时觉得看起来特别费劲的。
唐巧的文章主要是将block是怎么实现的,现在看来高级编程里面讲的要更详细,细节更多一些。但是在那个时候,大概6,7年前,他能总结到这个水平,还是很厉害,很超前的。
现在回过头来,反思自己当初学习的时候,觉得当时的学习方法和心态都有很大的问题,当时学的费劲,很大程度上,是这两方面的问题。
问题
- 没有看清楚他整体文章思路,因为有好多c语言的复杂代码都是通过clang编译的,并不需要完全记住,给当时看文章的时候的增加了很大的难度
- 站在了一个读者的角度,他这篇文章其实讲述的是一个实践的过程,所以,应该站在他的角度,最好能动手实践。
方案
- 这次在重新开一遍文章
- 根据他提供的参考资料,看看自己是否能写出一遍跟他的文章类似的高质量block文章
block和delegate的区别
- 首先,我想说的是blcok 和 delegate的区别,其实在iOS 开发中,使用block 和 delegate 的目的,基本都是为了实现回调。
block 的语法特点是代码集中,是一个集中的代码块。基于它的这一个特点,block 比较适合作为api设计的一部分,比如网络请求,需要有一个异步回调的操作,去把异步下载下来的数据,发送给对应的接受者。
delegate 的声明部分和实现部分是分开的,比如UITableViewDelegate的声明和实现分别在UITableView中和某一个UIViewController中,是分散代码块。
delegate 这样设计适用于公共接口较多的情况,这样做也更容易解耦
这个就是iOS 开发中,block和delegate最明显的一个区别了。
2.也有另外一个区别,从性能上来说,block的运行成本,要比delegate的运行成本高。
block出栈时,需要将使用的数据从栈内存赋值到堆内存。delegate只保存一个对象指针,直接回调,并没有额外消耗,想比C语言的函数指针,只多了一个查表动作。
我觉得而面试的时候,基本上也就问道这里就结束了。因为后面的分析,真的挺麻烦的。
block的实现原理
- block 是什么
block 是含有自定义变量的匿名函数 - 什么是匿名函数
无论是c语言还是oc语言,我们正常声明一个函数的时候,都是需要定义函数名的。block可以声明匿名函数,用^来作为标识,其实在block的数据结构定义里面,后续还是会把匿名函数转换为正常的c语言函数 - 什么是自定义变量
block 具有截获自动变量的功能,block的代码块里面如果有使用了外部的变量,block结构体中,会自动保存使用的外部变量。
但是保存的代码块中的自动变量,不能修改,如果想修改,需要使用__block修饰外部变量。 - block 的结构定义
block的结构定义这块的代码还有细节非常多,当时也是我看的非常乱的,而且现在也没有完全记住。我看巧大的博客,也是只记录了block的结构定义的关键点,然后通过clang将oc的源码改写成c语言进行分析的。
下面我也来按巧神发现问题解决问题的思路分析一下
- block的数据结构介绍
- block的三种类型及相关的内存管理方式
- block如何通过capture方式来访问函数外部的变量
- block的数据结构介绍
其实这个block的结构体相对来说是比较比较好理解的,
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
从图中我们可以看出来,block 的结构体有6个成员变量,但是这个跟clang 将objective-c转换过来的还是有区别的。clang装换的里面还有构造函数,block的实例是通过构造函数生成的。不过这个Block_layout看起来更通俗易懂。
- isa
任何对象都有isa指针,isa指针指向的是一个类对象,runtime中有详细的定义 - flags
表示block的附加信息,block copy 的实现代码中有对它的应用 - reserved
保留版本号,没有特别的用处 - invoeke
函数指针,指向具体的block函数实现的地址 - Block-descriptor
block的函数附加信息,包括大小,copy,dispose的函数指针。 - variables
capture 过来的变量,block能够访问它的外部局部变量,就是因为将这些变量复制到了结构体中。
其实这还存在一个问题,为什么block已经把外部局部变量复制到了结构体中,但是在block代码块中修改外部局部变量,仍然要将外部局部变量用__block去修饰。
用clong工具分析得出来的代码,这个地方巧大做了特别说明,就是为什么clang分析出来的代码和上面图中的代码有些不一样,clong里面的代码是嵌套的,而上图的代码却不是嵌套的。
这个问题我以前也没有注意过,我当时以为是作者为了表述清楚,故意简化了,原来并不是这样。
巧神给的代码例子,我copy过来了
struct SampleA {
int a;
int b;
int c;
};
struct SampleB {
int a;
struct Part1 {
int b;
};
struct Part2 {
int c;
};
};
原因就是结构体本身,不带有任何的附加信息
敲黑板,重要的事情,说三遍
- block 的三种类型以及相关的内存管理方式
现在看,三种类型无非就是block的实例放到哪里,可以放到,堆上,栈上,还有全局代变量区。
_NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
_NSConcreteStackBlock 保存到栈区的block,当函数返回时会被销毁。
_NSConcreteMallocBlock 保存到堆区的block,当引用计数为0时被销毁。
细节实现
- 全局的静态block如何实现的
建一个名为 block1.c 的源文件:
#include <stdio.h>
int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}
然后在命令行中输入clang -rewrite-objc block1.c即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 block 在 c 语言实现,我将 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;
__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()
{
(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
return 0;
}
下面我们就具体看一下是如何实现的。__main_block_impl_0 就是该 block 的实现,从中我们可以看出:
- 一个 block 实际是一个对象,它主要由一个 isa 和 一个 impl 和 一个 descriptor 组成。
在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是为了实现对象的所有特性,在此我们就不展开讨论了。- 由于 clang 改写的具体实现方式和 LLVM 不太一样,并且这里没有开启 ARC。所以这里我们看到 isa 指向的还是
_NSConcreteStackBlock
。但在 LLVM 的实现中,开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类型,具体可以看 《objective-c-blocks-quiz》 第二题的解释。 - impl 是实际的函数指针,本例中,它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样而已。
- descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 capture 和 dispose 的变量列表等。结构体大小需要保存是因为,每个 block 因为会 capture 一些变量,这些变量会加到 __main_block_impl_0 这个结构体中,使其体积变大。在该例子中我们还看不到相关 capture 的代码,后面将会看到。
这块巧大说的很细节了。几年前是真没看懂。现在感觉还可以。
- 存在栈上的block的实现
我们另外新建一个名为 block2.c 的文件,输入以下内容:
#include <stdio.h>
int main() {
int a = 100;
void (^block2)(void) = ^{
printf("%d\n", a);
};
block2();
return 0;
}
用之前提到的 clang 工具,转换后的关键代码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int 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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%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 a = 100;
void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
return 0;
}
1.isa指针指向了_NSConcreteStackBlock,说明这是一个分配到栈上的实例。
2.main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,被复制到 main_block_impl_0 结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,不会影响外部的实际变量 a。
3.main_block_impl_0 中由于增加了一个变量 a,所以结构体的大小变大了,该结构体大小被写在了 main_block_desc_0 中。
修改上面的源码,在变量前面增加 __block 关键字:
#include <stdio.h>
int main()
{
__block int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
i = 1023;
};
block1();
return 0;
}
查看转换后的代码
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref
printf("%d\n", (i->__forwarding->i));
(i->__forwarding->i) = 1023;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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()
{
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
void (*block1)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
return 0;
}
转换后的代码,明显比之前增加了好多代码
- 源码中增加了Block_byref_i_0 的结构体,这个结构体是用来保存截获并且要修改的变量i的。
注意,当不用__block修饰的时候,__main_block_impl_0结构体中只是增加一个变量,而用__block修饰的时候,__main_block_impl_0结构体中又增加了一个结构体Block_byref_i_0 - __Block_byref_i_0 结构体中带有 isa,说明它也是一个对象。
- main_block_impl_0 中引用的是 Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
- 我们需要负责 Block_byref_i_0 结构体相关的内存管理,所以 main_block_desc_0 中增加了 copy 和 dispose 函数指针,对于在调用前后修改相应变量的引用计数。
- NSConcreteMallocBlock 类型的 block 的实现
NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中。以下是一个 block 被 copy 时的示例代码,可以看到,在第 8 步,目标的 block 类型被修改为 _NSConcreteMallocBlock。
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
// 1
if (!arg) return NULL;
// 2
aBlock = (struct Block_layout *)arg;
// 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 8
result->isa = _NSConcreteMallocBlock;
// 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
变量的复制
- 对于block 外部变量的引用,block默认是将其复制到block的结构体中来实现访问。
2.对于用__block修饰的外部变量引用,block是复制其应用地址来实现访问的。
image.png
ARC对block的影响
在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。
原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出 <__NSMallocBlock__: 0x100109960>
。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
@autoreleasepool {
int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
};
block1();
NSLog(@"%@", block1);
}
return 0;
}
唐巧认为这么做的原因是,由于 ARC 已经能很好地处理对象的生命周期的管理,这样所有对象都放到堆上管理,对于编译器实现来说,会比较方便。
花了一下午时间,又重新梳理了block,目前已经基本都可以看懂了,以后还要更加深入的了解block的底层实现。
网友评论