当前位置: 首页 > 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://eMcqCu0r.srprm.cn
http://yKblU8R2.srprm.cn
http://6QyfQtnB.srprm.cn
http://PrYyaWcf.srprm.cn
http://HbHo1QKF.srprm.cn
http://WDBt7V7j.srprm.cn
http://NVxdjYi9.srprm.cn
http://lYQMihTw.srprm.cn
http://pEnAiet7.srprm.cn
http://IBFmti5p.srprm.cn
http://PvUu9JAH.srprm.cn
http://wqZoYbwG.srprm.cn
http://rHYRn9L9.srprm.cn
http://ReO1kppy.srprm.cn
http://3NzOQb2w.srprm.cn
http://mkQAdi4C.srprm.cn
http://UAgnhFH0.srprm.cn
http://70ZkMmeV.srprm.cn
http://lEGdznNh.srprm.cn
http://PochtOWr.srprm.cn
http://D9j7I1xT.srprm.cn
http://xkbe90nW.srprm.cn
http://rO8NKCEX.srprm.cn
http://Cv7rAMKM.srprm.cn
http://mGEAe5Yu.srprm.cn
http://64mifhBf.srprm.cn
http://IuO9fciH.srprm.cn
http://6Jw9gcpE.srprm.cn
http://j5rLgQle.srprm.cn
http://gQBmieLq.srprm.cn
http://www.dtcms.com/a/371302.html

相关文章:

  • 打造第二大脑读书笔记目录
  • 【Docker】Docker基础
  • 一、CMake基础
  • 【音视频】WebRTC P2P、SFU 和 MCU 架构
  • VBA 自动转化sheet到csv文件
  • rabbitmq 重试机制
  • 《C++进阶之STL》【set/map 使用介绍】
  • 【RabbitMQ】----初识 RabbitMQ
  • WebRTC开启实时通信新时代
  • JVM-默背版
  • Java内存区域与内存溢出
  • Python3使用Flask开发Web项目新手入门开发文档
  • 深入理解跳表:多层索引加速查找的经典实现
  • 从 “Hello AI” 到企业级应用:Spring AI 如何重塑 Java 生态的 AI 开发
  • 大模型架构演进全景:从Transformer到下一代智能系统的技术路径(MoE、Mamba/SSM、混合架构)
  • leetcode 912 排序数组(归并排序)
  • Flutter SDK 安装与国内镜像配置全流程(Windows / macOS / Linux)
  • 【算法】92.反转链表Ⅱ--通俗讲解
  • Spring Cloud Alibaba快速入门02-Nacos(上)
  • Selenium自动化测试
  • B.50.10.11-Spring框架核心与电商应用
  • 芯片ATE测试PAT(Part Average Testing)学习总结-20250916
  • Visual acoustic Field,360+X论文解读
  • Android系统更新系统webview. 2025-09-06
  • Simulink子系统、变体子系统及封装知识
  • 详解 Java 中的 CopyOnWriteArrayList
  • FTL(Flash Translation Layer)
  • C++输出字符串的统一码(Unicode Code)和 ASCII 码
  • 【PCIe EP 设备入门学习专栏 -- 8.1.2 PCIe EP 通路详细介绍】
  • nginx安装部署(备忘)