探秘block原理

news2025/1/14 1:37:30

01

概述

iOS开发中,block大家用的都很熟悉了,是iOS开发中闭包的一种实现方式,可以对一段代码逻辑进行封装,使其可以像数据一样被传递、存储、调用,并且可以保存相关的上下文状态。

很多block原理性的文章都比较老,里面讲的一些知识已经过时,这里用新版的iOS SDK再梳理一遍block原理,也是和大家一起对已有知识做一次复习。

02

内存布局

block本质上可以理解为结构体,对于结构体的内存布局,先用一张图来表示一下,图中字段顺序按照布局的先后顺序:

  • isa:block也有isa,从内存结构上也属于对象,isa指向的是block的类对象,类对象例如__NSMallocBlock__,后续文章会讲到;

  • flags:用于存储一些标志位信息,例如是否捕获外部变量;

  • reserved:系统保留字段,后续可能会用于一些编译优化标志位,或者存储一些临时变量的处理;

  • invoke:函数指针,指向了block要执行的函数地址,也就是block代码块对应的函数地址;

  • descriptor(现在叫desc):指向block_desc_0,包含block大小、捕获的外部变量布局信息、增加引用计数和销毁的相关函数指针;

  • variables:block捕获的外部变量。

e18475710e99f3b78225d8e2d40448b3.jpeg

03

类型

由于block也是对象,可以通过class方法获取到其类型,也就是类对象。block有下面三种类型:

  • __NSGlobalBlock__,没有访问auto变量的block,访问static变量是没问题的。这种类型的变量并没有什么意义,如果不需要用到auto变量,写成方法就可以满足需求;

  • __NSStackBlock__,在MRC环境下,访问了auto变量,会默认被放在栈区。需要手动copy到堆区,ARC环境下会在访问auto变量后,会自动拷贝到堆区;

  • __NSMallocBlock__,由开发者自己管理内存,不会由系统来释放。

block的分配主要是在三个区域,堆区、栈区、全局区,全局区的数据存储在数据段。

block在不同的场景会存在不同的内存区域中,在MRC中创建一个block首先是在__NSStackBlock__内存中的,然后我们使用copy方法将block拷贝到__NSMallocBlock__内存中进行内存管理。后来在ARC中系统已经帮我们做好了copy的操作,创建的block会自动copy__NSMallocBlock__内存中,堆区的block也有引用计数的概念。如果这个block中没有用到任何外部参数,系统会将这个block存放在__NSGlobalBlock__内存中。

c913a0afef72bdbd98f71395736359ac.jpeg

并且block也有继承关系,以下面TestBlock的实例来说,其父类是__NSGlobalBlock__,所有block的父类是NSBlock,并且NSBlock继承自NSObject类。在更早一些的iOS系统中,__NSGlobalBlock__NSBlock之间,还会有一层__NSGlobalBlock的关系(后面没有下划线)。

e9e860f99ca7d209648e62b2a95f8137.jpeg

04

转换C++

下面,我们通过clang命令将block转为结构体,来分析下其具体实现。虽然这并不是最终运行在iOS系统上的代码,其等于一种中间表现形式,后续编译链接优化才会形成运行在手机上的ipa包,但对于我们了解block的实现原理有很大帮助。

4.1转换命令

xcrunXcode用于查找和执行相关命令行的工具集,可以更好的执行clang命令,减少报错。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc [源文件路径] -o [目标文件路径]

clang命令有下面这些关键参数:

  • -fobjc-arc:如果项目是ARC或者ARCMRC混编的环境,需要通过此参数修饰,表示按ARC的方式进行转换,如果不需要ARC环境可以忽略;

  • -x objective-c++:此参数上面没用,如果包含Objective++源文件的时候,需要用到此参数,以确保clang可以区分OCC++代码;

  • -rewrite-objc:告诉clangC++的方式重写出来,包含的上层代码,clang会以底层代码的方式进行展现;

  • [目标文件路径]:非必传参数,不传的话默认在当前目录生成一个同名的cpp文件,例如main.m对应main.cpp

4.2转换示例

下面在main.m中实现了一个很简单的block,并且没有捕获任何外部变量,通过clang命令查看C++代码,观察block的具体实现原理。

00f1462166473024588d9fd02489c95a.jpeg

转换后将C++源文件拉到最下面,可以看到main函数以及TestBlock的实现,main函数中有很多转义代码,删掉后梳理逻辑会更清晰。

f7c52e76e5d0f1a69d472a96e9dd30f2.jpeg

05

结构体

5.1基础结构

