相信无论是学习 还是面试的时候 block 都是一个绕不开的问题
如果你连block用还都不会用 那么,请多用block
目录
1. Block是什么?
2. Block的截获变量
Block是什么?(怎样理解block?)
百度一下搜索到的很多人给出的答案都是 block 是代码块或者说是 匿名函数, 原则上都没有错,我以前面试时候 也是 这么对面试官说的;
但是当你在深入理解一下, 你会发现 iOS里的 Block 其实就是一个对象
扩展一点来说 block是封装了函数调用以及函数调用环境的OC对象
(我用了加粗,如果你也能对别人说 block 是一个对象, 想好怎么给人说明 block 为什么是一个对象)
block是封装了函数调用已经函数调用环境的OC对象
针对环境这一词用在block上 我是在MJ的底层原理课程上听到 杰哥提到的,而也是因为这一个词语解释出了Block的重要内容之一截获变量的概念(后面相关小结会给出具体解释)
一切从你写的一行block开始
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^block)(void) = ^{
NSLog(@"Hello, World!");
};
block();
}
return 0;
}
最简单的一个 block 声明 与调用, 相信大部分同学都从网上的视频 看到过.m文件转成.cpp文件
然后 查看Block的 内部实现啊 balabala........
如果你以为我也要 这么做, 恭喜你 答对了 我确实要这么做 不过我会换另外一个角度来用点通俗易懂的语言来解释 block 底层做了点什么
去掉了一些个强制转换后的上面Block的cpp版本(原文对照看注释)
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;
/*
void (^block)(void) = ^{
NSLog(@"Hello, World!");
};
*/
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
// block();
block->FuncPtr(block);
}
return 0;
}
ok, 已经有开发经验的同学可以很轻松的看出来cpp 版本里这是创建了一个block(废话!谁看不出来)
往__main_block_impl_0函数中传入了两个参数:__main_block_func_0 和 __main_block_desc_0_DATA
先来翻一下这俩参数是啥.
__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)};
这个东西 暂且可以理解为对block的一个说明, 里面有block的大小 和一个保留字段, 这个参数没啥好解释的 知道是啥就行
下面我们开看看 重头戏
__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
}
这个方法 就是block的具体实现了
这里你要注意了哦,在转换为 c++后 这段代码 被独立成了 一个函数
接着我们来看 __main_block_impl_0都做了些什么
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;
}
};
如果只学过OC的同学 可能会纠结
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
这是啥 为啥在结构里里有个这么和玩应 还和结构体名字一样?
在第一次看别人的文章时候 我也蒙 因为我也不懂 这是干啥用的?
其实这只是c++的语法而已,如果你不了解c++那么 可以 理解为
UIView
// __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
// 就是个初始化方法, 在初始化方法里面给参数进行了赋值
- (instancetype)initWithFrame:(CGRect)frame {
/* impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc; */
}
大意上就是 初始化一个block 然后将 上面生成的 __main_block_func_0 和 __main_block_desc_0_DATA 传给相应的参数
这里可能会绕一下 结构体里面的 struct __block_impl impl;
是啥东西?
你可以理解为其实就是一个嵌套结构而已,创建block 时将参数 传入impl里保存
// __block_impl的结构是这样的
struct __block_impl {
// isa 看到了没? 这也证明了 为什么说 block 是一个对象
void *isa;
int Flags;
int Reserved;
// FuncPtr : 创建blcok 时候的 __main_block_func_0 参数 赋值给了 这个参数
void *FuncPtr;
};
tips: 题外话 想当初你是不是也背过这个方法名?然后磨磨唧唧背不下来?
__main_block_impl_0
里的 impl 是啥? 后面为啥还有个 0 ?
这个保证你搜都不一定有答案 会的人觉得这个问题太2b, 不会的人 搜了半天也无解
---其实 impl 就是 implements 单词的缩写(英语不好的 包括我 自行解决吧)
---那么0又是啥呢? 打比方 你一个.m 中 可能有多个block吧 那你怎么保证 第 50行写下的 block 不会调用你 138行 写的第二个block 呢?
这就是 0的作用了 编译器 会将你的所有block 进行编号 从而保证一一对应的关系
第一个block 编号是0 第二个 编号是1 第三个编号是2 以此类推
对照一下
__main_block_impl_0
__main_block_func_0
__main_block_desc_0
大致就是样
这样一看 是不是脑子嗡的一样? 卧槽原来是这样...
Block的截获变量
先来讲个故事
block是封装了函数调用已经函数调用环境的OC对象
文章开端就提到了 "环境"这个词, 这也让我理解了截获变量到底是干啥用的
在很久很久以前电脑游戏 还需要CD/DVD来安装的年代
在安装游戏的时候,你是不是遇到过 要运行游戏,你需要先安装 DirectX 这个东西才能打开游戏痛快的玩耍?
再或者在网页上看个视频 提示你 要下载 FlashPlayer 才能播放?
而这里的 DirectX / FlashPlayer 就可以 理解为所需要的环境
而我 就将 截获外部的变量 理解为 函数表用 所需要的环境
这里不在重复解释局部变量,静态局部变量, 全局变量,被截获(捕获)后的处理方式
(局部变量值传递, 静态局部变量引用传递, 全局变量直接使用 balabala的 这些都是苹果规定的 背就完了)
我们来说说为什么会出现截获变量, 它是怎么来的?
来一份.m中的原代码
// 全局变量
int age_ = 10;
// 静态全局变量
static int height_ = 10;
void (^block)(void);
void test()
{
// 局部变量
auto int a = 10;
// 静态局部变量
static int b = 10;
block = ^{
NSLog(@"age is %d, height is %d, a is %d, b is %d", age_, height_, a, b);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
这份代码在编译成cpp文件之后 会长这样
iint age_ = 10;
static int height_ = 10;
void (*block)(void);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a;
int *b;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_k3_vyx2rf814vzfz9gzs0vz541h0000gn_T_main_bd3fa6_mi_0, age_, height_, a, (*b));
}
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)};
void test()
{
auto int a = 10;
static int b = 10;
// 关于静态变量 可以看到 b 被传入的 是 &b (引用)
block = &__test_block_impl_0(__test_block_func_0, &__test_block_desc_0_DATA, a, &b);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
test();
block->FuncPtr(block);
}
return 0;
}
大概就是这样了 在 main函数 外面生成了 block的相关对外不可见的函数(上文提到的俩参数)
ok,换个思维 我们来讨论一个问题
image.png
如图所示 我们想在 main
方法中拿到 tempA, tempB 两个参数 如果按照上述代码是肯定不可能的
我们需要创建 外部的变量去赋值 然后在 main
方法中使用外部变量才可以实现
局部变量的截获
对照一下上面生成的 cpp 文件 你发现了什么呢?
是的 block 的截获变量 其实就是作用域的原因从而产生的处理办法
虽然.m代码中block的实现是在main函数之内, 但是经过编译器转码 其实block的实现就是一个新的函数__test_block_func_0
, 而这个生成的实现函数__test_block_func_0
想要调用局部变量 那就需要block自身持有这些变量 (此处存在描述上的歧义, 主旨是为了看的通俗易懂,不要过分纠结此处的字面意思)
__test_block_impl_0
(需要持有 a和 b 呀)然后才能让__test_block_func_0
方法去调用a, b 两个参数;
全局变量的截获
网上基本上所有block底层原理的讲述 都会涉及到 局部变量与全局变量
上面说完了 为啥截获 局部变量, 那么就再来说说全局变量
那为啥全局变量 不用截获 直接就可以用呢?
相信看了关于 局部变量的截图就已经明白了 因为全局变量 本身就是在哪都可以用的,所以压根就不需要浪费资源再去持有全局变量, 拿过来直接用就好了呀😝
网友评论