首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Block原理探究(上篇)-Block本质及存储域问题

Block原理探究(上篇)-Block本质及存储域问题

作者头像
梧雨北辰
发布于 2019-10-08 06:47:12
发布于 2019-10-08 06:47:12
1.1K00
代码可运行
举报
运行总次数:0
代码可运行

主要内容: 1.理解Block的本质 2.理解Block的存储域分类 3.理解Block的Copy原理

一、探究Block的本质

从一个最简单的Block使用示例说起,我们分析如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//main.m文件:
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
    int num = 10;
    void (^myBlock)(void) =^{NSLog(@"num = %d",num);};
    myBlock();
    return 0;
}

Objective-C语言是基于C、C++的,为了深入理解Block的底层结构,我们可以通过如下的编译器命令将上述代码转换成C++源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
clang -rewrite-objc 源代码文件名(如此例中的main.m)

转化后的C++源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_9e3646_mi_0,num);}

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, char * argv[]) {
    int num = 10;
    void (*myBlock)(void) =((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
    return 0;
}

对比OC代码与C++源码中的main函数,我们发现创建Block其实是调用了__main_block_impl_0结构体的构造函数;而Block中待执行代码也都被封装到了__main_block_func_0函数中。

另外值得注意的是,这些C++的结构体和函数的命名,是根据Block语法所属的函数名(此处为main)和Block语法在该函数出现的顺序值(此处为0)来设定的;

根据这些对应关系,我们对C++源码中的内容一一分析:

1.__main_block_imp_0结构体

__main_block_impl_0结构体对应了Block的定义,结构体内部包含了三个成员变量implDescnum,num其实就是被捕获的变量(后续再讲),另外还有一个同名的构造函数__main_block_impl_0;具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

Block通过调用这里的构造函数得以创建,调用时需传入了四个参数:(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0),前三个参数对应成员变量的初始化,而最后一个参数flags携带默认值可暂不考虑。

2.__block_impl结构体

__main_block_imp_0结构体的第一个成员变量impl,就是__block_impl结构体类型;尤其注意该结构体中包含有isa指针,从这一点就可以说明Block本质上还是一个OC对象,因为OC中只有对象才会具有isa指针的概念。而FuncPtr是一个函数指针,在__main_block_imp_0构造函数调用时被赋值;

3.__main_block_desc_0结构体

__main_block_imp_0结构体构造函数中传入参数desc,其实就是__main_block_desc_0对象。该结构体包含两个成员变量: reserved:系统保留值 Block_size:代表Block的大小

4.__main_block_func_0函数

__main_block_imp_0结构体构造函数中传入函数指针fp,其实就是__main_block_func_0函数的地址。该函数将Block中所有的代码封装为函数,以待被调用;

重要总结: 1.Block对应底层__main_block_impl_0结构体,其中包含有isa指针,这说明Block本质上还是一个OC对象; 2.Block中待执行的代码,在底层也被封装为__main_block_func_0函数,以实现调用;说明Block还携带了函数执行的环境

Block的特点: 1.Block相当于其他语言中的闭包或者匿名函数; 2.Block与函数区别在于,Block相当于函数加上函数执行的上下文环境(捕获外部变量下面会讲到);

二、Block的存储域
1.Block的存储域分类

在之前Block结构体构造函数中,我们很容易能找到这样一句代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
impl.isa = &_NSConcreteStackBlock;

我们已经知道Block也是一个Objective-C对象,每个OC对象都有一个isa指针指向其类对象,这里的情况也是类似的;Block的isa指针指向了_NSConcreteStackBlock类对象,即此时的Block是以_NSConcreteStackBlock类为模板创建的实例;

除此之外,其实还有两个与之类似的类_NSConcreteGlobalBlock_NSConcreteMallocBlock,不同的Block类创建的对象用于不同的存储域,也对应了对应不同的OC类型,具体整理如下:

clang类

OC类

内存区域

_NSConcreteGlobalBlock

NSGlobalBlock

静态区

_NSConcreteStackBlock

NSStackBlock

栈区

_NSConcreteMallocBlock

NSMallocBlock

堆区

下面通过打印的方式验证Block对象本质,具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)testBlock5 {
    void(^block)(int a) = ^(int a) {
        NSLog(@"This is a block");
    };
    
    NSLog(@"%@",[block class]);
    NSLog(@"%@",[[block class] superclass]);
    NSLog(@"%@",[[[block class] superclass] superclass]);
    NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
}

//打印结果:
//__NSGlobalBlock__
//__NSGlobalBlock
//NSBlock
//NSObject

观察打印结果,我们看到Block最终继承于NSObject类型,这又一次验证了Block本质就是OC对象的结论;而打印结果中出现的__NSGlobalBlock__说明此处的Block的存储域为静态区;

2.区分Block不同存储域类型的方法

Block的不同存储域对其的使用影响巨大,而正确区分Block类型的关键在于:Block中是否引用了自动变量(需要MRC下测试),总结起来如下:

Block类型

环境

内存区域

_NSConcreteGlobalBlock( NSGlobalBlock)

没有访问自动变量;或者只用到静态区变量

静态区

_NSConcreteStackBlock( NSStackBlock)

访问了自动变量

栈区

_NSConcreteMallocBlock( NSMallocBlock)

NSStackBlock调用了copy

堆区

我们可以使用代码对上述情况进行验证,但需要首先切换ARC到MRC环境下,因为在ARC环境下的编译器为我们做了很多优化的工作,比如自动将栈区的Block拷贝到堆区,这样我们也就不容易捕获到Block初始状态的位置了。所以需要暂时将开发环境切换至MRC下来测试。相关的测试代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)testBlock7 {
    //1.Block内部没有调用外部自动变量
    void (^block1)(void) = ^{
        NSLog(@"Block");
    };
    
    //2.Block内部调用外部自动变量
    int a = 10;
    void (^block2)(void) = ^{
        NSLog(@"Block-%d",a);
    };
    
    //3.拷贝栈上的block
    void (^block3)(void) = ^{
        NSLog(@"Block-%d",a);
    };
    
    //打印Block类型
    NSLog(@"%@ %@ %@", [block1 class], [block2 class], [[block3 copy] class]);
}

