【iOS底层探索- Block原理分析-循环引用】

news2025/1/11 0:35:39

文章目录

  • 前言
  • 准备工作
  • 1. Block的分类
  • 2. Block的内存分析
    • 捕获外部变量引用计数的变化
    • 堆栈释放的差异
    • 总结
  • 3. Block的循环引用
    • 3.1 什么是循环引用?
      • 案例引入
    • 循环引用解决方法
      • 1. 强弱共舞
      • 2. 手动中断循环引用
      • 3. 参数形式解决循环引用(block传参)
    • Block循环引用案例
  • 总结

前言

本来打算这周看看源码的,因为某些原因进度耽误了,另外强调了 GCD多线程,Runtime RunLoop,BLock都是面试必问必考的点,想起之前看的比较简略,就借着Block的循环引用顺带复习一下Block的整个流程。

在学习之前 对于ARC以及涉及到的属性关键字都要有了解还是要有了解的基础,尤其是weak。

准备工作

weak的实现原理
iOS ARC实现
iOS Block基础再探究

1. Block的分类

简单复习
block的分类主要分为以下三种

  • __NSGlobalBlock__ 全局block
  • __NSStackBlock__ 栈区block
  • __NSMallocBlock__ 堆区block

他们的分类原因和block的定义有关系 block是一个带有自动变量值的匿名函数

  • 什么是带有自动变量?
  • 带有自动变量值”在Block中表现为“截取自动变量值

例如如下blk即截取了外部变量。

- (void)mallocBlock {
    NSString* str = @"mallocBlk";
    void (^glbBlk)(void) = ^{
        NSLog(@"%@", str);
    };
    NSLog(@"%@", glbBlk);
}

Block分类简单总结如下

  • A、没有引用外部变量 — block存放在全局区
  • B、引用了外部变量----显式声明为weak类型的block则存放在区,反之则是存在在堆区的,也就是说block是strong类型的。

2. Block的内存分析

捕获外部变量引用计数的变化

ARC环境,案例中定义了两个block,跟踪objc的引用计数变化。

#pragma mark- Blk内存分析
- (void)blkMemory {
    NSObject *objc = [NSObject new];
       NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1

       void(^strongBlock)(void) = ^{
           NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
       };
       strongBlock();

       void(^__weak weakBlock)(void) = ^{
           NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
       };
       weakBlock();
       
       void(^mallocBlock)(void) = [weakBlock copy];
       mallocBlock();
}

运行结果如下:
请添加图片描述

  • 第一个打印为1就是简单创建引用计数+1
  • 第二个为3:strongBlock是一个堆区的block这里捕获了objc这个外部变量会进行加一,这里会把栈区的objc拷贝到堆区又进行了加一,所以打印结果为 3。
  • 打印 4是因为这里weakBlock是栈block,没有进行拷贝只是捕获+1,所以为4。
  • 最后打印5是因为[weakBlock copy]进行了拷贝操作,再赋值给mallocBlock也是+1操作,所以打印结果为 5。

如何区分堆栈Blk 这个需要了解blk的实现过程,在之前已作学习。

堆栈释放的差异

  • 差异1
