当前位置: 首页
编程语言
iOS底层实例解析Swift闭包与OC闭包

iOS底层实例解析Swift闭包与OC闭包

热心网友 时间:2026-06-14
转载

iOS底层解析了OCBlock的本质、三种类型(NSGlobalBlock、NSMallocBlock、NSStackBlock)及堆Block的拷贝机制与底层结构,循环引用可通过__weak+__strong或__block解决。Swift闭包为引用类型,捕获值支持let不可变拷贝与直接指针修改,引用类型属性不受捕获列表影响。

基础

首先,理解 Block 的本质:Block 本质上是一个自包含的匿名函数代码块,可以捕获上下文中的常量或变量,像普通对象一样在代码中传递和使用,这在 iOS 开发中非常常见。

全局函数和嵌套函数实际上也是特殊的闭包。闭包有三种形态:

  • 全局函数:有名称,但不捕获任何值。
  • 嵌套函数:有名称,并且能捕获它所在封闭函数域中的值。
  • 闭包表达式:用轻量级语法编写的匿名闭包,能够捕获上下文中的变量或常量。

OC-Block

分类

OC 中的 Block 根据内存位置和引用方式可以分为三类:

NSGlobalBlock

  • 位于全局区。
  • 内部不使用外部变量,或者只使用静态变量和全局变量。

NSMallocBlock

  • 位于堆区。
  • 被强持有。
  • 内部使用局部变量或 OC 属性,可以赋值给用强引用或 copy 修饰的变量。

NSStackBlock

  • 位于栈区。
  • 没有被强持有。
  • 内部使用局部变量或 OC 属性,不能赋值给强引用或 copy 修饰的变量。

下面这个 demo 很直观地展示了这三种 Block:

int a = 10; // 局部变量
void(^Global)(void) = ^{
    NSLog(@"Global");
};
void(^Malloc)(void) = ^{
    NSLog(@"Malloc,%d",a);
};
void(^__weak Stack)(void) = ^{
    NSLog(@"Stack,%d",a);
};
NSLog(@"%@",Global); // <__NSGlobalBlock__: 0x101aa80b0>
NSLog(@"%@",Malloc); // <__NSMallocBlock__: 0x600003187900>
NSLog(@"%@",Stack); // <__NSStackBlock__: 0x7ff7b12c22f0>

接下来重点聊聊堆 Block(NSMallocBlock)的内存管理细节。

NSMallocBlock

Block 何时会被拷贝到堆上?有几个关键时机:

  • 手动调用 copy
  • Block 作为函数返回值。
  • 被强引用或 copy 修饰的变量持有。
  • 系统 API 中使用了 using Block 的场景(如 GCD、动画等)。

判断堆 Block 的依据可以总结为:

  • Block 内部是否使用了外部变量。
  • 使用的变量类型:局部变量、OC 属性、全局变量、静态变量?
  • 是否被强引用或 copy 修饰。

源码探究

拿一个捕获了局部变量的 Block 来拆解其底层实现:

#import 
void test() {
    int a = 10;
    void(^Malloc)(void) = ^{
        NSLog(@"%d",a);
    };
}

通过 clang -rewrite-objc main.m -o main.cpp 生成 C++ 代码,可以清晰看到 Block 的结构:

struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  int a; // 内部存储了变量a
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// 创建Malloc闭包
void(*Malloc)(void) = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a));
// 函数实现
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  NSLog(···);
}

\

打开 LLVM 调试可以看到,这个 Block 原本分配在栈上,随后调用了 objc_retainBlock 方法,而它实际调用的是 _Block_copy 方法。在 Block.h 的源码中,_Block_copy 的注释明确指出:“创建一个基于堆的 Block 副本,或者简单地添加一个对现有 Block 的引用。” 其核心实现如下:

void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, true);
}
static void *_Block_copy_internal(const void *arg, const bool wantsOne) {
    struct Block_layout *aBlock;
    aBlock = (struct Block_layout *)arg;
    // 分配堆内存
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return NULL;
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
    // reset refcount
    result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
    result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
    // isa重新标记为Malloc Block
    result->isa = _NSConcreteMallocBlock;
    _Block_call_copy_helper(result, aBlock);
    return result;
}

Block 的底层结构体是 Block_layout

struct Block_layout {
    void *isa;
    volatile int32_t flags;
    int32_t reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
};

一句话总结:Block 在运行时才被拷贝到堆上,会在堆中开辟新的内存空间,从而延长其生命周期。

循环引用

解决方案

__weak + __strong

思路很简单:在 Block 内部短暂持有 self 的生命周期,利用 weak 自动置空机制避免循环引用。示例代码如下:

self.name = @"YK";
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf;
    strongSelf.callFunc();
};

__block

另一种思路:通过 __block 进行值拷贝,但需要手动在适当位置将变量置空来打破循环。

来看一个使用 __block 的例子,并分析其 C++ 代码:

void test() {
    __block int a = 10;
    void(^Malloc)(void) = ^{
        a++;
        NSLog(@"%d",a);
    };
    Malloc();
}
// 注意第三个参数传入的是__Block_byref_a_0结构体类型的a变量地址
void(*Malloc)(void) =
((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

struct __test_block_impl_0 {
  struct __block_impl impl;
  struct __test_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__attribute__((__blocks__(byref))) __Block_byref_a_0 a =
{(void*)0, (__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

struct __Block_byref_a_0 {
  void *__isa;
  __Block_byref_a_0 *__forwarding;
  int __flags;
  int __size;
  int a; // 值拷贝存储
};

__block 的原理可以概括为:

  • 系统自动创建 __Block_byref_a_0 结构体。
  • 将结构体的指针传给 Block。
  • Block 内部修改的是与原始值同一片内存空间,从而实现变量在 Block 内外共享。

注意点

根据上面的分析很容易得出一个结论:在 OC 的 Block 中,如果捕获的外部变量没有加 __block,编译时就会把变量值拷贝到 Block 内部(值拷贝);如果加了 __block,则传入的是变量指针对应的内存地址(引用拷贝)。看下面这段代码验证:

int a = 1;
__block int b = 2;
void(^Malloc)(void) = ^{
    NSLog(@"a,%d",a);
    NSLog(@"b,%d",b);
};
a = 3;
b = 4;
Malloc();
// 输出:
// a,1
// b,4

Swift-Closure

  • Swift 的闭包表达式语法极为简洁,常见的优化写法包括:
    • 利用上下文推断参数类型和返回值类型。
    • 单表达式闭包隐式返回,可以省略 return
    • 参数名称缩写:用 $0$1 表示按顺序的参数。
    • 尾随闭包:如果函数的最后一个参数是闭包,可以写在括号外面,显著提升可读性。
  • Swift 的闭包是引用类型。这一点可以通过检查是否调用 swift_allocObject 来验证:
// 未调用swift_allocObject
let closure1 = { () -> () in
    print("closure1")
}
// 调用swift_allocObject
let a = 10
let closure2 = { () -> () in
    print("closure2 (a)")
}

捕获值

  • 如果在闭包中用 [variable1, variable2] 的形式捕获外部变量,捕获到的变量是 let 类型,不可变。
  • 如果直接捕获外部变量,获取的是指针,闭包内修改会影响到原始变量。
  • 如果捕获的是引用类型(比如 Class 实例),无论是否使用 [],修改对象的属性都会影响原始对象。

简单验证:

var variable = 10
let closure = { () -> () in
    variable += 1
    print("closure (variable)")
}
closure() // closure 11
print(variable) // 11

这里直接捕获了变量,因此闭包内修改了原始值。如果改成用 [variable] 则编译报错:“可变运算符的左侧不可变”。

var variable = 10
let closure = { [variable] () -> () in
    variable += 1  // ❌ 编译报错
    print("closure (variable)")
}

再看引用类型的例子:

class YKClass {
    var name = "old"
}
let demoC = YKClass()
let closure1 = { [demoC] () -> () in
    demoC.name = "new"
    print("closure1 (demoC.name)")
}
closure1() // closure1 new
print(demoC.name) // new

let closure2 = { () -> () in
    demoC.name = "new2"
    print("closure2 (demoC.name)")
}
closure2() // closure2 new2
print(demoC.name) // new2

可以看到,即使使用了 [demoC] 捕获,依然能修改对象的属性,因为引用类型的“不可变”仅指指针本身不能改变,但指针指向的对象内容仍然可以修改。

来源:https://www.jb51.net/article/267562.htm

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
AWS RDS 数据库配置入门与基础操作指南

AWS RDS 数据库配置入门与基础操作指南

本文介绍了AWSRDS的基本概念与核心价值,即提供托管式关系数据库服务,简化运维。详细阐述了创建RDS实例的关键配置步骤,包括引擎选择、实例规格、存储与网络设置。最后,指导读者如何通过多种方式安全连接至数据库实例,并开始进行数据操作,为后续应用开发奠定基础。

时间:2026-07-10 06:41
PHP MVC中AJAX请求无法调用控制器方法的原因与解决方案

PHP MVC中AJAX请求无法调用控制器方法的原因与解决方案

PHPMVC中AJAX请求返回整页HTML的常见原因是控制器方法未正确输出响应或未终止执行,导致框架渲染视图。解决方法是在控制器中设置JSON响应头、输出数据后调用exit()明确终止,同时前端使用小写url和dataType: "json "。

时间:2026-07-10 06:40
Go语言手动构造rsa.PublicKey:正确初始化大整数模数N完整指南

Go语言手动构造rsa.PublicKey:正确初始化大整数模数N完整指南

手动构造RSA公钥时,模数N为*big Int类型,不能直接使用超长十进制字面量,需通过SetString或UnmarshalText方法解析字符串。公钥指数E可直接赋值,推荐65537。生产环境应使用rsa GenerateKey生成密钥对,避免手动构造引发的安全和格式错误。

时间:2026-07-10 06:39
Go语言实现HTTP定时轮询监控多URL响应时间与状态检测

Go语言实现HTTP定时轮询监控多URL响应时间与状态检测

使用Go语言实现HTTP定时轮询监控,通过按行分割与Tab解析URL列表,避免闭包陷阱和nil指针,每个URL启动独立ticker安全并发请求,并配置超时控制与资源关闭,确保响应时间与状态码准确检测。

时间:2026-07-10 06:39
Tkinter中Label标签在主循环动态更新的正确方法

Tkinter中Label标签在主循环动态更新的正确方法

在Tkinter中正确动态更新标签的方法:将标签组件的textvariable参数绑定到一个StringVar变量,然后通过调用该变量的 set()方法更新其值,界面会自动刷新。这样避免直接修改text属性或调用update()。此做法实现数据与界面的解耦,代码更简洁,响应更及时,避免手动同步的闪烁,推荐做法。

时间:2026-07-10 06:39
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