//打印结果:
//__NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__

分析代码: NSGlobalBlock:Block中没有引用自动变量或者只用到静态区变量,这种Block与全局变量一样设置在程序的静态区,直到程序结束才会被回收;此类型的Block不依赖执行时的状态,所以整个程序只需一个实例,用的也较少;

NSStackBlock:Block中访问自动变量,并且存放在栈中,栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放;所以我们有可能遇到Block内存销毁之后才使用它的情况,开发中遇到的很多问题也都是因此而起;

NSMallocBlock_NSStackBlock__执行copy操作会生成__NSMallocBlock__;栈Block被拷贝后存放在堆中后,需要我们自己进行内存管理,否则还可能造成一些循环引用的问题;

三、Block的Copy的问题

Block有着不同的存储域类型,尤其是配置在栈上的Block(即__NSStackBlock__类型的Block),如果其所属的作用域结束该Block就会被释放。此时若继续使用Block,就需要执行copy操作,将其由栈区拷贝到堆区得到__NSMallocBlock__,而__NSMallocBlock__也会在其引用计数为0的时候被释放;

关于Block的拷贝,其实还需要分为MRC和ARC两种环境来考虑,下面是具体的分析:

1.MRC下的Block拷贝

在MRC环境下,我们只能显式的通过copy来实现Block的拷贝;通常为了避免Block的释放,我们定义Block属性的时候必须使用copy修饰符也正是基于这个原因。下面是在MRC环境下测试栈Block的使用,具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef void(^PrintBlock)(void);

@interface ViewController ()
@property (nonatomic ,copy)PrintBlock block1;
@property (nonatomic ,copy)PrintBlock block2;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createBlock];
    self.block1();
    self.block2();
    NSLog(@"block1:%@", [self.block1 class]); //报错Thread 1: EXC_BAD_ACCESS (code=1, address=0x7ffeeb90b8c0)
    NSLog(@"block2:%@", [self.block2 class]);
}