#pragma mark -堆栈释放差异
- (void)blockDemo1 {
    int a = 777;
    void(^__weak weakBlock)(void) = nil;
    {
        void(^__weak strongBlock)(void) = ^{
            NSLog(@"%d", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1 - %@ - %@", weakBlock,strongBlock);
    }
    weakBlock();
}

请添加图片描述
该段代码的运行并无任何问题:

  • 声明了一个weakBlock,该block为NSStackBlock;
  • 在代码块中,定义了strongBlock,其也为NSStackBlock
  • 对weakBlock进行了赋值,此时两个block均指向同一个NSStackBlock
  • 因为这两个栈block的生命周期到blockDemo方法运行结束,并不会被提前释放;(特地查了它的生命周期当创建 NSStackBlock 的函数返回时,栈上分配的内存将被释放,NSStackBlock 对象也将被销毁。)
  • 所以调用weakBlock()可以正常运行,并能够输出a的值。
  • 差异2

对上面的案例strongBlock修改成了NSMallocBlock,取消了__weak修饰符。

- (void)blockDemo2 {
    int a = 0;
    void(^__weak weakBlock)(void) = nil;
    {
        void(^strongBlock)(void) = ^{
            NSLog(@"---%d", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1 - %@ - %@", weakBlock,strongBlock);
    }
    weakBlock();
}

请添加图片描述

案例崩溃 ,lldb调试发现在代码块{}结束之前 调用p weakBlock都是有地址的,但是出了代码块{}发现就没有了。
请添加图片描述

结合lldb调试结果分析:

  • strongBlockNSMallocBlock,其生命周期范围在代码块{}内,也就是出了代码块其就会被释放;
  • 在代码块中对weakBlock进行了赋值,指针拷贝,指向了对应的NSMallocBlock,但是并没有强引用指向这个block(__weak修饰符不会带有强引用,创建暨被释放)
    • 这里的指针拷贝可以从下图看到,二者指向相同的一片内存地址,
      请添加图片描述
  • 代码块执行完毕后,该NSMallocBlock就会被释放,此时weakBlock指向的对象已经被释放,形成野指针,所以无法正常执行。

最后把两个block的__weak都去掉,则能够正常运行并打印结果

- (void)blockDemo3 {
    int a = 0;
    void(^weakBlock)(void) = nil;
    {
        void(^strongBlock)(void) = ^{
            NSLog(@"---%d", a);
        };
        weakBlock = strongBlock;
        NSLog(@"1 - %@ - %@", weakBlock, strongBlock);
    }
    weakBlock();
}

正确结果

上面的例子崩溃了,是因为堆区的 block出了作用域,被释放了。

此时的赋值,weakBlock对堆中的block进行了强引用,代码块运行结束后不会释放掉,也就不存在野指针的问题了。

总结

  • block分为全局 block、堆 block、栈 block
  • block可以捕获外部变量
  • 堆区的 block捕获外部变量会拷贝到堆区引计数+1
  • block 在使用的时候,堆block注意作用域的问题,弱引用指针赋值,出了作用域就释放了,可以通过强引用解决

3. Block的循环引用

  • 参考自:iOS_Block循环引用

3.1 什么是循环引用?

  • 正常情况对于对象的持有和释放是如下逻辑
    请添加图片描述
  • 循环引用就是对面持有导致对象不能及时的正常释放,容易造成内存泄漏。
    在这里插入图片描述

案例引入

如下代码可能造成循环引用

#import "ViewController.h"
typedef void (^TBlock)(void);
@interface ViewController ()
@property (nonatomic, strong) TBlock block;
@property (nonatomic, strong) NSString *name;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    // 循环引用
    self.name = @"ViewController";
    self.block = ^() {
        NSLog(@"%@", self.name);
    };
    self.block();
}

self持有了block,block持有了self,导致循环引用。 编译器也会提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle
请添加图片描述

  • 如果我们取消某一方的持有即可取消循环引用,恢复到正常的逻辑里面去。
 // 不会引起循环引用
    void(^blk1)(void);
    blk1 = ^() {
        NSLog(@"%@", self.name);
    };
    blk1();

这个案例就没有出现循环引用是因为当前self,也就是ViewController并没有对block进行强持有,block的生命周期只在viewDidLoad方法内,viewDidLoad方法执行完,block就会释放。

循环引用解决方法

在开始也提到过weak是基础,weak也是最为熟知的解决循环引用的方法,__weak不会进行引用计数的操作

  • weak的使用
    __weak typeof(self) weakSelf = self;

typeof(self)typeof 是一个运算符,用于获取表达式的类型。在这种情况下,表达式是 self,它代表当前对象的引用

避免循环引用方法:

 __weak typeof(self) weakSelf = self;
    self.block = ^(){
        NSLog(@"%@", weakSelf.name);
    } ;
    self.block();

此时self持有blockblock弱引用self,弱引用会自动变为nil,强持有中断,所以不会引起循环引用。

  • 反面案例

之前在学习GCD的时候学到过将其他线程的麻烦的方法执行完之后再次回到主线程,但是万一在其他线程执行的过程里面ViewController被销毁的话是什么情况,由于ARC不能显式调用dealloc 我们模拟一下延时。

 。。。。。。。
	__weak typeof(self) weakSelf = self;
    self.block = ^(){
    // 延迟2秒钟
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( 2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakSelf.name);
        });
    };
    self.block();
    [self fakeDealloc];
}
- (void)fakeDealloc {
    NSLog(@"调用了dealloc 模拟ViewController模拟销毁");
    // 模拟viewController被销毁
    self.name = nil;
}

dealloc方法调用了,VC 已经销毁了,block 也就释放了,内部的延时函数里面的打印还来不及打印,所以 name为 null.
请添加图片描述
虽然没有引起循环引用,但是block中延迟2秒钟执行任务,如果此时ViewController被销毁,此时block已经无法获取ViewController的属性name,很不合理。