转换后的代码看着比较复杂,但我们只看关键信息,__main_block_impl_0构造函数也可以去掉,整理后就是下面三个结构体。在不包含外部变量和__block的前提下,block结构体各个字段就这么简单,关键就是isaBlock_sizeFuncPtr这三个。

5184de03fe7c376000daf74131afb8a9.jpeg

我们也可以打印block结构体相关字段,但由于block的结构体并没有声明在某个.h文件中,所以需要我们讲clang转换后的结构体粘到对应的文件中,做显示声明。随后用__bridge的方式,将block对象桥接为自己声明的结构体,即可打印对应字段。

44e51b22f299d5b26fbae4462f3b9c28.jpeg

结构体中impl.FuncPtr存储的就是回调函数地址,从地址可以看出是一个虚拟地址,block结构体都存储在堆区。

f3cbcb3a80da27952f3524550dcac601.jpeg

5.2调用部分

看完block结构体的定义,我们来到main函数中,看block的实现和调用转换后是什么样的。将main函数中block相关的转换都去掉,结果如红圈部分。本质上就是两步,第一步是调用__main_block_impl_0的结构体构造函数,第二步是调用结构体的函数指针。

ae8765f95f711b319acf44797c17b6d0.jpeg

第一行main函数中调用的构造方法,是__main_block_impl_0结构体声明的C++构造函数,因为我们创建的是一个最简单block,可以看到block的存储区域是在stack栈区的。即main函数调用完,block生命周期就会结束。

81052833ccb9918733af2aabb3a32994.jpeg

__main_block_impl_0构造函数有两个参数,第一个红圈部分就是传入函数指针地址,函数对应的就是block内部的实现代码。第二个参数是__main_block_desc_0_DATA结构体,其定义为__main_block_desc_0,并且默认实现第一个参数传0,第二个参数是block结构体的大小,结构体为__main_block_impl_0 block自身的结构体大小。第三个参数有默认值,可以不传。

0fb6d3895960933a3d6590f8fb0a690b.jpeg

__main_block_desc_0结构体是一种紧凑型的写法,在声明__main_block_desc_0结构体后,紧接着声明了一个名为__main_block_desc_0_DATA的变量,变量类型为静态变量,并且实现了初始化相关代码。

e436962389273ae83f4dd6418f28067c.jpeg

在执行block的代码位置,可以看到并不是block->impl.FuncPtr的方式调用,而是直接block->FuncPtr的方式调用,中间少了一步。

严谨些来说应该加上impl,但不加也不会出问题。这是因为,如果看未删除转换代码的原始clang代码,可以看到block是被转换为__block_impl的,也就是说被当做__block_impl看待的。如果再结合__main_block_impl_0的结构体定义来看,__block_impl在成员变量的第一位,所以访问FuncPtr是没有问题的,只要不访问Desc就是可以的。

06

外部变量

6.1值类型

如果在block的调用中加一个外部变量,那结构体将会是怎样的?

f5260e86bcd1db4a6691b3d1485b8f04.jpeg

通过clang命令可以可以看到,转换后的__main_block_impl_0中增加了一个同名字段,这很简单没必要过多解释。在__main_block_impl_0构造函数中传入,通过冒号后的初始化列表对value参数进行初始化。

71a50a73be623393a2c6501c93cd7075.jpeg

后面传参和使用,就都是结构体赋值和取值逻辑,很简单。

94f5bfe29b82fe787deee58064b04390.jpeg

6.2值传递

下面这种写法,在block的使用中很容易踩坑。在block中使用value参数,并且打印value参数,发现结果为1,而不是2

b56fcbf676dbea7dbddd0000e1324668.jpeg

通过C++源码我们可以看到,这是因为如果block引用的外部变量是值类型,会采取直接复制值的方式,而不是指针引用。

a2d50b3f0432de4a7f16455ff65cea20.jpeg

想解决这个问题也很简单,通过__block修饰一下值类型,即可实现blockvalue的值和外部value参数统一。

5442504f43156c89f510a36b4c548e95.jpeg

6.3静态变量

我们看一下,如果捕获的是一个static修饰的静态变量,其结构体会是什么实现。

0db25bc636677e044134ce146939cd4f.jpeg

转换为C++代码后,可以看到原来的值传递变成了地址传递,__main_block_impl_0value的引用是指针引用,在main函数中将value的地址传入。如果被static修饰的本身就是一个对象,对象是通过指针引用的,在block的结构体中就是两个星号引用。也就是NSObject **obj

4dbc42fa4f8fc4699a4b7083cc57a9dc.jpeg

正是由于静态变量地址传递的实现,在block内可以对静态变量直接进行更改,而无需用__block进行修饰。

