当前位置: 首页 > news >正文

【iOS】block复习

目录

block的本质

block与内存管理

对于block在MRC和ARC下的区别

block内部的内存管理

Block的捕获

__block对变量的内存管理

调用Block的流程

Block的实现

Block循环引用及解决办法

常见的造成循环引用的一种情况:

使用weak

强弱共舞

手动中断持有关系

block传参

其他情况

静态变量持有

__strong持有问题

Block的类型

Block的使用规范


block的本质

block的概念是带有自动变量的匿名函数,block的本质是一个结构体,结构体里有四个部分,分别为isa指针,一个标志参数flag,一个表示所占空间的int变量,还有一个void*指针表示block花括号里的函数指针。因为结构体里有isa指针,所以block同样也是一个类。

block与内存管理

对于block在MRC和ARC下的区别

在MRC(Manual Reference Counting)下:

  • 在MRC中,使用Block需要手动管理其内存。

  • 当一个Block被创建时,它会在栈上分配内存,它不会强引用捕获的对象或者__block变量,因为二者都在栈上,生命周期基本是一样的

  • 当Block需要在长期存储或在异步操作中使用时,需要将Block进行copy操作,将其移动到堆上分配内存,这时Block中捕获的对象或者__block变量也会通过block底层的函数增加引用计数(block会持有捕获的对象或者__block变量底层的结构体),以确保Block及其引用的对象能够正确地存活。

在ARC(Automatic Reference Counting)下:

  • 在ARC中,编译器会自动处理Block的内存管理,无需手动管理retain和release操作。

  • ARC会自动根据Block对外部对象的引用情况来决定是否在Block创建时将外部对象进行retain操作,并在Block销毁时自动进行release操作。

  • 不需要手动执行copy操作,因为ARC会根据需要自动将Block从栈上移动到堆上。

ARC环境下Block自动拷贝的四种情况:

  1. 当Block作为函数返回值时

  2. 当Block被赋值给__strong修饰的指针时

  3. 在Block中作为GCD API参数时

  4. 当Block作为Cocoa API中有UsingBlock方法的参数时

block内部的内存管理

首先,在block并不是所有情况下都需要自行内存管理,在以下情况下不需要内存管理:

  • 当block在栈上时,block内部不会强引用__block变量,因为此时二者共享一个栈帧,二者的生命周期基本是一样的

  • 如果是基本数据类型并且没有加__block修饰符,那么block的捕获机制会直接捕获自动变量的瞬间值,在底层Block的结构体中会追加与自动变量相同类型的变量作为成员变量并赋值(bound by copy 只捕获值 不捕获地址 )(这里底层实现就是用一个“=”号,如果是捕获对象的话 对对象进行改变是可以的 因为对象变量的本质就是指针,捕获对象其实就相当于捕获了指向底层堆区里对象那块内存的指针,因此可以使用这个指针对那块内存进行更改,但是不能更改指针本身)

  • 对于static变量和全局变量,放在内存中的数据段,由程序统一管理,可全局访问,长期持有而且不会销毁,所以不需要内存管理。

因此,只有堆区的数据需要进行内存管理,有两种情况:

  • 对象类型的auto变量

  • 引用了__block修饰符的变量

无论在栈上还是堆上,当block捕获对象时,会在底层结构体中生成一个该对象类型的变量并赋值,因此会持有该对象,在结构体被销毁时释放这个对象。

因为对象的内存就是存储在堆上的,所以当block从栈上复制到堆上时,只会让block捕获的对象的引用计数加一,比如:

 NSObject * objc = [NSObject alloc];NSLog(@"objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)objc)));
​void (^__weak blockA)(void) = ^{NSLog(@"A objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)objc)));};blockA();
​void (^blockB)(void) = ^{NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)objc)));};blockB();

在ARC环境下,引用计数其实会是1、2、4,为什么是4?因为blockB是强引用的,因此会将栈上的block拷贝到堆上,栈上使引用计数+1,堆上使引用计数再加一,最后就会加2

当block被从栈上copy到堆上时,会调用__main_block_copy_0函数,这个函数底层会调用_Block_object_assign,如果是__block修饰的变量,就一定是强引用,会为底层__block变量结构体的引用计数加一(这里的引用计数与对象不同,是模拟出来的引用计数)并且会把__block变量从栈上复制到堆上;如果不是,引用类型同外部传进来的一致

如果__block变量本来就在堆上,在调用copy方法时,同样会调用这个函数,执行同样的操作,表现出来的就是引用计数加一

当block被销毁时,会调用__main_block_dispose_0这个函数,这个函数底层会调用_Block_object_dispose这个函数,会释放block持有的对象变量或者__block变量(引用计数减一)

Block的捕获

Block可以捕获的外部变量一共有四种:自动变量、静态变量、静态全局变量、全局变量、对象

对于静态全局变量和全局变量,block是不会去捕获的,因为变量放在全局区,block直接使用就好了