- (void)createBlock {
    int a = 10;
    //此处采用直接赋值的方式,不会触发setter方法
    _block1 = ^{
        NSLog(@"This is block1-%d",a);
    };
    
    self.block2 = ^{
        NSLog(@"This is block2-%d",a);
    };
    //离开此作用域,block1就会被释放
    NSLog(@"block1:%@、block2:%@", [self.block1 class],[self.block2 class]);
}
@end

打印结果及分析如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
block1:__NSStackBlock__、block2:__NSMallocBlock__
This is block1-10
This is block2-10

由于block1采用的是直接赋值的方式,没有调用setter方法,所以block1并没有被拷贝到堆上,是一个栈上的Block,这样也就直接导致了第二次打印block1时所发生的野指针崩溃;

2.ARC下的Block拷贝

在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,总结起来包含以下几种情况:

  • Block作为函数返回值时;这就类似与MRC中对返回值Block执行了[[returnedBlock copy] autorelease];
  • Block被强引用,如Block被赋值给__strong或者id类型;
  • Block作为GCD API的方法参数时;
  • Block作为系统方法名含有usingBlock的方法参数时;

下面的代码演示了这些情况:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
typedef void(^Block)(void);
-(Block)getBlock{
    //ARC下的Block中访问了auto变量,此时block类型应为__NSStackBlock__
   int a = 10;
   return  ^{
        NSLog(@"---------%d", a);
    };
}

- (void)testBlock9 {
    //1.测试block作为函数返回值时
    NSLog(@"bock1-:%@",[[self getBlock] class]);
    
    //2.测试将block赋值给__strong指针时
    int a = 10;
    
    //2.1.block内没有访问auto变量
    Block block21 = ^{
        NSLog(@"block21");
    };
    NSLog(@"block21-%@",[block21 class]);
    
    //2.2.block内访问了auto变量,但没有赋值给__strong指针
    NSLog(@"block22-%@",[^{
        NSLog(@"block22-%d", a);
    } class]);

    //2.3.block赋值给__strong指针
    Block block23 = ^{
        NSLog(@"block23");
    };
    NSLog(@"block23-%@",[block23 class]);
    
    //3.block作为Cocoa API中方法名含有usingBlock的方法参数时
    NSArray *array = @[@"1",@"2",@"3"];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
    }];
    
    //4.block作为GCD API的方法参数时
    //Block中的延时操作完成时,系统将会对Block进行释放
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
    });
}

//打印结果如下:
//bock1-:__NSMallocBlock__
//block21-__NSGlobalBlock__
//block22-__NSStackBlock__
//block23-__NSGlobalBlock__
3.其他存储域Block的拷贝

上面讲述的重点都于对栈Blok的拷贝,若是对于已经配置在堆上或者配置在静态区的上的Block调用copy方法又将如何呢?下面是不同存储域的Block执行copy进行的总结:

Block类型

副本源的配置存储域

复制效果

_NSConcreteStackBlock

栈区

从栈复制到堆

_NSConcreteGlobalBlock

静态区

什么也不做

_NSConcreteMallocBlock

堆区

引用增加

4. 总结Block需要拷贝的原理

Block默认创建于其所在函数的函数栈上,所以当函数作用域结束时就会随之销毁;

在MRC环境下,没有编译器的优化,所以我们非常强调要使用copy将Block拷贝到堆上,从而避免Block在其作用域结束时被直接释放;

在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,对于Block使用copy还是strong效果是一样的,所以写不写copy都行。在ARC环境下对于Block依然使用copy,更像是从MRC遗留下来的“传统”,时刻提醒我们:编译器自动对Block进行了拷贝操作。如果不写copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对Block进行了拷贝操作”,他们有可能会在调用之前自行拷贝属性值,这种操作多余而低效。

最后,总结Block修饰符的使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);

//ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
参考链接