737894d22f7b7ad64acd752ea7d789d9.jpeg

6.4全局变量

如果把value改为全局变量,结构体会有什么变化呢?

65c56008740b61e373f44ded28eeb87a.jpeg

因为全局变量的作用域很大,所以并不需要block进行单独持有即可访问,结构体并不会新增字段。

51f04513f76cc4e9ca9233f123b55202.jpeg

6.5对象类型变量

如果block中引用的是对象,而不是基础数据类型,结构体会是什么定义呢?

c4927fcbb5e2603ec6bfcf64770b4008.jpeg

执行clang命令,执行完成后结构体是下图的,下面代码去掉了转换,以及整理过代码。可以看到多了两个函数指针,__main_block_copy_0__main_block_dispose_0

copy的实现__main_block_copy_0为例,执行后会调用Block_object_assign的实现,在实现中系统会根据person的引用方式,__strong__weak__unsafe_unretained,是强引用还是弱引用,调用对应的内存管理方法。

__main_block_dispose_0函数在block从堆区移除的时候被调用,调用dispose时会调用实现Block_object_dispose函数,函数中会根据person的引用方式,进行对应的减少引用计数或释放操作。

copydispose两个函数都有一个3的参数,这个参数是一个标志位,表示外部变量类型。这里是BLOCK_FIELD_IS_OBJECT表示一个对象类型,也有BLOCK_FIELD_IS_WEAK表示weak引用的变量,BLOCK_FIELD_IS_BLOCK表示block类型的变量等。

25327a0ccd7b9c3b385e0207026a020d.jpeg

07

结尾

感谢大家能把文章读完,这篇文章并不会包含__block__weak相关知识,为了更系统的了解这两部分,后面会新出一篇文章整体来讲一下,敬请期待~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2276242.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker】入门教程

目录 一、Docker的安装 二、Docker的命令 Docker命令实验 1.下载镜像 2.启动容器 3.修改页面 4.保存镜像 5.分享社区 三、Docker存储 1.目录挂载 2.卷映射 四、Docker网络 1.容器间相互访问 2.Redis主从同步集群 3.启动MySQL 五、Docker Compose 1.命令式安装 …

Bootstrap 前端 UI 框架

Bootstrap官网:Bootstrap中文网 铂特优选 Bootstrap 下载 点击进入中文文档 点击下载 生产文件是开发响应式网页应用,源码是底层逻辑代码,因为是要制作响应式网页,所以下载开发文件 引入 css 文件, bootstrap.css 和 …

Docker与微服务实战2-基础篇

1.学习一门新技术的理念 1.是什么 2.能干吗 3.去哪下载 4.怎么玩 5.永远的helloworld跑起来一次 AB法则 before 与 after 的对比 2.为什么会有Docker出现 3.docker理念 解决了运行环境和配置问题的软件容器,方便做持续集成并有助于整体发布的容器虚拟化…

蓝桥杯_B组_省赛_2022(用作博主自己学习)

题目链接算法11.九进制转十进制 - 蓝桥云课 进制转换 21.顺子日期 - 蓝桥云课 时间与日期 31.刷题统计 - 蓝桥云课 时间与日期 41.修剪灌木 - 蓝桥云课 思维 51.X 进制减法 - 蓝桥云课 贪心 61.统计子矩阵 - 蓝桥云课 二维前缀和 71.积木画 - 蓝桥云课 动态规划 82.扫雷 - 蓝桥…

CES 2025|美格智能高算力AI模组助力“通天晓”人形机器人震撼发布

当地时间1月7日,2025年国际消费电子展(CES 2025)在美国拉斯维加斯正式开幕。美格智能合作伙伴阿加犀联合高通在展会上面向全球重磅发布人形机器人原型机——通天晓(Ultra Magnus)。该人形机器人内置美格智能基于高通QC…

PyMysql 01|(包含超详细项目实战)连接数据库、增删改查、异常捕获

目录 一、数据库操作应用场景 二、安装PyMysql 三、事务的概念 四、数据库的准备 五、PyMysql连接数据库 1、建立连接方法 2、入门案例 六、PyMysql操作数据库 1、数据库查询 1️⃣查询操作流程 2️⃣cursor游标 ​3️⃣查询常用方法 4️⃣案例 5️⃣异常捕获 …

了解Node.js

Node.js是一个基于V8引擎的JavaScript运行时环境,它允许JavaScript代码在服务器端运行,从而实现后端开发。Node.js的出现,使得前端开发人员可以利用他们已经掌握的JavaScript技能,扩展技能树并成为全栈开发人员。本文将深入浅出地…