对于自动变量,block捕获到一个自动变量,就在底层__main_block_impl_0的结构体中添加一个相同类型的属性,并且对这个属性进行值拷贝,只拷贝值而不拷贝地址,因此Block捕获的只是变量的瞬间值,并且不允许更改

对于静态变量,它与自动变量相同,也是作为成员变量追加到底层的结构体中,但是block捕获到的是变量的地址,也就是说变量传递给结构体的是变量的地址而不是变量的值,因此对于静态变量,在Block中是可以修改的

对于对象类型,Block会捕获他们的指针,并使他们的引用计数+1

__block对变量的内存管理

__block修饰对象类型时,如果在ARC类型下,会对对象强持有;如果在MRC环境下,不会对对象强持有。也就意味着在MRC环境下,要使用__block变量的话,必须要手动地去管理对象的引用计数。

调用Block的流程

调用流程:

1.首先需要定义block,指定其参数类型和返回值类型以及block内部要执行的代码

2.创建并赋值block:可以将block赋值给一个变量,或者作为参数传递给其他方法或函数

3.调用block:在需要执行block内部代码的地方调用block

4.在MRC下,需要手动管理block的copy和release,而在ARC下编译器会自动处理block的内存管理

不论ARC或MRC,在以下情况下,block会自动复制到堆上:当block作为函数返回值、赋值给__strong变量、传递给Cocoa框架的usingBlock:方法或GCD中带这个字符串的API

Block的实现

Block的实现在底层实质是通过一个结构体实现的:__mian_block_impl_0,这个结构体包含一个结构体__block_impl和一个结构体指针__main_block_desc_0,在__mian_block_impl_0这个结构体中,捕获到的自动变量会被追加到结构体的成员变量中以供使用,而关于Block要执行的代码,在结构体中通过一个函数指针指向要运行的函数,调用的时候再通过指针*FuncPtr访问,此外还有两个成员变量,与结构体指针指向的结构体中的变量,他们用来表示今后版本升级所需的区域和Block的大小

关于__block变量,给自动变量添加上修饰符__block时,其实会在栈上生成一个结构体__Block_byref_val_0,在这个结构体中有一个成员变量相当于原自动变量,还有一个成员变量是一个指向自己的指针__forwarding。Block要使用这个__block变量时,会持有一个指向这个变量底层结构体的指针,通过这个指针访问这个结构体中的指针__forwarding,再通过该指针访问相当于原自动变量的那个成员变量。当Block从栈上拷贝到堆上时,如果__block变量保存在栈上,会从栈上复制到堆上并被Block持有,如果变量本来就在堆上,就会被Block持有,引用计数加一

至于为什么要有__forwarding指针,就算__block变量的结构体被保存到了堆上,在使用时仍然有可能会访问到栈上的这个变量,因此必须保证二者的一致性,在栈上的__block变量被复制到堆上时,栈上的__block变量结构体会将成员变量___forwarding的值替换为复制目标堆上的__block变量结构体实例的地址

Block循环引用及解决办法

常见的造成循环引用的一种情况:

typedef void(^TBlock)(void);
​
@interface ViewController ()
@property (nonatomic, strong) TBlock block;
@property (nonatomic, copy) NSString *name;
@end
​
@implementation ViewController
​
- (void)viewDidLoad {[super viewDidLoad];
​// 循环引用self.name = @"Hello";self.block = ^(){NSLog(@"%@", self.name);};self.block();
}

self持有属性block,而block又捕获了self,因此block持有self,block和self互相持有,就会引起循环引用

使用weak

   // 循环引用self.name = @"Hello";
​__weak typeof(self) weakSelf = self;self.block = ^(){NSLog(@"%@", weakSelf.name);};
​self.block();

创建一个新变量弱引用self对象,block不会持有self,这时就不会循环引用。但是同样有一个问题,就是如果在block中使用GCD延时2秒就可能导致代码执行过程中对象被释放了,因为block弱引用ViewController,所以就算block没执行,vc也可以被销毁,如果ViewController被销毁了,那block就已经无法获取到属性name了

强弱共舞

    self.name = @"Hello";