1. 强弱共舞

为了解决上面的问题 由此引出了强弱共舞请添加图片描述
则一切就会正常打印。

原理:因为__weak会自动置为nil,所以这里使用__strong(strong-weak-dance)暂时延长 self的生命周期,使得可以正常打印。

为什么强弱共舞能够避免循环引用,不是也调用了self? 因为这里strongSelf是一个临时的变量,出了作用域也跟着释放了,所以不会出现循环引用🐮

简单分析:

  • 在完成block中的操作之后,才调用了dealloc方法。添加strongWeak之后,持有关系为:self -> block -> strongWeak -> weakSelf -> self
  • weakSelf被强引用了就不会自动释放,因为strongWeak只是一个临时变量,它的声明周期只在block内部,block执行完毕后,strongWeak就会释放,而弱引用weakSelf也会自动释放

2. 手动中断循环引用

    self.name = @"ViewController";
    __block ViewController * VC = 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(@"%@", ctrl.name);
            VC = nil;
        });
    };
    self.block();

这里使用一个临时的VC变量之后,持有关系为: self --> block --> VC–> selfVCblock使用完成后就被置为nilblock不构成对self的持有关系了,因此这里就不构成循环引用问题。

__block是因为需要对外部变量,进行赋值操作

3. 参数形式解决循环引用(block传参)

- (void)parameterMethod {
    self.name = @"ViewController";
    // blk传参解决循环引用
    // 循环引用
    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进行持有。请添加图片描述

Block循环引用案例

  • 静态变量持有
    // 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();
}
  • doWork内部,__strong typeof(self) strongSelf = weakSelf;
  • 用强引用持有了weakSelf,和前的情况类似,strongSelf的生命周期也就在doWork方法内;
  • 这里需要注意的是,doStudent这个内部block调用了外部变量,所以他会从栈block copy到堆中,从而导致strongSelf的引用计数增加,无法释放掉,进而导致循环引用!

总结

通过今天对Block的再学习,也算是复习了block的小知识点,同时学习了几种循环引用出现的场景和如何解决循环引用的方法,记录并复习。

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

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

相关文章

汇编指令执行过程及CS与IP和DS寄存器关系与变化

内存指令及寄存器初始值: CS:2000H IP:0 DS:1000H AX:0 BX:0 上面在内存中的汇编指令是如何执行的? 验证: 在debug下用a指令先向内存写入下面指令,然后用u指令查看 mov ax,2000 mov ds,ax mov ax,[0008] mov ax,[0002] 在debug下用a指令先向内存写入下面指令,然后用u指…

Computer之Compilation:Cmake的简介、安装、案例应用之详细攻略

Computer之Compilation:Cmake的简介、安装、案例应用之详细攻略 目录 Cmake的简介 Cmake的安装 1、官方下载 2、执行安装程序,并按照提示进行安装 3、验证测试 Cmake的案例应用 Cmake的简介 CMake(Cross-platform Make)是一…

【嵌入式烧录/刷写文件】-1.7-将一个文本文件转换为Motorola S-record(S19/SREC/mot/SX)文件

案例背景(共5页精讲): 有如下两个文本文件(*.txt,*.ini,*.asc…)转换成Motorola S-record(S19/SREC/mot/SX)文件。常用于Key密钥,signature签名…的导入,或对一段数据计算出hex记录的最后一个字…

服务器性能优化方法

文章目录 服务器性能优化方法什么是服务器并发处理能力?什么方法衡量服务器的并发能力?怎么提高服务器的并发处理能力?**1,提高CPU并发计算能力**(1)多进程&多线程(2)减少进程切…

spring cloud搭建(eureka)

天行健,君子以自强不息;地势坤,君子以厚德载物。 每个人都有惰性,但不断学习新东西是好好生活的根本,共勉! 文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习…

力扣高频SQL50题(基础版)——第二天

力扣高频SQL50题(基础版)——第二天 1 文章浏览Ⅰ 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # Write your MySQL query statement below SELECT distinct author_id id FROM Views WHERE author_idviewer_id ORDER BY id asc1.3 运行截图 2 无…

基于MSP430送药小车 ----- 基础篇【2021年全国电赛(F题)】

