探秘block原理

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.mzph.cn/bicheng/67930.shtml

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

相关文章

vue3+ts+element-plus 对话框el-dialog设置圆角

对话框el-dialog设置圆角,实现的需求效果: 目前只能通过行内样式(style"border-radius: 20px")来实现圆角效果:

机器学习算法(三):K近邻(k-nearest neighbors)

1 KNN的介绍和应用 1.1 KNN的介绍 kNN(k-nearest neighbors),中文翻译K近邻。我们常常听到一个故事:如果要了解一个人的经济水平,只需要知道他最好的5个朋友的经济能力, 对他的这五个人的经济水平求平均就是这个人的经济水平。这…

大语言模型兵马未动,数据准备粮草先行

​从OpenAI正式发布ChatGPT开始,大型语言模型(LLM)就变得风靡一时。对业界和吃瓜群众来说,这种技术最大的吸引力来自于理解、解释和生成人类语言的能力,毕竟这曾被认为是人类独有的技能。类似CoPilot这样的工具正在迅速…

Network Compression(李宏毅)机器学习 2023 Spring HW13 (Boss Baseline)

1. Introduction to Network Compression 深度学习中的网络压缩是指在保持神经网络性能的同时,减少其规模的过程。这非常重要,因为深度学习模型,尤其是用于自然语言处理或计算机视觉的大型模型,训练和部署的计算成本可能非常高。网络压缩通过降低内存占用并加快推理速度,…

UnityDots学习(二)

在一里已经概述了什么是Dots,已经如果使用它,我们要做的思维转变。 简单总结下: Dots使用了计算器多核,已经3级缓存的优势,在此基础上使用Brust编译器对各个平台实现了代码优化。从而达到了加速提升的效果。 我们要…

Linux (CentOS) 安装 Docker 和 Docker Compose

🚀 作者主页: 有来技术 🔥 开源项目: youlai-mall ︱vue3-element-admin︱youlai-boot︱vue-uniapp-template 🌺 仓库主页: GitCode︱ Gitee ︱ Github 💖 欢迎点赞 👍 收藏 ⭐评论 …

c++ 预备

目录 前言 一,知识点的补充 二,c语言与c 三,面向对象的三大特点 前言 将进入c的学习,接下来是对于c的预备和c的一些预习 一,知识点的补充 1 标识符 标识符不能为关键字 标识符只能由下划线,数字&#xf…

SpringBoot项目实战(41)--Beetl网页使用自定义函数获取新闻列表

在Beetl页面中可以使用自定义的函数从后台新闻列表中获取新闻数据展示到页面上。例如我们可以从后台新闻表中获取新闻按照下面的格式展示&#xff1a; <li><a href"#">东亚非遗展即将盛妆亮相 揭起盖头先睹为快</a></li><li><a hre…

从零开始开发纯血鸿蒙应用之多签名证书管理

从零开始开发纯血鸿蒙应用 一、前言二、鸿蒙应用配置签名证书的方式1、自动获取签名证书2、手动配置签名证书 三、多签名证书配置和使用四、多证书使用 一、前言 由于手机操作系统&#xff0c;比电脑操作系统脆弱很多&#xff0c;同时&#xff0c;由于手机的便携性&#xff0c…

数据结构初阶---排序

一、排序相关概念与运用 1.排序相关概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的…

系统看门狗配置--以ubuntu为例

linux系统配置看门狗 以 ubuntu 系统配置看门狗为例 配置看门狗使用的脚本文件&#xff0c;需要使用管理员权限来执行&#xff1a; 配置是&#xff1a;系统每 30S 喂一次狗&#xff0c;超过 60S 不进行投喂&#xff0c;就会自动重启。 1. 系统脚本内容&#xff1a; #!/bin/b…

opencv的NLM去噪算法

NLM&#xff08;Non-Local Means&#xff09;去噪算法是一种基于图像块&#xff08;patch&#xff09;相似性的去噪方法。其基本原理是&#xff1a; 图像块相似性&#xff1a;算法首先定义了一个搜索窗口&#xff08;search window&#xff09;&#xff0c;然后在该窗口内寻找…

Docker运维高级容器技术知识点总结

1、虚拟机部署和容器化部署的区别是什么&#xff1f; 1、技术基础&#xff1a; <1>.虚拟化技术在物理硬件上创建虚拟机&#xff0c;每台虚拟机运行自己完整的操作系统、从而实现资源隔离。 <2>.容器化技术&#xff1a;将应用程序打包在容器内&#xff0c;在进程空间…

双模充电桩发展前景:解锁新能源汽车未来的金钥匙,市场潜力无限

随着全球能源转型的浪潮席卷而来&#xff0c;新能源汽车行业正以前所未有的速度蓬勃发展&#xff0c;而作为其坚实后盾的充电基础设施&#xff0c;特别是双模充电桩&#xff0c;正逐渐成为推动这一变革的关键力量。本文将从多维度深入剖析双模充电桩的市场现状、显著优势、驱动…

python3GUI--大屏可视化-传染病督导平台 By:PyQt5

文章目录 一&#xff0e;前言二&#xff0e;预览三&#xff0e;软件组成&开发心得1.样式&使用方法2.左侧表格实现3.设计4.学习5.体验效果 四&#xff0e;代码分享1.环形渐变进度组件2.自定义图片的背景组件 五&#xff0e;总结 大小&#xff1a;60.9 M&#xff0c;软件…

某漫画网站JS逆向反混淆流程分析

文章目录 1. 写在前面1. 接口分析2. 反混淆分析 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Pyth…

ffmpeg aac s16 encode_audio.c

用ffmpeg库时&#xff0c;用代码对pcm内容采用aac编码进行压缩&#xff0c;出现如下错误。 [aac 000002bc5edc6e40] Format aac detected only with low score of 1, misdetection possible! [aac 000002bc5edc8140] Error decoding AAC frame header. [aac 000002bc5edc81…

深度学习的原理和应用

一、深度学习的原理 深度学习是机器学习领域的一个重要分支&#xff0c;其原理基于多层神经网络结构和优化算法。以下是深度学习的核心原理&#xff1a; 多层神经网络结构&#xff1a;深度学习模型通常由多层神经元组成&#xff0c;这些神经元通过权重和偏置相互连接。输入数据…

mv指令详解

&#x1f3dd;️专栏&#xff1a;计算机操作系统 &#x1f305;主页&#xff1a;猫咪-9527-CSDN博客 “欲穷千里目&#xff0c;更上一层楼。会当凌绝顶&#xff0c;一览众山小。” 目录 基本语法 主要功能 常用选项详解 1. 移动文件或目录 2. 重命名文件或目录 3. -i&am…

5 分布式ID

这里讲一个比较常用的分布式防重复的ID生成策略&#xff0c;雪花算法 一个用户体量比较大的分布式系统必然伴随着分表分库&#xff0c;分机房部署&#xff0c;单体的部署方式肯定是承载不了这么大的体量。 雪花算法的结构说明 如下图所示: 雪花算法组成 从上图我们可以看…