赞
踩
之前的Block写的没啥重点,这一次总结一下重点,比如Block如何捕获外界变量,__block的使用
首先先上代码,我们在Block中添加了localA局部变量
int main() { int localA = 7; void(^block)(void) = ^{ NSLog(@"block - %d - %d", localA, globalB); }; block(); localA += 10; block(); return 0; }
发现在外部修改localA并没有影响到Block内部
编译成源码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int localA;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, int flags=0) : localA(_localA) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到Block实现结构体中新增了localA
变量
我们具体看一下函数实现代码
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int localA = __cself->localA; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_tm_rlw5m0t17cg39hpr19j64k140000gn_T_main_e839a5_mi_0, localA);
}
我们分析一下这段代码,在我们int localA = __cself->localA; // bound by copy
,localA 通过拷贝与 struct __main_block_impl_0
绑定,意思是 localA 是拷贝过来的。那么得出结论:在外部改变 localA 不会影响到 block 内部。
当
Block
捕获基本数据类型(如 int、float 等)或结构体时,它会通过值捕获的方式复制变量的当前值。这意味着在 Block
内部使用的是变量捕获时刻的快照。由于是值复制,所以之后即使原始变量的值发生改变,Block 内部的值也不会改变。
一样的来看一个例子
#import <Foundation/Foundation.h> //全局变量 int global_a = 10; //静态全局变量 static int staic_global_a = 20; int main(int argc, const char * argv[]) { @autoreleasepool { //基本数据类型的局部变量 int a = 5; //对象类型的局部变量 //局部静态变量 static int staic_a = 6; void(^block)(void) = ^{ NSLog(@"局部变量.基本数据类型 %d",a); NSLog(@"局部静态变量 %d",staic_a); NSLog(@"全局变量 %d",global_a); NSLog(@"静态全局变量 %d",staic_global_a); }; block(); } return 0; }
编译源码
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int *staic_a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_staic_a, int flags=0) : a(_a), staic_a(_staic_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到只有自动变量a
与静态局部变量staic_a
被加到了Block实现结构体中,同时静态局部变量与Block建立关联的是指针(int *)
,也就是说Block捕获的静态局部变量捕获的是变量的指针,因此当我们对静态局部变量进行修改时,Block内部的静态局部变量的值也会随之改变
//局部静态变量 static int staic_a = 6; void(^block)(void) = ^{ NSLog(@"局部变量.基本数据类型 %d",a); // NSLog(@"局部变量.__unsafe_unretained.对象类型 %@",unsafe_objc); // NSLog(@"局部变量.__strong.对象类型 %@",strong_objc); NSLog(@"局部静态变量 %d",staic_a); NSLog(@"全局变量 %d",global_a); NSLog(@"静态全局变量 %d",staic_global_a); }; a++; staic_a++; global_a++; block();
我们可以看到全局、全局静态变量并没有出现在我们的Block实现结构体中,说明二者无法被捕获
我们先来引出一个问题,如果我们想在Block中修改我们捕获的自动变量该如何实现
如果我们直接进行修改会出现这样的错误
错误是:变量无法被赋值
我们一样给出例子:
int main() { int localA = 7; __block int local__blockB = 8; void(^block)(void) = ^{ NSLog(@"block - %d - %d", localA, local__blockB); }; block(); localA += 10; local__blockB += 10; block(); return 0; }
自由变量 localA 并没有改变(还是 7),但是被 __block 修饰的 local__blockB 改变了(8 += 10 >>> 18)。
查看源码
struct __Block_byref_local__blockB_0 { void *__isa; __Block_byref_local__blockB_0 *__forwarding; int __flags; int __size; int local__blockB; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int localA; __Block_byref_local__blockB_0 *local__blockB; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _localA, __Block_byref_local__blockB_0 *_local__blockB, int flags=0) : localA(_localA), local__blockB(_local__blockB->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_local__blockB_0 *local__blockB = __cself->local__blockB; // bound by ref int localA = __cself->localA; // bound by copy NSLog((NSString *)&__NSConstantStringImpl__var_folders_jv_7x7mpj5d6d14k8m53xr3m4240000gn_T_main___block_71facf_mi_0, localA, (local__blockB->__forwarding->local__blockB)); } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->local__blockB, (void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->local__blockB, 8/*BLOCK_FIELD_IS_BYREF*/);} static struct __main_block_desc_0 { size_t reserved; size_t Block_size; void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); void (*dispose)(struct __main_block_impl_0*); } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; int main() { int localA = 7; __attribute__((__blocks__(byref))) __Block_byref_local__blockB_0 local__blockB = {(void*)0,(__Block_byref_local__blockB_0 *)&local__blockB, 0, sizeof(__Block_byref_local__blockB_0), 8}; void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, localA, (__Block_byref_local__blockB_0 *)&local__blockB, 570425344)); ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); localA += 10; (local__blockB.__forwarding->local__blockB) += 10; ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block); return 0; }
被 __block
修饰的自由变量 local__blockB
竟然变成了一个结构体 struct __Block_byref_local__blockB_0
。
// struct __Block_byref_local__blockB_0 实例化 local__blockB
{
__isa = (void *)0,
__forwarding = (____Block_byref_local__blockB_0 *)&local__blockB,
__flags = 0,
__size = sizeof(__Block_byref_local__blockB_0),
local__blockB = 8 // 这就是我们赋值给 local__blockB 的值 8
}
在这个结构体中主要封装了两个东西——__forwarding指针
与原本的local__blockB变量
结构体中的
local_blockB
是被__block
修饰的实际变量,这里存储的是变量的值。在你的例子中,local__blockB 被初始化或赋值为 8。这个字段代表了原始变量在被
__block 修饰后的存储位置。
我们来看block函数实现中,发现读取__block变量时拐了一个大弯
local__blockB->__forwarding->local__blockB
在修改值的时候也是
(local__blockB.__forwarding->local__blockB) += 10
我们暂且按下不表,这与下面将要讲的Block
的copy
操作有关
不过在这里还是总结一下block对变量的捕获情况
变量类型 | 是否捕获到block内部 | 访问方式 |
---|---|---|
自由变量 | 是 | 值拷贝 |
静态变量 | 是 | 指针拷贝 |
全局变量 | 无法捕获 | 直接使用 |
为了研究 block 的 copy 操作,我们先要搞清楚 block 到底存储在栈上还是堆上???
我们先前讲过了iOS的内存分区,拿一张图回忆一下
【iOS】内存分区
常用级别的 Block 分为三类:
_NSConcreteGlobalBlock
: 全局 block,存储在全局内存中,相当于单例;
_NSConcreteMallocBlock
: 堆 block,存储在堆内存中,是一个带有引用计数的对象,需要自行管理器内存;
_NSConcreteStackBlock
: 栈 block,存储在栈内存中,超出作用域立马销毁。
说完了block的类型,我们来分情况讨论不同情况下block的存储位置
结果:NSGlobalBlock
结果:NSGlobalBlock
结果:NSMallocBlock
结果:NSGlobalBlock
结果:NSGlobalBlock
总结一下:
结论:
1、访问全局变量与没有访问变量是相同的,因此都没有捕获操作,全局变量直接引用
2、由于不创建Block
就没有copy
操作,因此创建Block
变量并且捕获自由变量时Block会被拷贝到堆上,如果没有copy操作使用完就直接释放了
我们这里讲一下为什么我们的Block访问自由变量时会自动进行copy操作存储到堆上
在 Objective-C 中,Block 最初是在栈上创建的。栈上的 Block(NSStackBlock)生命周期与其定义的作用域相关联,一旦该作用域结束,栈上的 Block 将不再有效。这意味着如果你需要在 Block 的定义作用域外使用它,比如将它作为回调传递或保存为后续使用,你需要将它复制到堆上(成为 NSMallocBlock)。
如何理解作用域结束后Block不再有效,如果想要继续使用就要拷贝到堆上?
给出一个例子
// 假设这个方法从网络获取数据 - (void)fetchDataWithCompletion:(void (^)(NSData *data, NSError *error))completion { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 模拟网络请求 NSData *data = [@"Test data" dataUsingEncoding:NSUTF8StringEncoding]; NSError *error = nil; // 模拟一个成功的获取数据的操作 sleep(2); // 模拟耗时操作 // 回到主线程来执行回调 dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(data, error); // 调用 Block 传递数据或错误信息 } }); }); }
当在子线程执行完操作需要回到主线程,我们就用Block进行回调,如果Block在栈上,那么超出作用域就会被销毁,无法回到主线程被调用,因此需要拷贝到堆上
在ARC环境下编译器会自动完成拷贝到堆上的操作,在MRC下需要我们手动拷贝与释放
我们通过[block copy]
操作进行拷贝,不同类型的Block
进行copy
效果也不同
我们通过源码研究一下copy操作
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
简化一下里面的操作
/* 拷贝 Block,或者增加 Block 的引用计数。若需要拷贝,调用拷贝协助方法(如果存在) */ static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; ///Junes 1、若不存在源 Block ,则返回 NULL if (!arg) return NULL; ///Junes 2、将源 Block 指针转换为 (struct Block_layout *) aBlock = (struct Block_layout *)arg; ///Junes 3、若源 Block 的 flags 包含 BLOCK_IS_GC,则其为堆块。 \ /// 此时增加其引用计数,并返回这个源 Block if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } ///Junes 4、源 Block 是全局块,直接返回源 Block(全局 Block 就是一个单例) else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } ///Junes 5、源 Block 是一个栈 Block,执行拷贝操作。首先申请相同大小的内存 struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void *)0; ///Junes 6、使用 memmove 方法将栈区里的源 Block 逐位复制到刚申请的堆区 Block 内存中。这样做是为了保证完全复制所有元数据。 memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first ///Junes 7、更新 result 的 flags。 result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed ///Junes 确保引用计数为 0。注释表示没这个必要,可能因为此时引用计数早已为 0。但是为了防止 bug 被保留下来。 result->flags |= BLOCK_NEEDS_FREE | 1; ///Junes 为 result 的 flags 添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1。表明这是一个堆 Block(一旦引用计数降为 0,则其内存将被回收) ///Junes 8、将 result 的 isa 指向 _NSConcreteMallocBlock。这意味着 result 是一个堆 Block。 result->isa = _NSConcreteMallocBlock; ///Junes 9、如果 result 存在拷贝协助方法,调用它。 /// 如果 block 捕获对象,编译器将会生成这个协助方法。 /// 这个协助方法将会 retain 被捕获的对象。 if (result->flags & BLOCK_HAS_COPY_DISPOSE) { (*aBlock->descriptor->copy)(result, aBlock); } return result; }
源码中对copy操作进行了分类
1、如果源Block不存在则返回NULL
2、如果源 Block 是 _NSConcreteMallocBlock
,增加其引用计数,然后返回源 Block;
3、如果源 Block 是 _NSConcreteGlobalBlock
,直接返回源 Block,因为_NSConcreteGlobalBlock
是一个单例;
4、如果源 Block 是 _NSConcreteStackBlock
,那么操作就比较复杂
- 申请一块相同大小的内存
- 拷贝栈上的block的所有元数据到新申请的内存空间上,也就是将数据拷贝到堆上,堆上的block我们叫做result
- 更新 result 的 flags,确保其引用计数为 0;
- 更新 result 的 flags,添加 BLOCK_NEEDS_FREE,并设置其引用计数为 1;
- 将 result 的 isa 指向
_NSConcreteMallocBlock
。标明 result 是一个堆 Block;- 如果 result 捕获了对象,调用编译器生成的拷贝协助方法 retain 被捕获的对象。
既然我们的Block被拷贝了,那么Block中的捕获的变量也会一起被拷贝到堆区,这里我们直接看一下总结
这里需要注意被捕获的__block结构体,我们分析一下源码
static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2; /* * 当拷贝目标为 __block 修饰变量而生成的结构体时,则执行 */ static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) { struct Block_byref **destp = (struct Block_byref **)dest; struct Block_byref *src = (struct Block_byref *)arg; ///Junes __block 变量结构体还在栈区,拷贝它 if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) { ///Junes 判断这是否为一个弱引用 bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)); ///Junes 申请相同大小的空间 struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak); ///Junes 将新结构体标记为堆,并将引用计数置为 2。一份给调用者,一份给栈。 copy->flags = src->flags | _Byref_flag_initial_value; ///Juens 将栈区结构体与新结构体的 __forwarding 都之上堆区中的新结构体 copy->forwarding = copy; src->forwarding = copy; ///Junes 赋值 size copy->size = src->size; ///Junes 如果是弱引用,isa 指向 _NSConcreteWeakBlockVariable。标记为 Block 的弱引用 if (isWeak) { copy->isa = &_NSConcreteWeakBlockVariable; } ///Junes 如果存在 copy_dispose 内存管理方法,执行 if (src->flags & BLOCK_HAS_COPY_DISPOSE) { ///Junes 将新结构体的内存管理方法指针指向全区源结构体的相应方法 copy->byref_keep = src->byref_keep; copy->byref_destroy = src->byref_destroy; ///Junes 调用源结构体的 byref_keep 方法(也就是 _Block_object_assign),管理被捕获的对象内存。不过会加上 BLOCK_BYREF_CALLER 标记 (*src->byref_keep)(copy, src); } else { ///Junes 仅适用于普通变量(非对象),全字节拷贝 byref_keep _Block_memmove( (void *)©->byref_keep, (void *)&src->byref_keep, src->size - sizeof(struct Block_byref_header)); } } ///Junes 这个结构体已经在堆区,引用计数 +1 else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) { latching_incr_int(&src->forwarding->flags); } // assign byref data block pointer into new Block ///Junes 将源结构体指针也指向堆区的这个新结构体 *destp = src->forwarding; // _Block_assign(src->forwarding, (void **)destp); }
这里解释了拷贝后forwarding
指针的变化
copy->forwarding = copy;
src->forwarding = copy;
当拷贝到堆上后,栈上的forwarding
指向了堆上的新结构体,堆上的结构体仍然指向自身,这里就引出我们在前面一直遗留的问题,forwarding
指针的作用
我们在前面知道了,如果一个block捕获了自由变量,编译器会自动将栈上的block拷贝到堆区,从一个 _NSConcreteStackBlock
变为一个 _NSConcreteMallocBlock
。
被 __block 标记的自由变量 local__blockB,Block 并不是简单的值拷贝,而是拷贝了 local__blockB 这个结构体(自动被重写成 sturct __Block_byref_local__blockB_0
的一个实例,而 __Block_byref_local__blockB_0.local__blockB
承载之前的自由变量 a)的指针。
当变量被拷贝到堆区后,我们会发现一个问题,现在有两个变量,一个在栈上一个在堆上,此时访问 local__blockB 到底是要访问栈上的 local__blockB 还是堆上的 local__blockB 呢?于是, __forwading
登场了。
我们来看一张经典的图
当一个变量被__block
修饰符声明时,编译器会将这个变量包装在一个结构体中,这个结构体除了存储变量值外,还包含一个名为forwarding
的指针。这个forwarding
指针的主要功能是指向包含实际变量值的最新版本的地址。
初始化时,__block
变量仍然存储在栈上,forwarding
指向自身
被拷贝后栈上的forwarding
指向堆上的被拷贝的结构体,因为后续栈上的变量可能被释放
这也使无论变量在栈上还是在堆上,我们都能访问到正确的同一个变量
所以才会存在绕一大圈的访问方法:
(local__blockB.__forwarding->local__blockB) += 10;
void (^stackBlock)(void) = ^{
NSLog(@"This is a block on the stack.");
};
void (^heapBlock)(void) = [stackBlock copy]; // 明确复制到堆
typedef void (^CompletionBlock)(void);
CompletionBlock myFunction() {
return [^{ NSLog(@"Block returned from a function."); } copy]; // 返回时复制到堆
}
CompletionBlock block = myFunction();
block();
如果你将一个栈上的 Block 赋值给一个 __block 修饰的变量,编译器会自动将这个 Block 复制到堆上。__block 变量用于存储指向堆上 Block 的指针。
__block int (^blockVar)(void);
int value = 42;
blockVar = ^{
return value; // 编译器会自动将该 Block 复制到堆上
};
NSLog(@"%d", blockVar()); // 输出 42
如果 Block 内部访问了 __block 修饰的变量,编译器会自动将该 Block 复制到堆上,以确保变量在 Block 执行时是有效的。
__block int value = 42;
int (^blockObj)(void) = ^{
value = 100; // 访问了 __block 变量,编译器会自动将该 Block 复制到堆上
return value;
};
NSLog(@"%d", blockObj()); // 输出 100
如果你将一个 Block 传递给 Grand Central Dispatch (GCD) API (如 dispatch_async),GCD 会自动将该 Block 复制到堆上,以确保在异步执行期间 Block 是有效的。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
int value = 42;
dispatch_async(queue, ^{
NSLog(@"%d", value); // GCD 会自动将该 Block 复制到堆上
});
Block的重点其实就在于捕获自动变量与使用__block修饰时forwarding
指针的变化,还有其发生拷贝的时机:使用__block变量,使用dispatch API, 手动copy与作为函数返回值
另外重要的还有block的循环引用,后面会一起总结循环引用
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。