文章目录 一、赛题1. 任务2. 要求3. 说明 二、构思 分析1. 引脚利用2. PID算法3. 灰度循迹及标志位4. 视觉模块5. 直角转弯、原地转向 三、硬件清单四、逻辑设计1. 近端送药2. 中端送药3. 远端送药 五、程序设计1. OpenMV2. 灰度循迹3. 装药卸药 总结 一、赛题 1. 任务 设计并…

Linux Socket 分包 和 粘包 问题 - 解决方案

分包和粘包在TCP网络编程中是非常常见的,分包会造成 接收端 接收的数据不全的问题,粘包会造成接收多余的数据的文件。 这里做一个举例,例如客户端A要发送字符串“helloworld”给服务器B,A是一次性发送,但TCP有可能会将…

A Unified Conditional Framework for Diffusion-based Image Restoration

A Unified Conditional Framework for Diffusion-based Image Restoration (Paper reading) Yi Zhang, CUHK, CN, arXiv2023, Cited:0, Code, Paper 1. 前言 最近,扩散概率模型(Diffusion Probabilistic Models,DPMs)在图像生成…

Android 自定义View 之 饼状进度条

饼状进度条 前言正文一、XML样式二、构造方法三、测量四、绘制① 绘制描边① 绘制进度 五、API方法六、使用七、源码 前言 前面写了圆环进度条,这次我们来写一个饼状进度条,首先看一下效果图: 正文 效果图感觉怎么样呢?下面我们…

GLTF/GLB模型轻量化简明教程

GLB 文件格式很方便,因为它包含渲染所需的所有文件,包括纹理。 但是,根据用途,你可能希望简化文件,因为它有时非常详细。 在本文中,我将使用 gltf-transform 来执行简化,并且假设你使用的是 Wi…

0x23 Read Version Information Service

0x23 Read Version Information Service ReadMemoryByAddress服务允许客户端通过提供的起始地址和要读取的内存大小向服务器请求内存数据。 ReadMemoryByAddress请求消息用于请求由参数memoryAddress和memorySize标识的服务器的内存数据。 用于memoryAddress和memorySize参数的…

SAP-MM-原始接受订单

业务场景: 供应商是强势供应商,产品紧缺,订购货物需要自提,运损也归我们公司,而且立刻付款,那么就不能按以往操作,等供应商送货,再开票 我们在付款,那么SAP如何快速实现…

JS 排序算法

在前端工作中算法不常用,但是排序可能会经常会用,下面学习几种常用算法。 引用借鉴:js的五种排序方法_js排序_木可生森的博客-CSDN博客 JS 常见的排序算法_js排序算法_东风过境F的博客-CSDN博客 1.冒泡排序: 思路:逐次…

儿童节快乐,基于CSS3绘制一个游乐场动效界面

0️⃣写在前面 让代码创造童话,共建快乐世界。六一儿童节——这是属于孩子们的节日,也是属于我们大人的节日。让我们一起「致童真」,用代码(HTMLCSSJS)创造出一个游乐场,让这个世界多一份快乐和惊喜&#x…

如何把vue项目部署服务器(宝塔面板)上

一,vue项目打包 首先我们把准备好的vue项目进行打包: 输入命令:npm run build 生成dist文件 二、进入宝塔管理界面,点击网站,然后点击添加站点 三。按下面输入 点设置 四。 输入好点添加,注意&#x…

【Python开发】FastAPI 04:响应模型

响应模型是指在接口调用之后,服务器返回给客户端的数据模型。这个数据模型可以是一个简单的字符串,也可以是一个复杂的数据结构,如 JSON 或 XML 格式的数据。本篇文章将详细介绍 FastAPI 中的响应模型。 目录 1 响应模型 1.1 response_mode…

HTML--Java EE

目录 一、认识 HTML 标签 二、HTML 文件基本结构 三、开发者工具 四、HTML常见标签 1.注释标签 2.标题标签(h1-h6) 3.段落标签(p) 4.换行标签 5.格式化标签 6.图片标签(img) 6.1网络路径 6.2绝…

Mysql索引慢解决

索引慢解决 explain关键字 explainSQL id执行顺序,id相同从上到下;id不同,如果是子查询id序号会递增,id值越大优先级越高;id相同和不同都存在时,id相同理解为一组,从上往下顺序执行&#xff0…

最优化——几种重要的凸集

引言 这是中科大最优化理论的笔记,中科大凌青老师的凸优化课程,详尽易懂,基础扎实。不论是初学者还是从业多年的人,都值得系统地好好学一遍。 本文介绍种重要的凸集:超平面与半空间、球和椭球、多面体、单纯形。 超平面与半空间…