​__weak typeof(self) weakSelf = self;self.block = ^(){__strong __typeof(weakSelf)strongWeak = weakSelf;
​dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", strongWeak.name);});};self.block();

这样就可以解决self中途被释放的问题,因为strongSelf是个局部变量,strongSelf强引用了weakSelf,因此对象的引用计数会加一,但是由于weakSelf是弱引用的self,因此并不会造成循环引用,当strongSelf局部变量生命周期结束后会被释放,对象的引用计数减一,这时如果VC再被释放,weakSelf就会自动置nil

手动中断持有关系

    self.name = @"Hello";
​__block ViewController * ctrl = self;self.block = ^(){dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", ctrl.name);ctrl = nil;});};self.block();

self->block->ctrl->self,在block执行完之后,手动地切断ctrl对self的引用,人为地避免循环引用

block传参

    // 循环引用self.name = @"Hello";self.block = ^(ViewController * ctrl){dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", ctrl.name);});};self.block(self);

这种方式将self作为参数给block,把对象的地址作为实参拷贝给虚参,本质上是指针拷贝,没有对self进行持有

其他情况

静态变量持有
    // staticSelf_定义:static ViewController *staticSelf_;
​- (void)blockWeak_static {__weak typeof(self) weakSelf = self;staticSelf_ = weakSelf;}

以上会出现循环引用,weakSelf虽然是弱引用,但是staticSelf_静态变量,并对weakSelf进行了持有,staticSelf_释放不掉,所以weakSelf也释放不掉!导致循环引用!

__strong持有问题
- (void)block_weak_strong {
​__weak typeof(self) weakSelf = self;
​self.doWork = ^{__strong typeof(self) strongSelf = weakSelf;NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));
​weakSelf.doStudent = ^{NSLog(@"%@", strongSelf);NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));};
​weakSelf.doStudent();};
​self.doWork();
}

这段代码中,虽然也是强弱共舞,strongSelf的生命周期就在方法内,但是在这个doStudent方法中捕获了strongSelf这个外部变量,因此会把block从栈上复制到堆上,也会给strongSelf的引用计数加一,导致strongSelf无法被释放,进而导致循环引用

Block的类型

常见的Block有三种类型:_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock

21d8f46423c74d388f881f4eb789bd0b

在记述全局变量的地方使用Block语法时以及Block语法的表达式中不使用应截获的自动变量时,生成的Block为_NSConcreteGlobalBlock类对象。也就是说在全局变量的地方声明的Block和不捕获外部变量的block为全局的block(包括只使用了全局变量和静态变量的Block)

除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象

如果需要异步操作中使用block或者长期存储,需要把block拷贝到堆上,类型变为_NSConcreteMallocBlock对象,MRC下需要手动copy(除了自动copy的三种情况),ARC会自动完成copy操作。(因此,block作为属性时,MRC下需要使用copy属性关键字,ARC下使用copy/strong都可以)对于捕获的外部变量,ARC下会自动管理引用计数,MRC下要手动retain/release管理,对于__block变量,block复制到堆上时会调用copy/release方法,方法中会自动对__block变量结构体的引用计数(实现与对象的不同,是模拟出来的引用计数)进行管理

对于栈上的block,copy会从栈上拷贝的堆上并持有,对于本来就在堆上的block,copy会增加引用计数

Block的使用规范

block语法可以省去返回值类型和参数列表,常常使用typedef来重命名Block类型

block在MRC下要使用copy属性关键字

block在调用前需要判空,因为block底层结构体是通过一个成员变量来保存函数的指针的,如果block为空,那么这个函数指针就也是无效的,这时访问这个无效的地址就会报错。当block为空时,调用block会访问0x10地址,因为底层isa(void*类型)占8字节,Flags(int类型)占4字节,Reserved(int类型)占4字节,0x10的地址就是FuncPtr的地址

http://www.xdnf.cn/news/1484587.html

相关文章:

  • 【Python脚本系列】PyCryptodome库解决网盘内.m3u8视频文件无法播放的问题(三)
  • macOS中设置环境变量的各文件及作用域
  • web后端知识(php和python)——第一阶段
  • java面试中经常会问到的mysql问题有哪些(基础版)
  • Android studio的adb和终端的adb互相抢占端口
  • SpringCloud Alibaba微服务--Gateway使用
  • 【音视频】WebRTC P2P、SFU 和 MCU 架构
  • Hadoop(九)
  • TypeORM、Sequelize、Hibernate 的优缺点对比:新手常见 SQL 与 ORM 踩坑总结
  • 【秋招笔试】2025.09.03华为研发岗
  • 基于Django的“社区爱心养老管理系统”设计与开发(源码+数据库+文档+PPT)
  • [C++刷怪笼]:set/map--优质且易操作的容器
  • 【C++】C++入门—(下)
  • pycharm如何设置对应的python解释器
  • PerfTest:轻量高性能压测工具,兼容 HTTP/1/2/3、WebSocket,并带实时监控
  • Conda 包管理器与环境管理使用指南
  • 【音频字幕】构建一个离线视频字幕生成系统:使用 WhisperX 和 Faster-Whisper 的 Python 实现
  • vue中axios与fetch比较
  • 硬件开发1-51单片机4-DS18B20
  • 在Word和WPS文字的表格中快速输入连续的星期、月、日
  • PyCharm 从入门到高效:安装教程 + 快捷键速查表
  • 通义万相wan2.2视频模型的基础模型与安装应用详解
  • STEM背景下人工智能素养框架的研究
  • 【基于深度学习的中草药识别系统】
  • 2.链表算法
  • Python 并行计算实战:用多进程高效实现矩阵乘法
  • 《C++进阶之STL》【set/map 使用介绍】
  • 数据结构面试重点
  • ZYNQ FLASH读写
  • 鸿蒙NEXT应用数据持久化全面解析:从用户首选项到分布式数据库