1.苹果官方Block文档 2.深入研究 Block 捕获外部变量和 __block 实现原理

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.10.07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
如何基于 yt-dlp 快速开发一个多平台的无水印视频下载软件?
你是否曾经需要从多个视频平台下载高清无水印的视频?无论是 YouTube、B站、抖音,还是其他主流视频平台,如何快速且高效地下载这些视频?今天,我们将通过一个简单的教程,教你如何利用 yt-dlp 工具,轻松实现这一目标。而如果你需要现成的工具,可以直接访问视频快下工具下载,实现一键下载多个平台的视频。
用户11590374
2025/03/31
6030
github短视频去除水印项目Douyin_TikTok_Download_API介绍
当下正值短视频盛行的时代。在我们浏览短视频的同时,经常能发现一些精美的图片、引人入胜的文案以及吸引眼球的视频,想要将它们保存到本地。然而,保存下来的图片或视频通常伴随着不太愉悦的水印,这显著降低了使用体验。因此,我时常思考是否存在途径能够下载一些无水印的图片。虽然有许多小程序等可以保存无水印的图片或视频,但它们往往伴随着一些令人不悦的广告或付费等。今天,在浏览 GitHub 时偶然发现了一个开源项目,名为“Douyin_TikTok_Download_API”,它能够满足我们的需求。在本文中,我将详细介绍这个项目,并分享如何进行部署和使用。
修己xj
2024/01/03
2.1K0
github短视频去除水印项目Douyin_TikTok_Download_API介绍
如何利用苹果快捷指令添加自己专属的URL
其实这么说有些夸张,其实并不是没有条件的,标题那么取只不过是标题党罢了,吸波流量,骗点点赞关注什么的。
知识与交流
2024/05/07
1.2K0
如何利用苹果快捷指令添加自己专属的URL
用Python搞定抖X无水印短视频下载
有时候刷抖音,遇到喜欢的视频保存在本地,然后都是带有水印的,作为有一点“洁癖”的小编,不太喜欢。索性就自己用Python制作了这个简单的小工具,用于下载抖音无水印短视频!
可以叫我才哥
2022/04/12
9550
用Python搞定抖X无水印短视频下载
b 站视频下载神器合集,支持电脑和手机端
这是个简单易用的b站视频下载工具https://github.com/leiurayer/downkyi ,几乎可以下载所有的B站视频,采用Aria下载器多线程下载,采用FFmpeg对视频进行混流、提取音视频等操作。
苏生不惑
2021/10/14
11.9K0
b 站视频下载神器合集,支持电脑和手机端
如何快速开发视频下载器
视频下载器网站是许多用户的刚需,它能帮助用户将在线视频保存到本地,方便离线观看或学习。本文将介绍如何利用优秀的开源项目yt-dlp开发一个功能齐全的视频下载器网站。
石臻臻的杂货铺[同名公众号]
2025/05/08
1350
如何快速开发视频下载器
Python实现超简单【抖音】无水印视频批量下载
导读:本文介绍了如何使用简单的Python爬虫爬取抖音上你喜欢的拍客的所有视频(包含有水印和无水印两种)。
我被狗咬了
2020/05/29
5.3K1
Python实现超简单【抖音】无水印视频批量下载
利用Python+Requests实现抖音无水印视频下载
首先,打开抖音应用,找到你想要下载的视频,点击右上角的“分享”按钮,选择“复制链接”。这样你就可以获取到视频的分享链接。
小白学大数据
2025/07/02
3960
一款跨平台的快速,简单,干净的视频下载器:Annie,支持Bilibili/Youtube等多个网站
说明:最近发现了个很强的下载神器Annie,一款用Go构建的快速,简单,干净的视频下载器,支持的平台很多,包括MacOS、Windows、Linux等,安装和使用是非常简单的,很适合新手,支持的网站也多,目前支持以下网站:
子润先生
2021/05/29
4.4K0
抖音视频一键去水印并下载:用 Python + MCP 实现自动化工具
不论是发布文章还是视频,都会面临着一个多平台发布的问题,如果能够一次性同时发布多个平台就好了。如果我在A平台发布了一个视频,如果这时候有一个工具可以帮我下载这个视频,并去掉水印发布自动发布到B平台上,简直就太完美了。
叫我阿柒啊
2025/05/17
8812
抖音视频一键去水印并下载:用 Python + MCP 实现自动化工具
python爬虫教程:抖音无水印视频批量下载
抖音越来越火,感觉它有毒,越刷越上瘾,总感觉下一个视频一定会更精彩,根本停不下来。想将抖音里喜欢的小哥哥/小姐姐的视频全部存到电脑硬盘里该如何操作?不想有抖音的视频水印该如何处理?
机器学习AI算法工程
2019/10/28
4.4K1
python爬虫教程:抖音无水印视频批量下载
使用跨平台工具 Lux 下载视频,支持所有视频平台
Lux 是一个使用 Go 语言编写的视频下载命令行工具,支持的平台很多,提供了包括 macOS、Windows、Linux 等等平台的命令行支持,安装和使用非常简单的。Lux 原来的名字是叫做 Annie(安妮),对标的是 macOS 上一款非常著名的视频下载软件叫做 Downie(唐尼)。但后来改名成了 Lux。
我的小碗汤
2023/03/19
3.1K0
使用跨平台工具 Lux 下载视频,支持所有视频平台
微信视频号下载器(微信视频号视频下载工具)
微信视频号下载器(微信视频号视频下载)重磅发布了,知识兔可以把微信的视频号里面的视频提取出来,适合广大的有需求的用户。主要提供微信视频号视频下载、知识兔直播流地址解析功能。
知识兔
2022/10/23
9.8K2
​微信视频号下载器免费版下载 (微信视频号视频下载)
微信视频号下载器(微信视频号视频下载)重磅发布了,知识兔可以把微信的视频号里面的视频提取出来,知识兔适合广大的有需求的用户。知识兔主要提供微信视频号视频下载、知识兔直播流地址解析功能。
用户10122115
2022/11/05
4.6K2
抖音解析-抖音视频去水印下载
2020/8/31更新 1.分享作品链接 2.粘贴抖音链接 3.解析作品和封面图去水印还有音乐都可以下载 微信截图_20200831114235.png <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="X-UA-Compatible" c
天下科技
2020/08/31
2.5K0
IOS捷径 睡眠灯 sleep-lamp
作者:matrix 被围观: 323 次 发布时间:2022-03-01 分类:零零星星 | 无评论 »
HHTjim 部落格
2022/09/26
6690
IOS捷径 睡眠灯 sleep-lamp
120行代码下载抖音无水印视频「Python语言」
过年在家没什么事情做,一直在刷抖音,就想写个代码,试着去下载抖音的原视频文件,昨天写了一会,在下载上面出现了问题,没有成功的下载视频文件。今天上午又研究了一下。成功实现了下载抖音无水印视频文件。整体代码120行。下面一起来看一下吧!
申霖
2020/02/14
4.7K2
120行代码下载抖音无水印视频「Python语言」
用 Python 下载抖音无水印视频
说起抖音,大家或多或少应该都接触过,如果大家在上面下载过视频,一定知道我们下载的视频是带有水印的,那么我们有什么方式下载不带水印的视频呢?其实用 Python 就可以做到,下面我们来看一下。
Python小二
2020/08/18
9770
用 Python 下载抖音无水印视频
Python3 网络爬虫(四):视频下载,那些事儿!
「you-get」支持各大视频网站的视频下载,国内外加起来近 80 家。像国内的爱奇艺、腾讯视频、抖音、快手、B站、A站,国外的 Youtube、Twitter、TED、Instagram等等。
Jack_Cui
2020/05/18
6.8K0
Python3 网络爬虫(四):视频下载,那些事儿!
一篇文章教会你使用Python下载抖音无水印视频
今天小编要跟大家分享的是,利用Python如何下载抖音无水印的视频;大家可能要问了,这个有什么用呢?当然有用了。那么有什么用呢?下面小编跟大家详细说说。
菜鸟小白的学习分享
2021/06/17
4420
一篇文章教会你使用Python下载抖音无水印视频
推荐阅读
相关推荐
如何基于 yt-dlp 快速开发一个多平台的无水印视频下载软件?
更多 >
LV.1
这个人很懒,什么都没有留下~
目录
  • 一、探究Block的本质
    • 1.__main_block_imp_0结构体
    • 2.__block_impl结构体
    • 3.__main_block_desc_0结构体
    • 4.__main_block_func_0函数
  • 二、Block的存储域
    • 1.Block的存储域分类
    • 2.区分Block不同存储域类型的方法
  • 三、Block的Copy的问题
    • 1.MRC下的Block拷贝
    • 2.ARC下的Block拷贝
    • 3.其他存储域Block的拷贝
    • 4. 总结Block需要拷贝的原理
  • 参考链接
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档