深入理解Objective-C的Block

2023-11-02

最近时间少,也变得懒了,好久没在这里写文章了,眼看就到8月末了,还是整理一篇酝酿已久的吧。之前的文章中整理过用ObjectiveC开发中常用到的Block代码块,其中也提到了一个和block使用不当的crash例子。接着这个问题,本篇文章将更深一步,对Block的内存使用相关的内容简要整理一下,解释其中的道理和使用Block需要注意的问题

0. 问题所在

下面给出一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- ( NSArray *) getBlockArray
{
     int num = 916;
     return [[ NSArray alloc] initWithObjects:
             ^{ NSLog ( @"this is block 0:%i" , num); },
             ^{ NSLog ( @"this is block 1:%i" , num); },
             ^{ NSLog ( @"this is block 2:%i" , num); },
             nil ];
}
 
- ( void ) forTest
{
     int a = 10;
     int b = 20;
}
 
- ( void )test
{
     NSArray * obj = [ self getBlockArray];
     [ self forTest];
     void (^blockObject)( void );
     blockObject = [obj objectAtIndex:2];
     blockObject();
}

如上两个方法实现的代码并不难理解,其中第三个方法我们要去调用。它会调用第一个方法,并返回一个数组,数组中的元素是block代码块。那么在特定的场景下,调用test会发生crash(闪退)。说明这样的调用存在问题,恐怕能看到的应该就是EXC_BAD_ACCESS错误,通常这可以理解为一个“野指针”错误,访问了内存中不该访问的内容。

问题在哪?从“野指针”错误,我们很直接能想到的就是block对象引用到的地址内容已经不是我们想要的了,简单说就是block无效了。可block是对象类型的啊,为什么放在数组对象中回传失效了呢,加入NSArray的对象本身就应该retain过啊。

问题就在这里,下面我们先来看简单下Block与对象的关系。

1. Block与对象

首先我们先反思几个问题:

  • block到底是不是对象?
  • 如果是对象,和某个已定义的类的实例对象在使用上是不是一样的?
  • 如果不一样,主要的区别是什么?

对于第一个问题,苹果的Objective-C官方文档中在“Working with Blocks”明确说明:

Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary.  ”

可见,Block是Objective-C语言中的对象

苹果在block的文档中也提过这么一句:

As an optimization, block storage starts out on the stack—just like blocks themselves do.

Clang的文档中也有说明:

The initial allocation is done on the stack,but the runtime provides a Block_copy function” (Block_copy在下面我会说)

凭这一点,我们就可以回答剩下的两个问题。Block对象与一般的类实例对象有所不同,一个主要的区别就是分配的位置不同,block默认在栈上分配,一般类的实例对象在堆上分配。

而这正是导致本文最初提到的那个问题发生的根本原因。Block对象在栈上分配,block的引用指向栈帧内存,而当方法调用过后,指针指向的内存上写的是什么数据就不确定了。但是到此,retain的疑问还是没有解开。

我们想一想Objective-C引用计数的原理,retain是对一个在堆中分配内存的对象的引用计数做了增加,执行release操作的时候检查计数是否为1,如果是则释放堆中内存。而对于在栈上分配的block对象,这一点显然有所不同,如果方法调用返回,栈帧上的数据自然会作废处理,不像堆上内存,需要单独release,就算NSArray对block对象本身做了retain也无济于事。

Clang文档中提到:

Block pointers may be converted to type id; block objects are laid out in a way that makes them compatible with Objective-C objects. There is a builtin class that all block objects are considered to be objects of; this class implements retain by adjusting the reference count, not by calling Block_copy.

那么要是想如本文开头那样,用一个方法对block数组做初始化是否有可行方案呢。答案是肯定的,不过需要真正了解block的使用,至少要会用Block_copy()和Block_release()。

2. Block的类型和使用

我这里有对某个Block数组的一段Console Log显示,如下:

1
2
3
4
5
6
7
8
9
<__NSArrayI 0x937f240>(
<__NSGlobalBlock__: 0x126750>,
<__NSStackBlock__: 0xbfffc788>,
<__NSMallocBlock__: 0x937f1c0>,
<__NSMallocBlock__: 0x937f1e0>,
<__NSMallocBlock__: 0x937f200>,
<__NSMallocBlock__: 0x937f220>,
<__NSGlobalBlock__: 0x126818>
)

可以看得出,这些对象都是block,而且还分了3种不同的类型。

其实在Clang的文档中,只定义了两个Block类型: _NSConcreteGlobalBlock 和 _NSConcreteStackBlock 。而在Console中的Log我们看到的3个类型应该是处理过的显示,这些字样在苹果的文档和Clang/LLVM的文档中实难找到。通过字面上来看,可以认为 _NSConcreteGlobalBlock对应于 __NSGlobalBlock__ ,_NSConcreteStackBlock对应于 __NSStackBlock__ ,而__NSMallocBlock__则是另一种情况。(实际上也正是如此)