Unreal Engine 5 (UE5) Metahuman 的头部材质

在图中,你展示了 Unreal Engine 5 (UE5) Metahuman 的头部材质部分,列出了头部材质的多个元素。以下是对每个部分的解释: 材质解释 Element 0 - MI_HeadSynthesized_Baked 作用: 这是 Metahuman 的主要头部材质,控制整…

《自动驾驶与机器人中的SLAM技术》ch7:基于 ESKF 的松耦合 LIO 系统

目录 基于 ESKF 的松耦合 LIO 系统 1 坐标系说明 2 松耦合 LIO 系统的运动和观测方程 3 松耦合 LIO 系统的数据准备 3.1 CloudConvert 类 3.2 MessageSync 类 4 松耦合 LIO 系统的主要流程 4.1 IMU 静止初始化 4.2 ESKF 之 运动过程——使用 IMU 预测 4.3 使用 IMU 预测位姿进…

SQL从入门到实战-2

高级语句 窗口函数 排序窗口函数 例题二十九 select yr,party,votes, rank() over (PARTITION BY yr ORDER BY votes desc) as pson from ge where constituency S14000021 order by party,yr 偏移分析函数 例题三十 select name,date_format(whn,%Y-%m-%d) data, confi…

Spring Security单点登录

本文介绍了Spring Security单点登录的概念和基本原理。单点登录是指用户只需登录一次,即可在多个相互信任的系统中实现无缝访问和授权。通过Spring Security框架的支持,可以实现有效的用户管理和权限控制。最后,本文提供了实际应用案例&#…

LKT4304新一代算法移植加密芯片,守护物联网设备和云服务安全

凌科芯安作为一家在加密芯片领域深耕18年的企业,主推的LKT4304系列加密芯片集成了身份认证、算法下载、数据保护和完整性校验等多方面安全防护功能,可以为客户的产品提供一站式解决方案,并且在调试和使用过程提供全程技术支持,针对…

浅谈云计算04 | 云基础设施机制

探秘云基础设施机制:云计算的基石 一、云基础设施 —— 云计算的根基![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/1fb7ff493d3c4a1a87f539742a4f57a5.png)二、核心机制之网络:连接云的桥梁(一)虚拟网络边界&#xff…

Qt C++读写NFC标签NDEF网址URI

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?spma21dvs.23580594.0.0.1d292c1biFgjSs&ftt&id615391857885 #include "mainwindow.h" #include "ui_mainwindow.h" #include <QDebug> #include "QLibrary" …

Java 如何传参xml调用接口获取数据

传参和返参的效果图如下&#xff1a; 传参&#xff1a; 返参&#xff1a; 代码实现&#xff1a; 1、最外层类 /*** 外层DATA类*/ XmlRootElement(name "DATA") public class PointsXmlData {private int rltFlag;private int failType;private String failMemo;p…

2024年度漏洞态势分析报告,需要访问自取即可!(PDF版本)

2024年度漏洞态势分析报告&#xff0c;需要访问自取即可!(PDF版本),大家有什么好的也可以发一下看看

不同音频振幅dBFS计算方法

1. 振幅的基本概念 振幅是描述音频信号强度的一个重要参数。它通常表示为信号的幅度值&#xff0c;幅度越大&#xff0c;声音听起来就越响。为了更好地理解和处理音频信号&#xff0c;通常会将振幅转换为分贝&#xff08;dB&#xff09;单位。分贝是一个对数单位&#xff0c;能…

Nginx反向代理请求头有下划线_导致丢失问题处理

后端发来消息说前端已经发了但是后端没收到请求。 发现是下划线的都没收到&#xff0c;搜索之后发现nginx默认request的header中包含’_’时&#xff0c;会自动忽略掉。 解决方法是&#xff1a;在nginx里的nginx.conf配置文件中的http部分中添加如下配置&#xff1a; unders…

C语言程序环境和预处理详解

本章重点&#xff1a; 程序的翻译环境 程序的执行环境 详解&#xff1a;C语言程序的编译链接 预定义符号介绍 预处理指令 #define 宏和函数的对比 预处理操作符#和##的介绍 命令定义 预处理指令 #include 预处理指令 #undef 条件编译 程序的翻译环境和执行环…

计算机组成原理(1)

系统概述 计算机硬件基本组成早期冯诺依曼机现代计算机 计算机各部分工作原理主存储器运算器控制器计算机工作过程 此文章的图片资源获取来自于王道考研 计算机硬件基本组成 早期冯诺依曼机 存储程序是指将指令以二进制的形式事先输入到计算机的主存储器&#xff0c;然后按照…