OC代码写多了,在闭包里用引用捕获就随便了起来,类似的写法,在 cpp 是有问题的
被 C++ lambda捕获的常量的生命周期并不会发生改变,并不会因为捕获有额外的生命周期,该释放的时候还是会释放,如果闭包超出了常量原有的生命周期,调用常量时就会出问题。
问题代码
void test() {
int a = 20;
runAsyncFunc(base::BindLambda([&] {
a = a + 10;
print("%d", a); // 乱码; 引用捕获
});
}
问题分析
如果我们需要再闭包里用到 int a,在 oc 里我们会这么写:
nslog 的时候,a 依旧是 20
- (void)test {
int a = 20;
[xxx runAsyncFunc:^{
a = a + 10;
NSLog(@"%d",a); // 30
}];
}
但是到 cpp 里,如果闭包是引用捕获,就会有问题,下面的方法捕获到 a 的引用后,不会影响其生命周期,a 还是随着函数销毁销毁了。实际使用a 的时候,引用悬空了。
void test() {
int a = 20;
runAsyncFunc(base::BindLambda([&] {
a = a + 10;
print("%d", a); // 乱码; 引用捕获
});
}
如果使用值引用,可以正常获取到值,但是使用的是a 的副本,确实是不会修改到外部的a
void test() {
int a = 20;
runAsyncFunc(base::BindLambda([=] {
int b = a + 10;
print("%d", b); // 30;值捕获
});
}
如果真想修改外部的 a,可以把 a 变成共享指针,这样闭包捕获 a 后,会增加 a 的引用计数,推迟a 的释放
auto is_reported_download_first_data = std::make_shared<bool>(false);
void test() {
auto a = std::make_shared<int>(20);
runAsyncFunc(base::BindLambda([=] {
*a = *a + 10;
print("%d", *a); // 30;捕获共享指针
});
}
至于 oc 的 block,其实函数结束时,a 也释放了。
能正常执行,其实是因为在 block 内部创建了一个 a 的副本,确实没有真正修改到外部的 a
- (void)test {
int a = 20;
[xxx runAsyncFunc:^{
a = a + 10; // 这个 a 其实是副本,不是外面的 a,有兴趣的可以打印 a 的地址验证
NSLog(@"%d",a); // 30
}];
}
如果 oc 中想要真的修改的外部的 a,其实也有办法,就是用__block 修饰
- (void)test {
__block int a = 20;
[xxx runAsyncFunc:^{
a = a + 10; // 这个 a 其实是副本,不是外面的 a,有兴趣的可以打印 a 的地址验证
NSLog(@"%d",a); // 30
}];
}
总结
Objective-C 的 Block 捕获机制
在Objective-C中,Block会自动管理被捕获变量的生命周期。具体来说:
-
值捕获:当一个局部变量被Block捕获时,Block会创建该变量的一个副本(即值捕获)。这意味着即使原始变量的作用域结束,Block中仍然保留一份有效的副本。
-
引用捕获:对于对象类型(如
id或指针),Block默认捕获的是引用,而不是值。但由于ARC(Automatic Reference Counting)的存在,只要Block本身存活,被捕获的对象也会被正确地保留和释放。
因此,在Objective-C中,无论是值捕获还是引用捕获,Block都能确保被捕获的变量在其生命周期内是安全的。
C++ 的 Lambda 捕获机制
相比之下,C++ 的 Lambda 表达式对变量的捕获方式更为严格,且不会自动延长被捕获变量的生命周期。以下是两种捕获方式的具体行为:
- 值捕获(
[=]或[a]):
-
当使用值捕获时,Lambda会在捕获时创建变量的一个副本。这个副本的生命周期独立于原始变量。
-
因此,即使原始变量的作用域结束,Lambda中的副本仍然有效。
- 引用捕获(
[&]或[&a]):
-
当使用引用捕获时,Lambda捕获的是变量的引用,而不是值。
-
如果原始变量的作用域结束,其内存可能被释放,而Lambda仍然持有对该内存的引用。此时访问该引用会导致未定义行为(通常是乱码或崩溃)。