NSGlobalBlock,我们只要实现一个没有对周围变量没有引用的Block,就会显示为是它。而如果其中加入了对定义环境变量的引用,就是NSStackBlock。那么NSMallocBlock又是哪来的呢?malloc一词其实大家都熟悉,就是在堆上分配动态内存时。没错,如果你对一个NSStackBlock对象使用了Block_copy()或者发送了copy消息,就会得到NSMallocBlock。这一段中的几项结论可从代码实验得出。

因此,也就得到了下面对block的使用注意点。

对于Global的Block,我们无需多处理,不需retain和copy,因为即使你这样做了,似乎也不会有什么两样。对于Stack的Block,如果不做任何操作,就会向上面所说,随栈帧自生自灭。而如果想让它获得比stack frame更久,那就调用Block_copy(),让它搬家到堆内存上。而对于已经在堆上的block,也不要指望通过copy进行“真正的copy”,因为其引用到的变量仍然会是同一份,在这个意义上看,这里的copy和retain的作用已经非常类似。

“The runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1. The paired function is Block_release, which decreases the reference count by 1 and destroys the object if the count reaches zero and is on the heap.

在类中,如果有block对象作为property,可以声明为copy。

3. 其它

如果注释掉其中看似无关的[self forTest]调用,用当前的Xcode版本(我用的是5.1.1)build后,crash是不会发生的,这看起来很有意思。因为forTest方法本身并没有在逻辑上对数组的构建造成什么影响。

实际上这是因为上一个方法调用的栈帧没有被新的数据覆盖,仍然保留原来block数据的原因所致。这样显然是不安全的,是不能保证block数据可用的。

在ARC情况下,我们会发现一个有意思的情况,那就是返回的Block Array,只有元素0是执行过copy的。比如block数组中的第0个block是stack的,那么返回之后在数组index为0处取到的block变成了malloc的。与此同时,其它的block都如同没有执行过copy一样,如上述各段所述。这是一个现象,或者说是一个结论。至于为什么这样,众说纷纭,很多人认为这是编译器的一个bug,欢迎大家多多讨论,给出见解。

在苹果官方的《Transitioning to ARC Release Notes》文档中,写了这样一段话,大家理解一下,尤其是其中的“just work”。

“How do blocks work in ARC?

Blocks ‘just work’ when you pass blocks up the stack in ARC mode, such as in a return. You don’t have to call Block Copy any more.”

4. 参考

以上整理了对Block的理解,在开发中注意到这些点足以解决block的特殊性带来的各类问题。要想继续深入,可参看LLVM文档中对block的介绍:

http://clang.llvm.org/docs/Block-ABI-Apple.html

http://clang.llvm.org/docs/AutomaticReferenceCounting.html?highlight=class

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入理解Objective-C的Block 的相关文章

随机推荐

  • logistic_regression---python实现

    logistic regression 用python实现 一 理论知识 logistic 回归 虽然名字里有 回归 二字 但实际上是解决分类问题的一类线性模型 在某些文献中 logistic 回归又被称作 logit 回归 maximum
  • C语言——ASCII 码表及分类

    目录 一 分类 1 0 31 127 删除键 是控制字符 2 空白字符 3 可显示字符 二 ASCII 表 ASCII 定义了 128 个字符 一 分类 1 0 31 127 删除键 是控制字符 2 空白字符 空格 32 制表符 垂直制表符
  • javascript字符串操作常用方法

    一 操作方法 我们也可将字符串常用的操作方法归纳为增 删 改 查 增 增的意思并不是直接增添内容 而是创建字符串的一个副本 再进行操作 除了常用 以及 进行字符串拼接之外 还可通过concat concat 用于将一个或多个字符串拼接成一个
  • 关于centos7桥接模式上不了网的配置

    我在vmware里装了一下CentOS7 然后选桥接模式竟然连不上网了 我以前装完系统之后直接设置一下模式为桥接模式就可以上外网了 究竟是怎么回事 我查了一下资料之后才发现有个地方忘记配置了 那就是编辑里面的虚拟网络编辑器 接下来我们就重开
  • Python实现异方差检验(statsmodels)

    什么是异方差 摘自知乎https www zhihu com question 354637231 一句话 就是当随机扰动项和模型中的解释变量 自变量 存在某种相关性 就会出现异方差 即对于不同的样本点 随机误差项的方差不再是常数 而互不相
  • RequiredFieldValidator控件验证不能为空时报错多种解决方法以及问题分析

    最近在学asp net 在使用RequiredFieldValidator控件进行验证时 发现报错 界面控件如下图 点击完确定之后按理来说是要报不能为空的提示的但是却报错如下图 经过一番研究发现 也看了其他人的解决方案 总结如下 net 4
  • pytest自动化测试两种执行环境切换的解决方案

    一 痛点分析 在实际企业的项目中 自动化测试的代码往往需要在不同的环境中进行切换 比如多套测试环境 预上线环境 UAT环境 线上环境等等 并且在DevOps理念中 往往自动化都会与Jenkins进行CI CD 不论是定时执行策略还是迭代测试
  • 为什么要同时重写equals方法和hashCode方法(详解)

    在解释为什么要重写equals方法和hashcode方法时 我们要先了解一下这样重写的目的是什么 也让自己有一个思路 围绕这个思路去思考问题 能更好的整握其中的缘由 针对这个目的去思考为什么要去重写 重写的作用是什么 以及如何去重写它们 一
  • MySql SQL语句优化方法

    1 插入优化 当数据过大时 通过load函数上传 2 主键优化 页分裂 当数据乱序插入时 由于主键是按序排的 所以再插入时 当发现页的空间不够时 会通过重新开辟一个页 将原页中的数据拷贝进新的页中 并重新设定链表指针方向 页融合 当删除页内
  • Arduino Uno 实验7——SG90舵机

    SG90舵机简介 舵机是一种位置 角度 伺服的驱动器 适用于那些需要角度不断变化并可以保持的控制系统 主要是由外壳 电路板 驱动马达 直流电机 减速齿轮组 位置检测元件 控制电路 所构成 是一套自动 闭环 控制装置 所谓自动 闭环 控制就是
  • Spring包结构以及各个包之间引用关系说明

    Spring 包结构说明 spring jar 包含有完整发布的单个jar包 他包含有除spring mock jar之外的所有jar 原因是 spring mock jar只有在开发环境中才会用到 而且仅仅是作为一个辅助测试类存在 除了s
  • 解析#pragma指令

    在所有的预处理指令中 Pragma 指令可能是最复杂的了 它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作 pragma指令对每个编译器给出了一个方法 在保持与C和C 语言完全兼容的情况下 给出主机或操作系统专有的特征 依据定义
  • Eclipse安装STS(Spring Tool Suite (STS) for Eclipse)插件

    由于最近在学习SpringBooot 用Eclipse创建SpringBoot项目比较不爽 听说STS插件能直接创建SpringBoot项目 就动手安装一下 希望能对像我一样的小白有所帮助 STS 官网 https spring io to
  • 【毕业设计】基于stm32的便携式U盘设计与实现 - stm32制作U盘

    文章目录 0 前言 1 简介 2 主要器件 3 实现过程 4 部分核心代码 5 最后 0 前言 这两年开始毕业设计和毕业答辩的要求和难度不断提升 传统的毕设题目缺少创新和亮点 往往达不到毕业答辩的要求 这两年不断有学弟学妹告诉学长自己做的项
  • 如何解决网站被黑客攻击-深夜一次网站被攻击瘫痪

    情况 客户网站深夜被黑客攻击 服务器对外发出大量攻击行为流量 运维人员已经不可登录服务器进行安全操作 1首先看能不能后台登录经网站管理界面 如果可以的话那就去查看相关日志 确定黑客攻击的范围 一定要尽可能的得到所有的日志 数据库的 Web服
  • (AJAX/JSON)技术实现校验用户名是否存在

    案例 校验用户名是否存在 1 服务器响应的数据 在客户端使用时 要想当做json数据格式使用 有两种解决方案 1 get type 将最后一个参数type指定为 json 2 在服务器端设置MIME类型 response setConten
  • tensorflow教程_TensorFlow教程

    tensorflow教程 TensorFlow教程 TensorFlow Tutorial PDF Version Quick Guide Resources Job Search Discussion PDF版本 快速指南 资源资源 求职
  • nginx配置中root和alias的区别

    例 访问http 127 0 0 1 download 这个目录时候让他去 opt app code这个目录找 方法一 使用root关键字 location root usr share nginx location download gz
  • 水平集分割

    基于距离正则的水平集分割MATLAB代码 无需初始化 This Matlab code demonstrates an edge based active contour model as an application of the Dis
  • 深入理解Objective-C的Block

    最近时间少 也变得懒了 好久没在这里写文章了 眼看就到8月末了 还是整理一篇酝酿已久的吧 之前的文章中整理过用ObjectiveC开发中常用到的Block代码块 其中也提到了一个和block使用不当的crash例子 接着这个问题 本篇文章将